fix(catalog): release-plugin.mjs shares one push token across a whole run
Q3b (order 20260912T213049Z-5772222747) corrects two measured defects in Q3'sea9bf7a: pushWithToken checked-and-consumed per call, so a single `--create-tag --write --commit --push` run spent the operator's one-shot token on the tag push and always saw blocked:true on the catalog push right after (D1). And `git tag -a` ran before any token check at all, so a blocked run left a local annotated tag behind, breaking the retry with "tag already exists" (D2). createPushGate replaces the per-push check-and-consume with a run-scoped gate: ensure() checks the token once and every later call in the same run reuses that result, consume() fires once after the run's last successful push. main() calls ensure() before the tag write (not just before the push) and consume() once at the end. pushWithToken is now a single-push convenience wrapper over the same gate — its existing tests stay green unmodified. Red-first: `createPushGate` did not exist onea9bf7a(import error), proving both new tests were red before the fix. After: node --test scripts/release-plugin.test.mjs -> 37/37 (35 + 2 new) node --test scripts/*.test.mjs -> 154/154 node scripts/check-versions.mjs -> 0 ERROR (2 known WARN: claude-design, repo-mailbox) Live D2 check: `release-plugin.mjs repo-mailbox --create-tag --write` without a token -> BLOCKED, exit 1, no local tag created. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
ea9bf7ab02
commit
5bc6c4ecbd
2 changed files with 105 additions and 16 deletions
|
|
@ -5,7 +5,7 @@ import { test } from 'node:test';
|
|||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
planRelease, reconcileReadmeLabel, preflightErrors, applyRelease, shouldCreateTag,
|
||||
pushAuthorisation, requirePushAuthorisation, pushWithToken, consumeToken,
|
||||
pushAuthorisation, requirePushAuthorisation, pushWithToken, consumeToken, createPushGate,
|
||||
} from './release-plugin.mjs';
|
||||
import { classifyPlugin } from './check-versions.mjs';
|
||||
|
||||
|
|
@ -363,3 +363,52 @@ test('consumeToken deletes the token when it is present', () => {
|
|||
consumeToken({ tokenPath: '/x', exists: () => true, unlink: (p) => unlinked.push(p) });
|
||||
assert.deepEqual(unlinked, ['/x']);
|
||||
});
|
||||
|
||||
// --- Q3b fix: D1 (shared token across BOTH pushes in one run) + D2 (checked before
|
||||
// the tag write, not just before the push) — order 20260912T213049Z-5772222747 -----
|
||||
//
|
||||
// Q3's pushWithToken checked-and-consumed per call: with one token, the tag push consumed
|
||||
// it before the catalog push ran, so inside a single `--create-tag --write --commit --push`
|
||||
// run the catalog push always saw blocked:true. And `git tag -a` (pre-fix main():295) ran
|
||||
// before ANY token check, so a blocked run left a local annotated tag behind — the retry
|
||||
// after the operator drops the token then fails with "tag already exists" (exit 128).
|
||||
// createPushGate fixes both: ONE ensure() shared across every push this run makes, checked
|
||||
// before the FIRST write (including a local tag meant to precede a later push), consumed
|
||||
// ONCE after the run's last push succeeds.
|
||||
|
||||
test('createPushGate: one token covers two pushes in the same run, consumed once at the end', () => {
|
||||
let existsCalls = 0;
|
||||
const exists = () => { existsCalls++; return true; };
|
||||
const unlinked = [];
|
||||
const gate = createPushGate({ cwd: '/c', home: '/h', exists, unlink: (p) => unlinked.push(p) });
|
||||
|
||||
const authForTag = gate.ensure();
|
||||
assert.equal(authForTag.authorised, true, 'first push (tag) must be authorised by the one token');
|
||||
// ... tag push happens here in main() ...
|
||||
const authForCatalog = gate.ensure();
|
||||
assert.equal(authForCatalog.authorised, true, "second push (catalog) must reuse the SAME token, not find it already consumed");
|
||||
assert.equal(existsCalls, 1, 'the token is checked ONCE for the whole run, not once per push');
|
||||
|
||||
gate.consume();
|
||||
assert.deepEqual(unlinked, [authForTag.tokenPath], "the token is consumed exactly once, after the run's last push");
|
||||
gate.consume();
|
||||
assert.deepEqual(unlinked, [authForTag.tokenPath], 'a second consume() must not double-unlink');
|
||||
});
|
||||
|
||||
test('createPushGate: an unauthorised run must not create the tag or touch the catalog', () => {
|
||||
let tagCreated = false;
|
||||
let catalogWritten = false;
|
||||
const gate = createPushGate({
|
||||
cwd: '/c', home: '/h', exists: () => false,
|
||||
unlink: () => { throw new Error('BUG: must not unlink without a token'); },
|
||||
});
|
||||
|
||||
const auth = gate.ensure();
|
||||
assert.equal(auth.authorised, false);
|
||||
if (auth.authorised) tagCreated = true; // mirrors main(): `git tag -a` only runs past this check
|
||||
if (auth.authorised) catalogWritten = true; // mirrors main(): applyRelease() only runs past this check
|
||||
|
||||
assert.equal(tagCreated, false, 'D2: the tag must not be created before the token check passes');
|
||||
assert.equal(catalogWritten, false, 'no catalog change either — the run stops at the first blocked check');
|
||||
gate.consume(); // must be safe even though ensure() never authorised anything
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue