fix(release): name the running model in the catalog commit via --model

The catalog commit hardcoded "Co-Authored-By: Claude Opus 4.8 (1M context)"
whatever model actually ran the release. A script cannot know its driver,
so the running session now states it with --model "<name>"; without the
flag the trailer reads "Claude" and names no model. A value with a newline
or angle brackets is refused before any tag or write. Operator decision
2026-11-04. Tests first (7 red on assertions), suite 238/238.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-11-04 09:32:09 +01:00
commit e52805e317
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
3 changed files with 71 additions and 4 deletions

View file

@ -14,6 +14,7 @@ import {
runRelease, preflightStatMismatches, reportPostWriteCheck,
parseForgejoRepo, planForgejoRelease, ensureForgejoRelease,
extractChangelogSection, releaseBodyFrom, pendingCatalogChanges,
coAuthorTrailer, parseArgs,
} from './release-plugin.mjs';
import { classifyPlugin } from './check-versions.mjs';
@ -701,12 +702,15 @@ test('BUG 2 (order 20260922T185613Z): --write --commit after a prior --write-onl
// Invocation 2 is a FRESH process re-reading marketplace.json off disk (already v1.1.0).
const marketplaceOnDisk = JSON.parse(fsReadFileSync(mktPath, 'utf8'));
const code2 = runRelease({
args: { name: 'demo-plugin', version: undefined, createTag: false, write: true, commit: true, push: false },
args: { name: 'demo-plugin', version: undefined, createTag: false, write: true, commit: true, push: false, model: 'Opus 5.5' },
catalogDir, mktPath, marketplace: marketplaceOnDisk, pushGate,
runCheckVersions: () => '1 plugins — 1 OK, 0 WARN, 0 ERROR, 0 SKIP — verified 1/1\n',
});
assert.equal(code2, 0, 'finishing a pending release must succeed');
const body = execFileSync('git', ['-C', catalogDir, 'log', '-1', '--pretty=%B'], { encoding: 'utf8' });
assert.match(body, /^Co-Authored-By: Claude Opus 5\.5 <noreply@anthropic\.com>$/m, 'the commit names the model the running session passed with --model');
assert.doesNotMatch(body, /4\.8/, 'the stale hardcoded model must be gone');
const statusAfter2 = execFileSync('git', ['-C', catalogDir, 'status', '--porcelain'], { encoding: 'utf8' });
assert.equal(statusAfter2.trim(), '', 'BUG 2: --write --commit after --create-tag --write must actually commit the pending write');
const log = execFileSync('git', ['-C', catalogDir, 'log', '-1', '--pretty=%s'], { encoding: 'utf8' }).trim();
@ -764,6 +768,8 @@ test('BUG 3 (order 20260922T185613Z): --write --commit --push after a prior --wr
assert.equal(code2, 0, 'finishing a pending release (commit+push) must succeed');
const originLog = execFileSync('git', ['--git-dir', bare, 'log', '-1', '--pretty=%s', 'refs/heads/main'], { encoding: 'utf8' }).trim();
assert.match(originLog, /demo-plugin/, 'BUG 3: NOOP must not skip the push of a pending catalog commit');
const originBody = execFileSync('git', ['--git-dir', bare, 'log', '-1', '--pretty=%B', 'refs/heads/main'], { encoding: 'utf8' });
assert.match(originBody, /^Co-Authored-By: Claude <noreply@anthropic\.com>$/m, 'no --model: the trailer names Claude without inventing a model');
} finally {
rmSync(root, { recursive: true, force: true });
}
@ -1395,3 +1401,34 @@ test('releaseBodyFrom: a MECHANICAL tag message loses to the CHANGELOG — that
const r = releaseBodyFrom({ changelogText: CL_BRACKET, tag: 'v8.0.0', tagMessage: 'llm-security v8.0.0' });
assert.notEqual(r.body, 'llm-security v8.0.0');
});
// --- Commit attribution (operator decision 2026-11-04) ----------------------
// The catalog commit used to hardcode `Claude Opus 4.8 (1M context)` whatever model
// actually ran the release. A script cannot know that, so the running session says it
// with --model; without it the trailer names Claude and no model — never a wrong one.
test('coAuthorTrailer: a given model is named in the trailer', () => {
assert.equal(coAuthorTrailer('Opus 5.5'), 'Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>');
});
test('coAuthorTrailer: a leading "Claude " in the model is not doubled', () => {
assert.equal(coAuthorTrailer('Claude Opus 5.5'), 'Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>');
assert.equal(coAuthorTrailer(' claude Sonnet 5.5 '), 'Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>');
});
test('coAuthorTrailer: no model (absent, empty, blank) names Claude alone', () => {
for (const m of [undefined, null, '', ' ']) {
assert.equal(coAuthorTrailer(m), 'Co-Authored-By: Claude <noreply@anthropic.com>', `model=${JSON.stringify(m)}`);
}
});
test('coAuthorTrailer: a model that would break the trailer line is refused, not written', () => {
assert.throws(() => coAuthorTrailer('Opus 5.5\nSigned-off-by: someone'), /--model/);
assert.throws(() => coAuthorTrailer('Opus <x@y>'), /--model/);
});
test('parseArgs: --model takes the next argument; absent means null', () => {
assert.equal(parseArgs(['voyage', '--model', 'Opus 5.5', '--write']).model, 'Opus 5.5');
assert.equal(parseArgs(['voyage', '--write']).model, null);
assert.equal(parseArgs(['voyage', '--write']).name, 'voyage');
});