feat(scripts): add marketplace version-consistency gate
scripts/check-versions.mjs cross-checks every plugin in marketplace.json against its sibling repo: - catalog ref must resolve to a real git tag → ERROR (dangling = install breaks) - plugin.json version == README version-badge → ERROR (internal corruption) - catalog ref == plugin.json version → WARN (catalog lags / unreleased bump) - sibling repo missing → SKIP Exit 1 on any ERROR; --strict also fails on WARN. Pure classifier covered by check-versions.test.mjs (9 tests, node --test). Documented in CLAUDE.md catalog-maintenance. Run before committing any ref change. Current run: 8 OK, 2 WARN (linkedin-studio, ms-ai-architect — catalog ref lags an untagged plugin.json bump), 0 ERROR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
This commit is contained in:
parent
563dde7c51
commit
a8d44e6d3f
3 changed files with 210 additions and 0 deletions
82
scripts/check-versions.test.mjs
Normal file
82
scripts/check-versions.test.mjs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
// Tests for the marketplace version-consistency gate.
|
||||
// Pure classifier is the unit under test — I/O shell (runGate/inspectPlugin) is exercised
|
||||
// against the live tree by the CLI, not here.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
normalizeVersion,
|
||||
extractBadgeVersion,
|
||||
classifyPlugin,
|
||||
} from './check-versions.mjs';
|
||||
|
||||
test('normalizeVersion strips a leading v', () => {
|
||||
assert.equal(normalizeVersion('v5.4.0'), '5.4.0');
|
||||
assert.equal(normalizeVersion('5.4.0'), '5.4.0');
|
||||
assert.equal(normalizeVersion(' v1.16.0 '), '1.16.0');
|
||||
});
|
||||
|
||||
test('extractBadgeVersion pulls the version from a shields.io badge', () => {
|
||||
assert.equal(extractBadgeVersion(''), '5.4.0');
|
||||
assert.equal(extractBadgeVersion('no badge here'), null);
|
||||
});
|
||||
|
||||
test('all-consistent plugin → OK', () => {
|
||||
const r = classifyPlugin({
|
||||
name: 'config-audit', catalogRef: 'v5.4.0', pluginVersion: '5.4.0',
|
||||
readmeBadge: '5.4.0', tags: ['v5.4.0', 'v5.3.0'],
|
||||
});
|
||||
assert.equal(r.status, 'OK');
|
||||
assert.ok(!r.findings.some(f => f.level === 'ERROR' || f.level === 'WARN'));
|
||||
});
|
||||
|
||||
test('catalog ref with no matching tag → ERROR (install-breaking)', () => {
|
||||
const r = classifyPlugin({
|
||||
name: 'voyage', catalogRef: 'v5.5.0', pluginVersion: '5.5.0',
|
||||
readmeBadge: '5.5.0', tags: ['v5.1.1'],
|
||||
});
|
||||
assert.equal(r.status, 'ERROR');
|
||||
assert.ok(r.findings.some(f => f.level === 'ERROR' && /no matching git tag/.test(f.msg)));
|
||||
});
|
||||
|
||||
test('plugin.json version != README badge → ERROR (internal corruption)', () => {
|
||||
const r = classifyPlugin({
|
||||
name: 'x', catalogRef: 'v1.0.0', pluginVersion: '1.0.0',
|
||||
readmeBadge: '0.9.0', tags: ['v1.0.0'],
|
||||
});
|
||||
assert.equal(r.status, 'ERROR');
|
||||
assert.ok(r.findings.some(f => f.level === 'ERROR' && /README version-badge/.test(f.msg)));
|
||||
});
|
||||
|
||||
test('catalog ref behind a RELEASED version (tag exists) → WARN, suggests bumping catalog', () => {
|
||||
const r = classifyPlugin({
|
||||
name: 'x', catalogRef: 'v1.15.0', pluginVersion: '1.16.0',
|
||||
readmeBadge: '1.16.0', tags: ['v1.16.0', 'v1.15.0'],
|
||||
});
|
||||
assert.equal(r.status, 'WARN');
|
||||
assert.ok(r.findings.some(f => f.level === 'WARN' && /bump catalog ref/.test(f.msg)));
|
||||
});
|
||||
|
||||
test('plugin.json ahead with no tag of its own → WARN, flags unreleased/untagged', () => {
|
||||
const r = classifyPlugin({
|
||||
name: 'ms-ai-architect', catalogRef: 'v1.15.0', pluginVersion: '1.16.0',
|
||||
readmeBadge: '1.16.0', tags: ['v1.15.0'],
|
||||
});
|
||||
assert.equal(r.status, 'WARN');
|
||||
assert.ok(r.findings.some(f => f.level === 'WARN' && /never tagged|unreleased/.test(f.msg)));
|
||||
});
|
||||
|
||||
test('plugin repo not found locally → SKIP', () => {
|
||||
const r = classifyPlugin({
|
||||
name: 'gone', catalogRef: 'v1.0.0', pluginVersion: null,
|
||||
readmeBadge: null, tags: null,
|
||||
});
|
||||
assert.equal(r.status, 'SKIP');
|
||||
});
|
||||
|
||||
test('ERROR dominates WARN when both apply', () => {
|
||||
const r = classifyPlugin({
|
||||
name: 'x', catalogRef: 'v2.0.0', pluginVersion: '2.1.0',
|
||||
readmeBadge: '2.1.0', tags: ['v1.9.0'], // ref v2.0.0 dangling AND catalog behind v2.1.0
|
||||
});
|
||||
assert.equal(r.status, 'ERROR');
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue