feat(catalog): gate catalog README labels against ref + close the drift
The catalog README's per-plugin `vX.Y.Z` labels were an UNGUARDED surface: check-versions validated each plugin's OWN README badge, never the catalog README's labels, so they drifted (config-audit shown v5.5.0 while pinned to v5.7.0; voyage v5.1.1 while pinned to v5.6.0) — the doc misstated what `claude plugin install` actually resolves. Close the class, same pattern as the ref surface: - check-versions.mjs: new ERROR rule "catalog README label == catalog ref" via pure extractCatalogLabel() (matches the /open/<name>) heading, takes the first `vX.Y.Z`, ignores a trailing lang/flag badge). No legitimate transient state lets label != ref, so ERROR (not WARN). +5 tests. - release-plugin.mjs: on --write, bump the README label atomically with the ref via pure reconcileReadmeLabel() and git add README.md on --commit, so future releases keep label and ref in lock-step. +4 tests. - README.md: reconcile the 2 stale labels (config-audit -> v5.7.0, voyage -> v5.6.0). Gate now 9 OK / 1 WARN / 0 ERROR. - CLAUDE.md: doctrine updated to document the new gate rule + atomic label bump. ms-ai-architect stays WARN by decision (1.16.0 is unreleased WIP, never tagged). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cm2RxKbomdLqjiWGcwCCPi
This commit is contained in:
parent
cf2420f7c5
commit
2e1d206ab3
6 changed files with 155 additions and 18 deletions
|
|
@ -10,10 +10,11 @@
|
|||
//
|
||||
// This helper makes the catalog side impossible to do wrong: it REFUSES unless
|
||||
// plugin.json == README badge == the target version AND the vX.Y.Z tag exists,
|
||||
// then bumps the ref so `check-versions.mjs` is green by construction. The pure
|
||||
// planner (planRelease) is fully tested; the I/O shell reads the tree and, under
|
||||
// explicit flags, writes/commits/pushes. Dry-run by default — it changes nothing
|
||||
// until you pass --write.
|
||||
// then bumps the ref AND the catalog README's per-plugin label together so
|
||||
// `check-versions.mjs` is green by construction (it now also gates label == ref).
|
||||
// The pure planner (planRelease) and label reconciler (reconcileReadmeLabel) are
|
||||
// fully tested; the I/O shell reads the tree and, under explicit flags,
|
||||
// writes/commits/pushes. Dry-run by default — it changes nothing until you pass --write.
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/release-plugin.mjs <name> [--version X.Y.Z] # dry-run: print the plan
|
||||
|
|
@ -87,6 +88,23 @@ export function planRelease({ marketplace, name, observed, targetVersion }) {
|
|||
};
|
||||
}
|
||||
|
||||
// Bump the catalog README's per-plugin label so the human-facing doc matches the new ref.
|
||||
// Replaces the FIRST `vX.Y.Z` token on the plugin's `/open/<name>)` heading line, leaving any
|
||||
// trailing lang/flag badge untouched. Returns the new text, or null if nothing changed
|
||||
// (label already correct, or the plugin has no heading).
|
||||
export function reconcileReadmeLabel(readmeText, name, newRef) {
|
||||
let changed = false;
|
||||
const out = String(readmeText || '').split('\n').map(line => {
|
||||
if (!changed && line.includes(`/open/${name})`)) {
|
||||
const replaced = line.replace(/`v\d+\.\d+\.\d+`/, '`' + newRef + '`');
|
||||
if (replaced !== line) changed = true;
|
||||
return replaced;
|
||||
}
|
||||
return line;
|
||||
});
|
||||
return changed ? out.join('\n') : null;
|
||||
}
|
||||
|
||||
// --- I/O shell --------------------------------------------------------------
|
||||
|
||||
function gitTags(repoDir) {
|
||||
|
|
@ -157,7 +175,7 @@ function main() {
|
|||
|
||||
// READY
|
||||
if (!args.write) {
|
||||
console.log(` ✓ ready — would bump catalog ref and commit:\n ${plan.commitSubject}`);
|
||||
console.log(` ✓ ready — would bump catalog ref + README label and commit:\n ${plan.commitSubject}`);
|
||||
console.log(' (dry-run) re-run with --write [--commit] [--push] to apply.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
|
@ -165,6 +183,18 @@ function main() {
|
|||
writeFileSync(mktPath, JSON.stringify(plan.newMarketplace, null, 2) + '\n', 'utf8');
|
||||
console.log(` ✓ wrote ${mktPath} (ref ${plan.currentRef} -> ${plan.newRef})`);
|
||||
|
||||
// Keep the human-facing catalog README label in lock-step with the ref (gated by check-versions).
|
||||
const readmePath = join(catalogDir, 'README.md');
|
||||
try {
|
||||
const newReadme = reconcileReadmeLabel(readFileSync(readmePath, 'utf8'), plan.name, plan.newRef);
|
||||
if (newReadme !== null) {
|
||||
writeFileSync(readmePath, newReadme, 'utf8');
|
||||
console.log(` ✓ updated README label (${plan.name} -> ${plan.newRef})`);
|
||||
} else {
|
||||
console.log(` · README label already ${plan.newRef} (or no heading found)`);
|
||||
}
|
||||
} catch { console.log(' · no catalog README to update'); }
|
||||
|
||||
// Confirm the gate is green for this plugin after the write.
|
||||
const gate = execFileSync('node', [join(catalogDir, 'scripts', 'check-versions.mjs')], { cwd: catalogDir, encoding: 'utf8' });
|
||||
const line = gate.split('\n').find(l => l.includes(args.name)) ?? '';
|
||||
|
|
@ -173,7 +203,7 @@ function main() {
|
|||
if (args.commit) {
|
||||
const body = `${plan.name} ${plan.newRef} — release. Catalog ref now pins the ${plan.newRef} tag so \`claude plugin update\` resolves the release.`;
|
||||
const msg = `${plan.commitSubject}\n\n${body}\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>\n`;
|
||||
execFileSync('git', ['-C', catalogDir, 'add', '.claude-plugin/marketplace.json'], { stdio: 'inherit' });
|
||||
execFileSync('git', ['-C', catalogDir, 'add', '.claude-plugin/marketplace.json', 'README.md'], { stdio: 'inherit' });
|
||||
execFileSync('git', ['-C', catalogDir, 'commit', '-m', msg], { stdio: 'inherit' });
|
||||
console.log(' ✓ committed the catalog');
|
||||
if (args.push) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue