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
|
|
@ -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, preflightErrors, applyRelease } from './release-plugin.mjs';
|
||||
import { planRelease, reconcileReadmeLabel, preflightErrors, applyRelease, shouldCreateTag } from './release-plugin.mjs';
|
||||
import { classifyPlugin } from './check-versions.mjs';
|
||||
|
||||
const marketplace = () => ({
|
||||
|
|
@ -210,3 +210,46 @@ test('a missing catalog README does not abort the ref bump', () => {
|
|||
assert.equal(r.readme, 'missing');
|
||||
assert.deepEqual(io.writes, [paths.mktPath]);
|
||||
});
|
||||
|
||||
// --- shouldCreateTag: --create-tag is a WRITE, so it must obey --write --------
|
||||
//
|
||||
// `--create-tag` mints AND PUSHES a tag to a public remote — the one genuinely
|
||||
// irreversible side effect in this helper. It used to fire on the documented
|
||||
// dry-run entry point (`<name> --create-tag`, no --write), which contradicts
|
||||
// "dry-run by default": the tag was already public before the plan was printed.
|
||||
// The decision (2026-08-11) was to gate it on --write and leave the catalog-wide
|
||||
// pre-flight where it is — that gate can only prevent an EARLY tag, never a WRONG
|
||||
// one, since the preconditions below already make the tag correct by construction.
|
||||
|
||||
const tagArgs = (o = {}) => ({ createTag: true, write: true, ...o });
|
||||
|
||||
test('shouldCreateTag: --create-tag --write on a consistent plugin with no tag → create', () => {
|
||||
assert.equal(shouldCreateTag(tagArgs(), observed({ tags: ['v1.0.0'] }), '1.1.0'), 'create');
|
||||
});
|
||||
|
||||
test('shouldCreateTag: --create-tag WITHOUT --write never pushes (dry-run stays dry)', () => {
|
||||
assert.equal(shouldCreateTag(tagArgs({ write: false }), observed({ tags: ['v1.0.0'] }), '1.1.0'), 'dry-run');
|
||||
});
|
||||
|
||||
test('shouldCreateTag: no --create-tag → skip, even with --write', () => {
|
||||
assert.equal(shouldCreateTag(tagArgs({ createTag: false }), observed({ tags: ['v1.0.0'] }), '1.1.0'), 'skip');
|
||||
});
|
||||
|
||||
test('shouldCreateTag: tag already exists → skip (retry after a red gate is idempotent)', () => {
|
||||
assert.equal(shouldCreateTag(tagArgs(), observed({ tags: ['v1.0.0', 'v1.1.0'] }), '1.1.0'), 'skip');
|
||||
});
|
||||
|
||||
test('shouldCreateTag: skips when the plugin is not internally consistent', () => {
|
||||
// plugin.json behind the target — planRelease would BLOCK anyway; never mint for it.
|
||||
assert.equal(shouldCreateTag(tagArgs(), observed({ pluginVersion: '1.0.0', readmeBadge: '1.0.0', tags: ['v1.0.0'] }), '1.1.0'), 'skip');
|
||||
// README badge disagrees with plugin.json
|
||||
assert.equal(shouldCreateTag(tagArgs(), observed({ readmeBadge: '1.0.0', tags: ['v1.0.0'] }), '1.1.0'), 'skip');
|
||||
// no target version resolved
|
||||
assert.equal(shouldCreateTag(tagArgs(), observed({ tags: ['v1.0.0'] }), null), 'skip');
|
||||
// plugin repo absent (gitTags returned null) — nothing to tag
|
||||
assert.equal(shouldCreateTag(tagArgs(), observed({ tags: null }), '1.1.0'), 'skip');
|
||||
});
|
||||
|
||||
test('shouldCreateTag: a null README badge is tolerated (badge-less plugin)', () => {
|
||||
assert.equal(shouldCreateTag(tagArgs(), observed({ readmeBadge: null, tags: ['v1.0.0'] }), '1.1.0'), 'create');
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue