fix(scripts): run check-versions BEFORE release-plugin writes, not after
release-plugin.mjs --write wrote marketplace.json and the catalog README label first, and only THEN ran check-versions via execFileSync (which throws on exit 1). A red catalog therefore left a half-applied release in the working tree — exactly the state a parallel session has already been observed carrying to the public remote. Adds applyRelease() with an injected io seam so the ORDER is testable: runGate() runs first, and any ERROR aborts with nothing written. The pre-flight reads the ERROR set only, never failed/--strict — pre-bump the released plugin is SUPPOSED to be WARN (catalog ref behind plugin.json), so a WARN gate would brick every release. Verified against the real classifier, not synthetic data. The post-write gate stays: pre-flight validates the old state, that one validates the new state. Known remaining hole, documented not built: --create-tag mints and pushes the plugin tag before the pre-flight runs. Tests 14 -> 19 (120 -> 125 across the six suites); check-versions 12 OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Ga5tZ3AgUxAcdLtWB8Kig
This commit is contained in:
parent
22ef8c7e65
commit
ac7ad424d1
3 changed files with 147 additions and 18 deletions
|
|
@ -27,7 +27,7 @@ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|||
import { execFileSync } from 'node:child_process';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { normalizeVersion } from './check-versions.mjs';
|
||||
import { normalizeVersion, runGate } from './check-versions.mjs';
|
||||
|
||||
// --- Pure planner (unit under test) -----------------------------------------
|
||||
|
||||
|
|
@ -105,6 +105,46 @@ export function reconcileReadmeLabel(readmeText, name, newRef) {
|
|||
return changed ? out.join('\n') : null;
|
||||
}
|
||||
|
||||
// --- Pre-flight gate + write step (unit under test via injected io) ----------
|
||||
|
||||
// Which plugins does check-versions call ERROR right now? Catalog-wide on purpose: one red
|
||||
// plugin blocks every bump, because check-versions' exit code is global — a bump committed
|
||||
// on top of someone else's ERROR ships a catalog that cannot pass its own gate.
|
||||
//
|
||||
// Reads the ERROR set explicitly and NEVER `failed`/`hasWarn`: pre-bump, the plugin being
|
||||
// released is SUPPOSED to be WARN (catalog ref behind plugin.json). Gating on WARN would
|
||||
// brick every release.
|
||||
export function preflightErrors(gateResult) {
|
||||
return (gateResult?.results ?? []).filter(r => r.status === 'ERROR').map(r => r.name);
|
||||
}
|
||||
|
||||
// Run the gate FIRST, then write. The old order wrote both files and only then ran the gate
|
||||
// (which throws on exit 1), leaving a half-applied release in the working tree for a parallel
|
||||
// session to carry to the public remote. `io` is injected so the ORDER is testable.
|
||||
export function applyRelease({ plan, catalogDir, mktPath, readmePath }, io) {
|
||||
const errors = preflightErrors(io.runGate(catalogDir));
|
||||
if (errors.length > 0) return { verdict: 'BLOCKED', preflightErrors: errors, writes: [], readme: null };
|
||||
|
||||
const writes = [];
|
||||
io.writeFileSync(mktPath, JSON.stringify(plan.newMarketplace, null, 2) + '\n', 'utf8');
|
||||
writes.push(mktPath);
|
||||
|
||||
// Keep the human-facing catalog README label in lock-step with the ref (gated by check-versions).
|
||||
let readme;
|
||||
try {
|
||||
const newReadme = reconcileReadmeLabel(io.readFileSync(readmePath, 'utf8'), plan.name, plan.newRef);
|
||||
if (newReadme !== null) {
|
||||
io.writeFileSync(readmePath, newReadme, 'utf8');
|
||||
writes.push(readmePath);
|
||||
readme = 'written';
|
||||
} else {
|
||||
readme = 'unchanged';
|
||||
}
|
||||
} catch { readme = 'missing'; }
|
||||
|
||||
return { verdict: 'WROTE', preflightErrors: [], writes, readme };
|
||||
}
|
||||
|
||||
// --- I/O shell --------------------------------------------------------------
|
||||
|
||||
function gitTags(repoDir) {
|
||||
|
|
@ -180,22 +220,23 @@ function main() {
|
|||
process.exit(0);
|
||||
}
|
||||
|
||||
writeFileSync(mktPath, JSON.stringify(plan.newMarketplace, null, 2) + '\n', 'utf8');
|
||||
console.log(` ✓ wrote ${mktPath} (ref ${plan.currentRef} -> ${plan.newRef})`);
|
||||
|
||||
// Keep the human-facing catalog README label in lock-step with the ref (gated by check-versions).
|
||||
const readmePath = join(catalogDir, 'README.md');
|
||||
try {
|
||||
const newReadme = reconcileReadmeLabel(readFileSync(readmePath, 'utf8'), plan.name, plan.newRef);
|
||||
if (newReadme !== null) {
|
||||
writeFileSync(readmePath, newReadme, 'utf8');
|
||||
console.log(` ✓ updated README label (${plan.name} -> ${plan.newRef})`);
|
||||
} else {
|
||||
console.log(` · README label already ${plan.newRef} (or no heading found)`);
|
||||
}
|
||||
} catch { console.log(' · no catalog README to update'); }
|
||||
const applied = applyRelease({ plan, catalogDir, mktPath, readmePath }, { readFileSync, writeFileSync, runGate });
|
||||
|
||||
// Confirm the gate is green for this plugin after the write.
|
||||
if (applied.verdict === 'BLOCKED') {
|
||||
console.log(' ✗ pre-flight check-versions is RED — nothing written.');
|
||||
for (const n of applied.preflightErrors) console.log(` ERROR: ${n}`);
|
||||
console.log(' Fix every ERROR (any plugin — the gate exit code is catalog-wide), then re-run.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(` ✓ wrote ${mktPath} (ref ${plan.currentRef} -> ${plan.newRef})`);
|
||||
if (applied.readme === 'written') console.log(` ✓ updated README label (${plan.name} -> ${plan.newRef})`);
|
||||
else if (applied.readme === 'unchanged') console.log(` · README label already ${plan.newRef} (or no heading found)`);
|
||||
else console.log(' · no catalog README to update');
|
||||
|
||||
// Confirm the gate is green for this plugin AFTER the write — the pre-flight validated the
|
||||
// old state, this validates the new one. Different jobs; the redundancy is only apparent.
|
||||
const gate = execFileSync('node', [join(catalogDir, 'scripts', 'check-versions.mjs')], { cwd: catalogDir, encoding: 'utf8' });
|
||||
const line = gate.split('\n').find(l => l.includes(args.name)) ?? '';
|
||||
console.log(` check-versions: ${line.trim() || '(no line)'}`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue