mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-30 19:00:57 +08:00
fix(release): derive approval gate paths from version (#2383)
Co-authored-by: jan <jan@w-saxs001.local>
This commit is contained in:
parent
8973d0f6c5
commit
9896644dab
@ -5,13 +5,8 @@ const crypto = require('crypto');
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const RELEASE = '2.0.0-rc.1';
|
|
||||||
const RELEASE_DIR = `docs/releases/${RELEASE}`;
|
|
||||||
const SCHEMA_VERSION = 'ecc.release-approval-gate.v1';
|
const SCHEMA_VERSION = 'ecc.release-approval-gate.v1';
|
||||||
const SCRIPT_PATH = 'scripts/release-approval-gate.js';
|
const SCRIPT_PATH = 'scripts/release-approval-gate.js';
|
||||||
const OWNER_PACKET_PATH = `${RELEASE_DIR}/owner-approval-packet-2026-05-19.md`;
|
|
||||||
const URL_LEDGER_PATH = `${RELEASE_DIR}/release-url-ledger-2026-05-19.md`;
|
|
||||||
const PREVIEW_MANIFEST_PATH = `${RELEASE_DIR}/preview-pack-manifest.md`;
|
|
||||||
const REQUIRED_COMMAND = 'npm run release:approval-gate -- --format json';
|
const REQUIRED_COMMAND = 'npm run release:approval-gate -- --format json';
|
||||||
|
|
||||||
const REQUIRED_DECISIONS = [
|
const REQUIRED_DECISIONS = [
|
||||||
@ -87,20 +82,19 @@ const REQUIRED_URL_SURFACES = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const ANNOUNCEMENT_FILES = [
|
const ANNOUNCEMENT_FILE_NAMES = [
|
||||||
`${RELEASE_DIR}/release-notes.md`,
|
'release-notes.md',
|
||||||
`${RELEASE_DIR}/x-thread.md`,
|
'x-thread.md',
|
||||||
`${RELEASE_DIR}/linkedin-post.md`,
|
'linkedin-post.md',
|
||||||
`${RELEASE_DIR}/article-outline.md`,
|
'article-outline.md',
|
||||||
`${RELEASE_DIR}/partner-sponsor-talks-pack.md`,
|
'partner-sponsor-talks-pack.md',
|
||||||
'docs/business/social-launch-copy.md',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
function usage() {
|
function usage() {
|
||||||
console.log([
|
console.log([
|
||||||
'Usage: node scripts/release-approval-gate.js [--format <text|json>] [--root <dir>]',
|
'Usage: node scripts/release-approval-gate.js [--format <text|json>] [--root <dir>]',
|
||||||
'',
|
'',
|
||||||
'Final approval gate for ECC 2.0 rc.1 publication and outbound actions.',
|
'Final approval gate for the release version declared by package.json.',
|
||||||
'',
|
'',
|
||||||
'Options:',
|
'Options:',
|
||||||
' --format <text|json> Output format (default: text)',
|
' --format <text|json> Output format (default: text)',
|
||||||
@ -195,6 +189,32 @@ 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 {
|
||||||
|
ownerPacketPath: `${releaseDir}/owner-approval-packet-2026-05-19.md`,
|
||||||
|
urlLedgerPath: `${releaseDir}/release-url-ledger-2026-05-19.md`,
|
||||||
|
previewManifestPath: `${releaseDir}/preview-pack-manifest.md`,
|
||||||
|
announcementFiles: [
|
||||||
|
...ANNOUNCEMENT_FILE_NAMES.map(fileName => `${releaseDir}/${fileName}`),
|
||||||
|
'docs/business/social-launch-copy.md',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeLabel(value) {
|
function normalizeLabel(value) {
|
||||||
return String(value)
|
return String(value)
|
||||||
.replace(/[`*_]/g, '')
|
.replace(/[`*_]/g, '')
|
||||||
@ -366,11 +386,13 @@ function topActionsForChecks(checks) {
|
|||||||
function buildReport(options = {}) {
|
function buildReport(options = {}) {
|
||||||
const rootDir = path.resolve(options.root || process.cwd());
|
const rootDir = path.resolve(options.root || process.cwd());
|
||||||
const packageJson = safeParseJson(readText(rootDir, 'package.json')) || {};
|
const packageJson = safeParseJson(readText(rootDir, 'package.json')) || {};
|
||||||
|
const release = resolveRelease(packageJson, options);
|
||||||
|
const releasePaths = releasePathsFor(release);
|
||||||
const packageScripts = packageJson.scripts || {};
|
const packageScripts = packageJson.scripts || {};
|
||||||
const packageFiles = Array.isArray(packageJson.files) ? packageJson.files : [];
|
const packageFiles = Array.isArray(packageJson.files) ? packageJson.files : [];
|
||||||
const ownerPacket = readText(rootDir, OWNER_PACKET_PATH);
|
const ownerPacket = readText(rootDir, releasePaths.ownerPacketPath);
|
||||||
const ledger = readText(rootDir, URL_LEDGER_PATH);
|
const ledger = readText(rootDir, releasePaths.urlLedgerPath);
|
||||||
const manifest = readText(rootDir, PREVIEW_MANIFEST_PATH);
|
const manifest = readText(rootDir, releasePaths.previewManifestPath);
|
||||||
const decisions = parseDecisionRegister(ownerPacket);
|
const decisions = parseDecisionRegister(ownerPacket);
|
||||||
|
|
||||||
const missingDecisions = [];
|
const missingDecisions = [];
|
||||||
@ -388,11 +410,11 @@ function buildReport(options = {}) {
|
|||||||
.filter(surface => !ledger.includes(surface.label))
|
.filter(surface => !ledger.includes(surface.label))
|
||||||
.map(surface => surface.label);
|
.map(surface => surface.label);
|
||||||
const urlBlockers = ledgerBlockers(ledger);
|
const urlBlockers = ledgerBlockers(ledger);
|
||||||
const announcementOffenders = findAnnouncementOffenders(rootDir, ANNOUNCEMENT_FILES);
|
const announcementOffenders = findAnnouncementOffenders(rootDir, releasePaths.announcementFiles);
|
||||||
const commandListedIn = [
|
const commandListedIn = [
|
||||||
ownerPacket.includes(REQUIRED_COMMAND) ? OWNER_PACKET_PATH : '',
|
ownerPacket.includes(REQUIRED_COMMAND) ? releasePaths.ownerPacketPath : '',
|
||||||
ledger.includes(REQUIRED_COMMAND) ? URL_LEDGER_PATH : '',
|
ledger.includes(REQUIRED_COMMAND) ? releasePaths.urlLedgerPath : '',
|
||||||
manifest.includes(REQUIRED_COMMAND) ? PREVIEW_MANIFEST_PATH : '',
|
manifest.includes(REQUIRED_COMMAND) ? releasePaths.previewManifestPath : '',
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
const checks = [
|
const checks = [
|
||||||
@ -440,7 +462,7 @@ function buildReport(options = {}) {
|
|||||||
'announcement-copy-finalized',
|
'announcement-copy-finalized',
|
||||||
announcementOffenders.length === 0 ? 'pass' : 'fail',
|
announcementOffenders.length === 0 ? 'pass' : 'fail',
|
||||||
announcementOffenders.length === 0
|
announcementOffenders.length === 0
|
||||||
? `${ANNOUNCEMENT_FILES.length} launch/outbound copy files have no placeholders or private paths`
|
? `${releasePaths.announcementFiles.length} launch/outbound copy files have no placeholders or private paths`
|
||||||
: `offenders: ${announcementOffenders.map(item => `${item.path}:${item.line}`).join(', ')}`,
|
: `offenders: ${announcementOffenders.map(item => `${item.path}:${item.line}`).join(', ')}`,
|
||||||
'Replace placeholders with live URLs and remove private local paths from launch/outbound copy.'
|
'Replace placeholders with live URLs and remove private local paths from launch/outbound copy.'
|
||||||
),
|
),
|
||||||
@ -465,7 +487,7 @@ function buildReport(options = {}) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
schema_version: SCHEMA_VERSION,
|
schema_version: SCHEMA_VERSION,
|
||||||
release: RELEASE,
|
release,
|
||||||
ready: failed.length === 0,
|
ready: failed.length === 0,
|
||||||
digest,
|
digest,
|
||||||
summary: {
|
summary: {
|
||||||
@ -543,11 +565,12 @@ if (require.main === module) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
ANNOUNCEMENT_FILES,
|
ANNOUNCEMENT_FILE_NAMES,
|
||||||
REQUIRED_COMMAND,
|
REQUIRED_COMMAND,
|
||||||
REQUIRED_DECISIONS,
|
REQUIRED_DECISIONS,
|
||||||
REQUIRED_URL_SURFACES,
|
REQUIRED_URL_SURFACES,
|
||||||
buildReport,
|
buildReport,
|
||||||
|
releasePathsFor,
|
||||||
parseArgs,
|
parseArgs,
|
||||||
renderText,
|
renderText,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,7 +15,12 @@ const {
|
|||||||
renderText,
|
renderText,
|
||||||
} = require(SCRIPT);
|
} = require(SCRIPT);
|
||||||
|
|
||||||
const RELEASE_DIR = 'docs/releases/2.0.0-rc.1';
|
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) {
|
function createTempDir(prefix) {
|
||||||
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||||||
@ -31,14 +36,14 @@ function writeFile(rootDir, relativePath, content) {
|
|||||||
fs.writeFileSync(targetPath, content);
|
fs.writeFileSync(targetPath, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
function approvedPacketContent(overrides = {}) {
|
function approvedPacketContent(overrides = {}, release = CURRENT_RELEASE) {
|
||||||
const decisions = new Map(REQUIRED_DECISIONS.map(decision => [decision.label, 'approve']));
|
const decisions = new Map(REQUIRED_DECISIONS.map(decision => [decision.label, 'approve']));
|
||||||
for (const [label, value] of Object.entries(overrides)) {
|
for (const [label, value] of Object.entries(overrides)) {
|
||||||
decisions.set(label, value);
|
decisions.set(label, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'# ECC v2.0.0-rc.1 Owner Approval Packet',
|
`# ECC v${release} Owner Approval Packet`,
|
||||||
'',
|
'',
|
||||||
'## Decision Register',
|
'## Decision Register',
|
||||||
'',
|
'',
|
||||||
@ -58,16 +63,16 @@ function approvedPacketContent(overrides = {}) {
|
|||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function finalLedgerContent(extra = '') {
|
function finalLedgerContent(extra = '', release = CURRENT_RELEASE) {
|
||||||
return [
|
return [
|
||||||
'# ECC v2.0.0-rc.1 Release URL Ledger',
|
`# ECC v${release} Release URL Ledger`,
|
||||||
'',
|
'',
|
||||||
'## Final Published URLs',
|
'## Final Published URLs',
|
||||||
'',
|
'',
|
||||||
'| Surface | URL | Verification |',
|
'| Surface | URL | Verification |',
|
||||||
'| --- | --- | --- |',
|
'| --- | --- | --- |',
|
||||||
...REQUIRED_URL_SURFACES.map(surface => (
|
...REQUIRED_URL_SURFACES.map(surface => (
|
||||||
`| ${surface.label} | ${surface.exampleUrl} | readback from final release commit |`
|
`| ${surface.label} | ${surface.exampleUrl.split(RC_RELEASE).join(release)} | readback from final release commit |`
|
||||||
)),
|
)),
|
||||||
'',
|
'',
|
||||||
'## Final Verification Commands',
|
'## Final Verification Commands',
|
||||||
@ -80,9 +85,9 @@ function finalLedgerContent(extra = '') {
|
|||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function manifestContent() {
|
function manifestContent(release = CURRENT_RELEASE) {
|
||||||
return [
|
return [
|
||||||
'# ECC v2.0.0-rc.1 Preview Pack Manifest',
|
`# ECC v${release} Preview Pack Manifest`,
|
||||||
'',
|
'',
|
||||||
'| Artifact | Role | Gate |',
|
'| Artifact | Role | Gate |',
|
||||||
'| --- | --- | --- |',
|
'| --- | --- | --- |',
|
||||||
@ -96,23 +101,26 @@ function manifestContent() {
|
|||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function seedRepo(rootDir, overrides = {}) {
|
function seedRepo(rootDir, overrides = {}, options = {}) {
|
||||||
|
const release = options.release || CURRENT_RELEASE;
|
||||||
|
const releaseDir = releaseDirFor(release);
|
||||||
const files = {
|
const files = {
|
||||||
'package.json': JSON.stringify({
|
'package.json': JSON.stringify({
|
||||||
|
version: release,
|
||||||
files: ['scripts/release-approval-gate.js'],
|
files: ['scripts/release-approval-gate.js'],
|
||||||
scripts: {
|
scripts: {
|
||||||
'release:approval-gate': 'node scripts/release-approval-gate.js',
|
'release:approval-gate': 'node scripts/release-approval-gate.js',
|
||||||
},
|
},
|
||||||
}, null, 2),
|
}, null, 2),
|
||||||
'scripts/release-approval-gate.js': 'release approval gate script',
|
'scripts/release-approval-gate.js': 'release approval gate script',
|
||||||
[`${RELEASE_DIR}/owner-approval-packet-2026-05-19.md`]: approvedPacketContent(),
|
[`${releaseDir}/owner-approval-packet-2026-05-19.md`]: approvedPacketContent({}, release),
|
||||||
[`${RELEASE_DIR}/release-url-ledger-2026-05-19.md`]: finalLedgerContent(),
|
[`${releaseDir}/release-url-ledger-2026-05-19.md`]: finalLedgerContent('', release),
|
||||||
[`${RELEASE_DIR}/preview-pack-manifest.md`]: manifestContent(),
|
[`${releaseDir}/preview-pack-manifest.md`]: manifestContent(release),
|
||||||
[`${RELEASE_DIR}/release-notes.md`]: 'Release notes with final URLs.',
|
[`${releaseDir}/release-notes.md`]: 'Release notes with final URLs.',
|
||||||
[`${RELEASE_DIR}/x-thread.md`]: 'X post with final URLs.',
|
[`${releaseDir}/x-thread.md`]: 'X post with final URLs.',
|
||||||
[`${RELEASE_DIR}/linkedin-post.md`]: 'LinkedIn post with final URLs.',
|
[`${releaseDir}/linkedin-post.md`]: 'LinkedIn post with final URLs.',
|
||||||
[`${RELEASE_DIR}/article-outline.md`]: 'Article outline with final URLs.',
|
[`${releaseDir}/article-outline.md`]: 'Article outline with final URLs.',
|
||||||
[`${RELEASE_DIR}/partner-sponsor-talks-pack.md`]: 'Outbound copy with final URLs.',
|
[`${releaseDir}/partner-sponsor-talks-pack.md`]: 'Outbound copy with final URLs.',
|
||||||
'docs/business/social-launch-copy.md': 'Business launch copy with final URLs.',
|
'docs/business/social-launch-copy.md': 'Business launch copy with final URLs.',
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -189,6 +197,7 @@ function runTests() {
|
|||||||
const report = buildReport({ root: rootDir });
|
const report = buildReport({ root: rootDir });
|
||||||
|
|
||||||
assert.strictEqual(report.schema_version, 'ecc.release-approval-gate.v1');
|
assert.strictEqual(report.schema_version, 'ecc.release-approval-gate.v1');
|
||||||
|
assert.strictEqual(report.release, CURRENT_RELEASE);
|
||||||
assert.strictEqual(report.ready, true);
|
assert.strictEqual(report.ready, true);
|
||||||
assert.strictEqual(report.summary.failed, 0);
|
assert.strictEqual(report.summary.failed, 0);
|
||||||
assert.deepStrictEqual(report.top_actions, []);
|
assert.deepStrictEqual(report.top_actions, []);
|
||||||
@ -202,12 +211,27 @@ function runTests() {
|
|||||||
}
|
}
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('release override keeps rc.1 approval fixtures testable', () => {
|
||||||
|
const rootDir = createTempDir('release-approval-rc-');
|
||||||
|
|
||||||
|
try {
|
||||||
|
seedRepo(rootDir, {}, { release: RC_RELEASE });
|
||||||
|
const report = buildReport({ root: rootDir, release: RC_RELEASE });
|
||||||
|
|
||||||
|
assert.strictEqual(report.release, RC_RELEASE);
|
||||||
|
assert.strictEqual(report.ready, true);
|
||||||
|
} finally {
|
||||||
|
cleanup(rootDir);
|
||||||
|
}
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
if (test('deferred owner decisions keep the publication gate blocked', () => {
|
if (test('deferred owner decisions keep the publication gate blocked', () => {
|
||||||
const rootDir = createTempDir('release-approval-deferred-');
|
const rootDir = createTempDir('release-approval-deferred-');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const releaseDir = releaseDirFor(CURRENT_RELEASE);
|
||||||
seedRepo(rootDir, {
|
seedRepo(rootDir, {
|
||||||
[`${RELEASE_DIR}/owner-approval-packet-2026-05-19.md`]: approvedPacketContent({
|
[`${releaseDir}/owner-approval-packet-2026-05-19.md`]: approvedPacketContent({
|
||||||
'GitHub prerelease': 'defer',
|
'GitHub prerelease': 'defer',
|
||||||
'Sponsor, partner, consulting, conference, podcast outreach': 'block',
|
'Sponsor, partner, consulting, conference, podcast outreach': 'block',
|
||||||
}),
|
}),
|
||||||
@ -230,15 +254,16 @@ function runTests() {
|
|||||||
const rootDir = createTempDir('release-approval-ledger-');
|
const rootDir = createTempDir('release-approval-ledger-');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const releaseDir = releaseDirFor(CURRENT_RELEASE);
|
||||||
seedRepo(rootDir, {
|
seedRepo(rootDir, {
|
||||||
[`${RELEASE_DIR}/release-url-ledger-2026-05-19.md`]: [
|
[`${releaseDir}/release-url-ledger-2026-05-19.md`]: [
|
||||||
'# ECC v2.0.0-rc.1 Release URL Ledger',
|
`# ECC v${CURRENT_RELEASE} Release URL Ledger`,
|
||||||
'',
|
'',
|
||||||
'## Approval-Gated URLs',
|
'## Approval-Gated URLs',
|
||||||
'',
|
'',
|
||||||
'| Surface | Intended URL or command | Gate before use |',
|
'| Surface | Intended URL or command | Gate before use |',
|
||||||
'| --- | --- | --- |',
|
'| --- | --- | --- |',
|
||||||
'| GitHub prerelease | https://github.com/affaan-m/ECC/releases/tag/v2.0.0-rc.1 | must return the prerelease |',
|
`| GitHub prerelease | https://github.com/affaan-m/ECC/releases/tag/v${CURRENT_RELEASE} | must return the prerelease |`,
|
||||||
].join('\n'),
|
].join('\n'),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -257,8 +282,9 @@ function runTests() {
|
|||||||
const rootDir = createTempDir('release-approval-copy-');
|
const rootDir = createTempDir('release-approval-copy-');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const releaseDir = releaseDirFor(CURRENT_RELEASE);
|
||||||
seedRepo(rootDir, {
|
seedRepo(rootDir, {
|
||||||
[`${RELEASE_DIR}/x-thread.md`]: 'Ship copy with <video-url> and /Users/affaan/raw-footage.',
|
[`${releaseDir}/x-thread.md`]: 'Ship copy with <video-url> and /Users/affaan/raw-footage.',
|
||||||
});
|
});
|
||||||
|
|
||||||
const report = buildReport({ root: rootDir });
|
const report = buildReport({ root: rootDir });
|
||||||
@ -266,7 +292,7 @@ function runTests() {
|
|||||||
|
|
||||||
assert.strictEqual(report.ready, false);
|
assert.strictEqual(report.ready, false);
|
||||||
assert.strictEqual(copy.status, 'fail');
|
assert.strictEqual(copy.status, 'fail');
|
||||||
assert.ok(copy.evidence.includes(`${RELEASE_DIR}/x-thread.md:1`));
|
assert.ok(copy.evidence.includes(`${releaseDir}/x-thread.md:1`));
|
||||||
} finally {
|
} finally {
|
||||||
cleanup(rootDir);
|
cleanup(rootDir);
|
||||||
}
|
}
|
||||||
@ -280,10 +306,12 @@ function runTests() {
|
|||||||
const stdout = run(['--format=json', `--root=${rootDir}`], { cwd: rootDir });
|
const stdout = run(['--format=json', `--root=${rootDir}`], { cwd: rootDir });
|
||||||
const parsed = JSON.parse(stdout);
|
const parsed = JSON.parse(stdout);
|
||||||
assert.strictEqual(parsed.ready, true);
|
assert.strictEqual(parsed.ready, true);
|
||||||
|
assert.strictEqual(parsed.release, CURRENT_RELEASE);
|
||||||
|
|
||||||
|
const releaseDir = releaseDirFor(CURRENT_RELEASE);
|
||||||
writeFile(
|
writeFile(
|
||||||
rootDir,
|
rootDir,
|
||||||
`${RELEASE_DIR}/owner-approval-packet-2026-05-19.md`,
|
`${releaseDir}/owner-approval-packet-2026-05-19.md`,
|
||||||
approvedPacketContent({ 'Video upload': 'defer' })
|
approvedPacketContent({ 'Video upload': 'defer' })
|
||||||
);
|
);
|
||||||
const failedRun = runProcess(['--format=json', `--root=${rootDir}`], { cwd: rootDir });
|
const failedRun = runProcess(['--format=json', `--root=${rootDir}`], { cwd: rootDir });
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user