diff --git a/scripts/release-video-suite.js b/scripts/release-video-suite.js index d4975d5a..39f40d77 100644 --- a/scripts/release-video-suite.js +++ b/scripts/release-video-suite.js @@ -5,9 +5,7 @@ const fs = require('fs'); const path = require('path'); const { spawnSync } = require('child_process'); -const RELEASE = '2.0.0-rc.1'; const SCHEMA_VERSION = 'ecc.release-video-suite.v1'; -const VIDEO_MANIFEST_PATH = `docs/releases/${RELEASE}/video-suite-production.md`; const HYPERGROWTH_DOC_PATH = 'docs/releases/2.0.0/ecc-2-hypergrowth-release-command-center.md'; const REQUIRED_DOC_MARKERS = [ @@ -320,7 +318,7 @@ function usage() { console.log([ 'Usage: node scripts/release-video-suite.js [options]', '', - 'Validates the ECC 2.0 release video production lane without committing raw media paths.', + 'Validates the ECC 2.0 release video production lane for the package.json release version without committing raw media paths.', '', 'Options:', ' --format Output format (default: text)', @@ -455,6 +453,28 @@ function safeParseJson(text) { } } +function resolveRelease(packageJson, options = {}) { + if (typeof options.release === 'string' && options.release.trim()) { + return options.release.trim(); + } + + return typeof packageJson.version === 'string' ? packageJson.version.trim() : ''; +} + +function releaseDirFor(release) { + return `docs/releases/${release}`; +} + +function releasePathsFor(release) { + const releaseDir = releaseDirFor(release); + + return { + videoManifestPath: `${releaseDir}/video-suite-production.md`, + previewManifestPath: `${releaseDir}/preview-pack-manifest.md`, + launchChecklistPath: `${releaseDir}/launch-checklist.md`, + }; +} + function lineNumberForIndex(text, index) { return text.slice(0, index).split('\n').length; } @@ -841,17 +861,19 @@ function buildReport(options = {}) { const suiteRoot = options.suiteRoot ? path.resolve(options.suiteRoot) : ''; const skipProbe = Boolean(options.skipProbe); const packageJson = safeParseJson(readText(rootDir, 'package.json')) || {}; + const release = resolveRelease(packageJson, options); + const releasePaths = releasePathsFor(release); const packageScripts = packageJson.scripts || {}; const packageFiles = Array.isArray(packageJson.files) ? packageJson.files : []; - const manifest = readText(rootDir, VIDEO_MANIFEST_PATH); + const manifest = readText(rootDir, releasePaths.videoManifestPath); const hypergrowth = readText(rootDir, HYPERGROWTH_DOC_PATH); const missingDocMarkers = REQUIRED_DOC_MARKERS.filter(marker => !manifest.includes(marker)); const forbiddenPaths = scanForbiddenPaths(rootDir, [ - VIDEO_MANIFEST_PATH, + releasePaths.videoManifestPath, HYPERGROWTH_DOC_PATH, - `docs/releases/${RELEASE}/preview-pack-manifest.md`, - `docs/releases/${RELEASE}/launch-checklist.md`, + releasePaths.previewManifestPath, + releasePaths.launchChecklistPath, ]); const sourceAssets = inspectSourceAssets(sourceRoot, skipProbe); const suiteArtifacts = inspectSuiteArtifacts(suiteRoot, skipProbe); @@ -875,7 +897,7 @@ function buildReport(options = {}) { 'video-suite-manifest-present', manifest && missingDocMarkers.length === 0 ? 'pass' : 'fail', manifest && missingDocMarkers.length === 0 - ? `${VIDEO_MANIFEST_PATH} includes the required production markers` + ? `${releasePaths.videoManifestPath} includes the required production markers` : `missing markers: ${missingDocMarkers.join(', ') || 'manifest file missing'}`, 'Restore the video production manifest and required production markers.' ), @@ -960,7 +982,7 @@ function buildReport(options = {}) { return { schema_version: SCHEMA_VERSION, - release: RELEASE, + release, generatedAt: options.generatedAt || new Date().toISOString(), root: rootDir, sourceRootConfigured: Boolean(sourceRoot), @@ -1090,6 +1112,7 @@ module.exports = { REQUIRED_SOURCE_ASSETS, REQUIRED_SUITE_ARTIFACTS, buildReport, + releasePathsFor, parseArgs, renderText, summarizeReport, diff --git a/tests/scripts/release-video-suite.test.js b/tests/scripts/release-video-suite.test.js index cc114399..ca793aec 100644 --- a/tests/scripts/release-video-suite.test.js +++ b/tests/scripts/release-video-suite.test.js @@ -19,6 +19,13 @@ const { summarizeReport, } = require(SCRIPT); +const CURRENT_RELEASE = require(path.join(__dirname, '..', '..', 'package.json')).version; +const RC_RELEASE = '2.0.0-rc.1'; + +function releaseDirFor(release) { + return `docs/releases/${release}`; +} + function createTempDir(prefix) { return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); } @@ -33,31 +40,39 @@ function writeFile(rootDir, relativePath, content = 'fixture') { fs.writeFileSync(targetPath, content); } -function seedRepo(rootDir, overrides = {}) { +function videoManifestContent(extra = '') { + return [ + '# ECC 2.0 Video Suite Production Manifest', + 'ECC_VIDEO_SOURCE_ROOT', + 'ECC_VIDEO_RELEASE_SUITE_ROOT', + 'Primary launch video', + 'video-use compatible workflow', + 'Self-Eval Gate', + 'Do Not Publish If', + 'Do not commit raw footage, transcript JSON, or timeline exports', + extra, + ].join('\n'); +} + +function seedRepo(rootDir, overrides = {}, options = {}) { + const release = options.release || CURRENT_RELEASE; + const releaseDir = releaseDirFor(release); const files = { 'package.json': JSON.stringify({ name: 'ecc-universal', + version: release, files: ['scripts/release-video-suite.js'], scripts: { 'release:video-suite': 'node scripts/release-video-suite.js', }, }, null, 2), - 'docs/releases/2.0.0-rc.1/video-suite-production.md': [ - '# ECC 2.0 Video Suite Production Manifest', - 'ECC_VIDEO_SOURCE_ROOT', - 'ECC_VIDEO_RELEASE_SUITE_ROOT', - 'Primary launch video', - 'video-use compatible workflow', - 'Self-Eval Gate', - 'Do Not Publish If', - 'Do not commit raw footage, transcript JSON, or timeline exports', - ].join('\n'), + [`${releaseDir}/video-suite-production.md`]: videoManifestContent(), 'docs/releases/2.0.0/ecc-2-hypergrowth-release-command-center.md': [ 'Keep raw absolute paths out of public docs', 'Pick final video cuts, upload after approval, and attach public URLs', ].join('\n'), - 'docs/releases/2.0.0-rc.1/preview-pack-manifest.md': 'video-suite-production.md', - 'docs/releases/2.0.0-rc.1/launch-checklist.md': 'release video suite', + [`${releaseDir}/preview-pack-manifest.md`]: 'video-suite-production.md', + [`${releaseDir}/launch-checklist.md`]: 'release video suite', }; for (const [relativePath, content] of Object.entries({ ...files, ...overrides })) { @@ -171,6 +186,7 @@ function runTests() { }); assert.strictEqual(report.schema_version, 'ecc.release-video-suite.v1'); + assert.strictEqual(report.release, CURRENT_RELEASE); assert.strictEqual(report.ready, true); assert.strictEqual(report.mediaPathsRedacted, true); assert.ok(report.checks.every(check => check.status === 'pass')); @@ -194,6 +210,33 @@ function runTests() { } })) passed++; else failed++; + if (test('release override keeps rc.1 video fixtures testable', () => { + const rootDir = createTempDir('release-video-rc-'); + const sourceRoot = createTempDir('release-video-source-'); + const suiteRoot = createTempDir('release-video-suite-'); + + try { + seedRepo(rootDir, {}, { release: RC_RELEASE }); + seedMedia(sourceRoot, suiteRoot); + + const report = buildReport({ + root: rootDir, + release: RC_RELEASE, + sourceRoot, + suiteRoot, + skipProbe: true, + generatedAt: '2026-05-19T00:00:00.000Z', + }); + + assert.strictEqual(report.release, RC_RELEASE); + assert.strictEqual(report.ready, true); + } finally { + cleanup(rootDir); + cleanup(sourceRoot); + cleanup(suiteRoot); + } + })) passed++; else failed++; + if (test('publish candidate videos require visual blank-frame QA', () => { const publishVideos = REQUIRED_PUBLISH_CANDIDATES.filter(candidate => candidate.kind === 'video'); @@ -231,18 +274,9 @@ function runTests() { const suiteRoot = createTempDir('release-video-suite-'); try { + const releaseDir = releaseDirFor(CURRENT_RELEASE); seedRepo(rootDir, { - 'docs/releases/2.0.0-rc.1/video-suite-production.md': [ - '# ECC 2.0 Video Suite Production Manifest', - 'ECC_VIDEO_SOURCE_ROOT', - 'ECC_VIDEO_RELEASE_SUITE_ROOT', - 'Primary launch video', - 'video-use compatible workflow', - 'Self-Eval Gate', - 'Do Not Publish If', - 'Do not commit raw footage, transcript JSON, or timeline exports', - '/Users/affoon/private-media', - ].join('\n'), + [`${releaseDir}/video-suite-production.md`]: videoManifestContent('/Users/affoon/private-media'), }); seedMedia(sourceRoot, suiteRoot); @@ -283,6 +317,7 @@ function runTests() { const parsed = JSON.parse(output); assert.strictEqual(parsed.ready, true); + assert.strictEqual(parsed.release, CURRENT_RELEASE); assert.strictEqual(parsed.sourceRootConfigured, true); assert.strictEqual(parsed.suiteRootConfigured, true); assert.strictEqual(parsed.sourceAssetSummary.present, REQUIRED_SOURCE_ASSETS.length);