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
|
|
@ -3,7 +3,7 @@
|
|||
// is exercised by the CLI against the live tree, not here.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { planRelease, reconcileReadmeLabel } from './release-plugin.mjs';
|
||||
import { planRelease, reconcileReadmeLabel, preflightErrors, applyRelease } from './release-plugin.mjs';
|
||||
import { classifyPlugin } from './check-versions.mjs';
|
||||
|
||||
const marketplace = () => ({
|
||||
|
|
@ -131,3 +131,82 @@ test('reconcileReadmeLabel returns null when the plugin has no heading', () => {
|
|||
const readme = '### [Other](https://x/open/other) `v1.0.0`';
|
||||
assert.equal(reconcileReadmeLabel(readme, 'ghost', 'v2.0.0'), null);
|
||||
});
|
||||
|
||||
// --- pre-flight gate: check-versions must run BEFORE the writes, not after -----
|
||||
//
|
||||
// The old order wrote marketplace.json + the README label first and only THEN ran the
|
||||
// gate (which throws on exit 1) — leaving a half-applied release in the working tree
|
||||
// that a parallel session could carry to the public remote. These tests pin the
|
||||
// ORDER, so they must assert on writes-not-taken, not just on a verdict string.
|
||||
|
||||
const gateResult = (statuses) => {
|
||||
const results = Object.entries(statuses).map(([name, status]) => ({ name, status, findings: [] }));
|
||||
return {
|
||||
results,
|
||||
hasError: results.some(r => r.status === 'ERROR'),
|
||||
hasWarn: results.some(r => r.status === 'WARN'),
|
||||
failed: results.some(r => r.status === 'ERROR'),
|
||||
};
|
||||
};
|
||||
|
||||
const fakeIo = (gate, readmeText = '### [Alpha](https://x/open/alpha) `v1.0.0`') => {
|
||||
const writes = [];
|
||||
return {
|
||||
writes,
|
||||
runGate: () => gate,
|
||||
readFileSync: () => readmeText,
|
||||
writeFileSync: (p) => { writes.push(p); },
|
||||
};
|
||||
};
|
||||
|
||||
const paths = { catalogDir: '/cat', mktPath: '/cat/.claude-plugin/marketplace.json', readmePath: '/cat/README.md' };
|
||||
|
||||
test('preflightErrors names every ERROR plugin, catalog-wide (not just the target)', () => {
|
||||
assert.deepEqual(preflightErrors(gateResult({ alpha: 'WARN', beta: 'ERROR', gamma: 'ERROR' })), ['beta', 'gamma']);
|
||||
assert.deepEqual(preflightErrors(gateResult({ alpha: 'OK', beta: 'WARN', gamma: 'SKIP' })), []);
|
||||
});
|
||||
|
||||
test('pre-flight ERROR aborts BEFORE any file is written (ordering, not just verdict)', () => {
|
||||
const plan = planRelease({ marketplace: marketplace(), name: 'alpha', observed: observed() });
|
||||
const io = fakeIo(gateResult({ alpha: 'WARN', beta: 'ERROR' })); // ERROR on a DIFFERENT plugin
|
||||
const r = applyRelease({ plan, ...paths }, io);
|
||||
assert.equal(r.verdict, 'BLOCKED');
|
||||
assert.deepEqual(r.preflightErrors, ['beta']);
|
||||
assert.deepEqual(io.writes, [], 'no file may be written when the gate is red');
|
||||
});
|
||||
|
||||
test('pre-bump WARN is the NORMAL state and must NOT block the release', () => {
|
||||
const plan = planRelease({ marketplace: marketplace(), name: 'alpha', observed: observed() });
|
||||
// Real classifier, pre-bump: catalog ref still v1.0.0 while plugin.json is 1.1.0.
|
||||
// This is exactly what a release looks like before it is applied — it MUST be WARN,
|
||||
// or gating on `failed`/`--strict` would brick every release.
|
||||
const pre = classifyPlugin({
|
||||
name: 'alpha', catalogRef: plan.currentRef,
|
||||
pluginVersion: '1.1.0', readmeBadge: '1.1.0', tags: ['v1.0.0', 'v1.1.0'],
|
||||
});
|
||||
assert.equal(pre.status, 'WARN');
|
||||
|
||||
const io = fakeIo({ results: [pre], hasError: false, hasWarn: true, failed: false });
|
||||
const r = applyRelease({ plan, ...paths }, io);
|
||||
assert.equal(r.verdict, 'WROTE');
|
||||
assert.deepEqual(io.writes, [paths.mktPath, paths.readmePath]);
|
||||
});
|
||||
|
||||
test('green pre-flight writes the ref and reports an already-correct README label as unchanged', () => {
|
||||
const plan = planRelease({ marketplace: marketplace(), name: 'alpha', observed: observed() });
|
||||
const io = fakeIo(gateResult({ alpha: 'OK' }), '### [Alpha](https://x/open/alpha) `v1.1.0`');
|
||||
const r = applyRelease({ plan, ...paths }, io);
|
||||
assert.equal(r.verdict, 'WROTE');
|
||||
assert.equal(r.readme, 'unchanged');
|
||||
assert.deepEqual(io.writes, [paths.mktPath]);
|
||||
});
|
||||
|
||||
test('a missing catalog README does not abort the ref bump', () => {
|
||||
const plan = planRelease({ marketplace: marketplace(), name: 'alpha', observed: observed() });
|
||||
const io = fakeIo(gateResult({ alpha: 'OK' }));
|
||||
io.readFileSync = () => { throw new Error('ENOENT'); };
|
||||
const r = applyRelease({ plan, ...paths }, io);
|
||||
assert.equal(r.verdict, 'WROTE');
|
||||
assert.equal(r.readme, 'missing');
|
||||
assert.deepEqual(io.writes, [paths.mktPath]);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue