The polyrepo split made a plugin release a TWO-repo act: tag the plugin repo AND bump the catalog `ref`. The second step is manual and easily forgotten — that drift just stranded linkedin-studio on v0.4.0 while its plugin.json moved to 0.5.0. This adds the canonical release path that makes the catalog side impossible to do wrong. scripts/release-plugin.mjs: - Pure planner planRelease() — given the catalog + observed plugin state + target version, computes verdict (READY/NOOP/BLOCKED), the bumped marketplace object, and the commit subject. REFUSES (BLOCKED) unless plugin.json == README badge == target AND the vX.Y.Z tag exists, so a READY plan is check-versions- green by construction. Reuses normalizeVersion/classifyPlugin from check-versions.mjs (no duplicated rules). - I/O shell: dry-run by default; --write bumps the ref + re-runs the gate; --commit/--push apply; --create-tag mints+pushes a missing plugin tag first. - 10/10 unit tests (scripts/release-plugin.test.mjs); check-versions 9/9 still green. - Dogfooded: linkedin-studio → NOOP (already pinned v0.5.0); ms-ai-architect → BLOCKED (v1.16.0 in plugin.json was never tagged — refuses to publish it). Also: fix the stale LinkedIn Studio README label v0.4.0 -> v0.5.0 (loose end of the v0.5.0 release), and document the canonical release path in CLAUDE.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
104 lines
4.6 KiB
JavaScript
104 lines
4.6 KiB
JavaScript
// Tests for the atomic plugin-release helper.
|
|
// Pure planner is the unit under test — the I/O shell (read files, git tag/commit/push)
|
|
// is exercised by the CLI against the live tree, not here.
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { planRelease } from './release-plugin.mjs';
|
|
import { classifyPlugin } from './check-versions.mjs';
|
|
|
|
const marketplace = () => ({
|
|
name: 'ktg-plugin-marketplace',
|
|
plugins: [
|
|
{ name: 'alpha', source: { source: 'url', url: 'https://x/alpha.git', ref: 'v1.0.0' }, description: 'a' },
|
|
{ name: 'beta', source: { source: 'url', url: 'https://x/beta.git', ref: 'v2.3.0' }, description: 'b' },
|
|
],
|
|
});
|
|
|
|
const observed = (o = {}) => ({
|
|
pluginVersion: '1.1.0',
|
|
readmeBadge: '1.1.0',
|
|
tags: ['v1.1.0', 'v1.0.0'],
|
|
...o,
|
|
});
|
|
|
|
test('READY: consistent plugin, tag exists, catalog ref behind → bump planned', () => {
|
|
const p = planRelease({ marketplace: marketplace(), name: 'alpha', observed: observed() });
|
|
assert.equal(p.verdict, 'READY');
|
|
assert.equal(p.targetVersion, '1.1.0');
|
|
assert.equal(p.currentRef, 'v1.0.0');
|
|
assert.equal(p.newRef, 'v1.1.0');
|
|
assert.deepEqual(p.blockers, []);
|
|
assert.equal(p.commitSubject, 'chore(catalog): bump alpha v1.0.0 -> v1.1.0');
|
|
});
|
|
|
|
test('READY plan bumps ONLY the target plugin and does not mutate the input', () => {
|
|
const mkt = marketplace();
|
|
const p = planRelease({ marketplace: mkt, name: 'alpha', observed: observed() });
|
|
// input untouched
|
|
assert.equal(mkt.plugins[0].source.ref, 'v1.0.0');
|
|
// output bumped on alpha only
|
|
const out = p.newMarketplace.plugins;
|
|
assert.equal(out.find(x => x.name === 'alpha').source.ref, 'v1.1.0');
|
|
assert.equal(out.find(x => x.name === 'beta').source.ref, 'v2.3.0');
|
|
});
|
|
|
|
test('READY plan is green by construction (post-bump classifyPlugin is OK)', () => {
|
|
const o = observed();
|
|
const p = planRelease({ marketplace: marketplace(), name: 'alpha', observed: o });
|
|
const post = classifyPlugin({
|
|
name: 'alpha', catalogRef: p.newRef,
|
|
pluginVersion: o.pluginVersion, readmeBadge: o.readmeBadge, tags: o.tags,
|
|
});
|
|
assert.equal(post.status, 'OK');
|
|
});
|
|
|
|
test('explicit --version targets that version when consistent', () => {
|
|
const o = observed({ pluginVersion: '1.1.0', readmeBadge: '1.1.0', tags: ['v1.1.0', 'v1.0.0'] });
|
|
const p = planRelease({ marketplace: marketplace(), name: 'alpha', observed: o, targetVersion: '1.1.0' });
|
|
assert.equal(p.verdict, 'READY');
|
|
assert.equal(p.newRef, 'v1.1.0');
|
|
});
|
|
|
|
test('NOOP: catalog ref already pins the target version', () => {
|
|
const o = observed({ pluginVersion: '1.0.0', readmeBadge: '1.0.0', tags: ['v1.0.0'] });
|
|
const p = planRelease({ marketplace: marketplace(), name: 'alpha', observed: o });
|
|
assert.equal(p.verdict, 'NOOP');
|
|
assert.equal(p.newRef, 'v1.0.0');
|
|
assert.equal(p.newMarketplace, null);
|
|
});
|
|
|
|
test('BLOCKED: target version has no git tag in the plugin repo', () => {
|
|
const o = observed({ pluginVersion: '1.1.0', readmeBadge: '1.1.0', tags: ['v1.0.0'] }); // no v1.1.0
|
|
const p = planRelease({ marketplace: marketplace(), name: 'alpha', observed: o });
|
|
assert.equal(p.verdict, 'BLOCKED');
|
|
assert.ok(p.blockers.some(b => /tag v1\.1\.0/.test(b) && /not found|tag the plugin/.test(b)));
|
|
assert.equal(p.newMarketplace, null);
|
|
});
|
|
|
|
test('BLOCKED: plugin.json version != target (asked to release an undeclared version)', () => {
|
|
const o = observed({ pluginVersion: '1.1.0', readmeBadge: '1.1.0', tags: ['v1.2.0', 'v1.1.0'] });
|
|
const p = planRelease({ marketplace: marketplace(), name: 'alpha', observed: o, targetVersion: '1.2.0' });
|
|
assert.equal(p.verdict, 'BLOCKED');
|
|
assert.ok(p.blockers.some(b => /plugin\.json/.test(b) && /1\.1\.0/.test(b)));
|
|
});
|
|
|
|
test('BLOCKED: README badge disagrees with plugin.json (internal corruption)', () => {
|
|
const o = observed({ pluginVersion: '1.1.0', readmeBadge: '1.0.0', tags: ['v1.1.0'] });
|
|
const p = planRelease({ marketplace: marketplace(), name: 'alpha', observed: o });
|
|
assert.equal(p.verdict, 'BLOCKED');
|
|
assert.ok(p.blockers.some(b => /badge/i.test(b)));
|
|
});
|
|
|
|
test('BLOCKED: plugin not present in the catalog', () => {
|
|
const p = planRelease({ marketplace: marketplace(), name: 'ghost', observed: observed() });
|
|
assert.equal(p.verdict, 'BLOCKED');
|
|
assert.ok(p.blockers.some(b => /not in (the )?catalog/i.test(b)));
|
|
assert.equal(p.currentRef, null);
|
|
});
|
|
|
|
test('BLOCKED: target version cannot be resolved (no --version, no plugin.json)', () => {
|
|
const o = observed({ pluginVersion: null });
|
|
const p = planRelease({ marketplace: marketplace(), name: 'alpha', observed: o });
|
|
assert.equal(p.verdict, 'BLOCKED');
|
|
assert.ok(p.blockers.some(b => /resolve.*version|version.*not/i.test(b)));
|
|
});
|