fix(scripts): --create-tag is a write and must obey --write
`--create-tag` minted AND PUSHED a plugin tag to a public remote without
`--write`, on the entry point CLAUDE.md documents as "dry-run by default".
The tag was public before the plan was even printed.
Extracts `shouldCreateTag(args, observed, target)` as a pure exported
predicate ('create' | 'dry-run' | 'skip') so the flag's write-ness is
testable, and gates minting on `--write`. Without it the CLI now reports
what it would mint, printed after the missing-tag blocker that points at
the flag.
Deliberately NOT placed behind the catalog-wide pre-flight: every
precondition it checks is local to the plugin being released
(plugin.json == target, badge agrees, tag absent), so the tag is correct
by construction. A red *other* plugin can only make the tag early, never
wrong, and the tag-absent check makes the retry idempotent. Gating it
would let plugin Y block the tagging of plugin X — the same over-coupling
that reading the ERROR set only (never `failed`) exists to avoid.
Tests 19 -> 25; 131/131 across the six suites. check-versions 12 OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019i3rnU2VNRdGcrUFMnRb6R
This commit is contained in:
parent
ac7ad424d1
commit
ea8231f3c8
3 changed files with 87 additions and 9 deletions
|
|
@ -18,7 +18,8 @@
|
|||
//
|
||||
// Usage:
|
||||
// node scripts/release-plugin.mjs <name> [--version X.Y.Z] # dry-run: print the plan
|
||||
// node scripts/release-plugin.mjs <name> --create-tag # create+push the missing vX.Y.Z plugin tag first
|
||||
// node scripts/release-plugin.mjs <name> --create-tag --write # create+push the missing vX.Y.Z plugin tag first
|
||||
// # (--create-tag is a WRITE: without --write it only reports)
|
||||
// node scripts/release-plugin.mjs <name> --write # write the bumped catalog ref
|
||||
// node scripts/release-plugin.mjs <name> --write --commit # + git commit the catalog
|
||||
// node scripts/release-plugin.mjs <name> --write --commit --push # + push (you own the push window)
|
||||
|
|
@ -118,6 +119,27 @@ export function preflightErrors(gateResult) {
|
|||
return (gateResult?.results ?? []).filter(r => r.status === 'ERROR').map(r => r.name);
|
||||
}
|
||||
|
||||
// --create-tag mints AND PUSHES a tag to a public remote — the one genuinely irreversible
|
||||
// side effect here — so it is a WRITE and must obey --write. It used to fire on the
|
||||
// documented dry-run entry point, publishing the tag before the plan was even printed.
|
||||
//
|
||||
// Deliberately NOT gated on the catalog-wide pre-flight: every precondition below is
|
||||
// local to this plugin (plugin.json == target, badge agrees, tag absent), so the minted
|
||||
// tag is correct by construction. A red OTHER plugin can only make the tag EARLY, never
|
||||
// WRONG — and `!tags.includes(newRef)` makes the retry idempotent once that plugin is
|
||||
// fixed. Gating on it would let plugin Y block the tagging of plugin X: the same
|
||||
// over-coupling that `preflightErrors` reading ERROR-only (never `failed`) exists to avoid.
|
||||
//
|
||||
// Returns 'create' (mint + push), 'dry-run' (would, but no --write), or 'skip'.
|
||||
export function shouldCreateTag(args, observed, target) {
|
||||
if (!args?.createTag || !target) return 'skip';
|
||||
if (observed?.tags === null || observed?.tags === undefined) return 'skip';
|
||||
if (observed.tags.includes('v' + target)) return 'skip';
|
||||
if (observed.pluginVersion !== target) return 'skip';
|
||||
if (observed.readmeBadge !== null && observed.readmeBadge !== observed.pluginVersion) return 'skip';
|
||||
return args.write ? 'create' : 'dry-run';
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
|
@ -195,9 +217,10 @@ function main() {
|
|||
let obs = observePlugin(catalogDir, args.name);
|
||||
const target = normalizeVersion(args.version ?? obs.pluginVersion ?? '');
|
||||
|
||||
// --create-tag: if the only thing missing is the tag, mint + push it first.
|
||||
if (args.createTag && target && obs.tags !== null && !obs.tags.includes('v' + target)
|
||||
&& obs.pluginVersion === target && (obs.readmeBadge === null || obs.readmeBadge === obs.pluginVersion)) {
|
||||
// --create-tag: if the only thing missing is the tag, mint + push it first — but only
|
||||
// under --write. Without it this is a dry-run and must publish nothing.
|
||||
const tagStep = shouldCreateTag(args, obs, target);
|
||||
if (tagStep === 'create') {
|
||||
const tag = 'v' + target;
|
||||
console.log(`→ creating annotated tag ${tag} in ${obs.repoDir}`);
|
||||
execFileSync('git', ['-C', obs.repoDir, 'tag', '-a', tag, '-m', `${args.name} ${tag}`], { stdio: 'inherit' });
|
||||
|
|
@ -209,6 +232,11 @@ function main() {
|
|||
|
||||
console.log(`\nrelease-plugin: ${plan.name} ${plan.currentRef ?? '?'} -> ${plan.newRef ?? '?'} [${plan.verdict}]`);
|
||||
if (plan.blockers.length) { for (const b of plan.blockers) console.log(` ✗ ${b}`); }
|
||||
// Printed AFTER the blockers: the missing-tag blocker points at --create-tag, and this is
|
||||
// the answer to "I did pass it" — the flag is a write, so it waited for --write.
|
||||
if (tagStep === 'dry-run') {
|
||||
console.log(` (dry-run) --create-tag would mint + push v${target} in ${obs.repoDir} — re-run with --write.`);
|
||||
}
|
||||
|
||||
if (plan.verdict === 'BLOCKED') process.exit(1);
|
||||
if (plan.verdict === 'NOOP') { console.log(' ✓ catalog already pins this version — nothing to do.'); process.exit(0); }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue