A pushed git tag is filed by Forgejo under /tags; only an explicit release object appears under /releases. release-plugin.mjs only ever made a tag, so every plugin's public releases page sat a version behind the ref the catalog pinned -- llm-security showed v7.8.3 against a v8.0.0 tag. Measured 2026-09-18 against the instance API: 24 repos in org `open`, 21 with at least one tag, 11 of those 21 with no release object for their newest tag. That reproduces the order's own independently-measured list exactly. - parseForgejoRepo / planForgejoRelease / ensureForgejoRelease: pure, tested. The release body is the tag's own message VERBATIM or empty -- never generated prose. Read via %(contents:subject)+%(contents:body), never %(contents), which drags the SSH signature block into the notes. - The step fires only on a run that PUBLISHES (--create-tag --write, or --push): filing a release object is itself a publish and must not ride along on a local --write past the operator's one-shot push token. - Synchronous (curl via execFileSync), like check-versions.mjs's checkHomepage: runRelease is called without an await and its return value becomes the exit code, so an async step would let a rejected POST surface after the run had already exited 0 and called the release complete. - 429 and the 502/503/504 family are retried with backoff, never swallowed. An unthrottled sweep drew 17 HTTP 429s and the first version of that sweep read every one as an empty list -- "verified nothing" was indistinguishable from "verified everything, all clean". - The token reaches curl through a 0600 header file, never argv. scripts/backfill-forgejo-releases.mjs covers the backlog and retries the one step, reusing the same planner and API shell so the two cannot drift. Only the newest tag is considered. Documented exception: ktg-plugin-marketplace pre-polyrepo-archive, an archive marker, not a release; the register is keyed by repo AND tag so that repo's next real release is still backfilled. Tests written red first: 20 new (12 release path, 8 backfill), and the two real-git integration tests were probed known-negative -- breaking the wiring turns 68/0 into 66/2. Suite 193/193; check-versions 12/12 OK. The backfill of the 10 outstanding release objects is NOT done: it was denied in-session as a public-surface write and is the operator's call. Order: 20260917T235642Z-730962924-from-from-ai-to-chitta Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
64 lines
3.2 KiB
JavaScript
64 lines
3.2 KiB
JavaScript
// Tests for the Forgejo release-object backfill.
|
|
// Pure selector is the unit under test — the API shell (curl via release-plugin.mjs's
|
|
// forgejoApi) is exercised against the live instance, not here.
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { planBackfill, EXCLUDED_TAGS } from './backfill-forgejo-releases.mjs';
|
|
|
|
const repo = (name, tags, releases = []) => ({ name, tags, releases });
|
|
const tag = (name, message = '') => ({ name, message });
|
|
|
|
test('plans a CREATE for a repo whose newest tag has no release object', () => {
|
|
const r = planBackfill({ repos: [repo('llm-security', [tag('v8.0.0', 'llm-security v8.0.0'), tag('v7.8.3')], ['v7.8.3'])] });
|
|
assert.equal(r.create.length, 1);
|
|
assert.equal(r.create[0].repo, 'llm-security');
|
|
assert.equal(r.create[0].tag, 'v8.0.0');
|
|
});
|
|
|
|
test('the release body is the tag message VERBATIM — the backfill invents no release notes', () => {
|
|
const msg = '0.10.0 — a bundle carries the images its sources declare\n\nFive readers place them.';
|
|
const r = planBackfill({ repos: [repo('llm-ingestion-okf', [tag('v0.10.0', msg)], [])] });
|
|
assert.equal(r.create[0].body, msg);
|
|
});
|
|
|
|
test('skips a repo whose newest tag already has a release object (idempotent re-run)', () => {
|
|
const r = planBackfill({ repos: [repo('ai-psychosis', [tag('v1.2.2')], ['v1.2.2'])] });
|
|
assert.equal(r.create.length, 0);
|
|
assert.equal(r.skip.length, 1);
|
|
assert.match(r.skip[0].reason, /already has a release/);
|
|
});
|
|
|
|
test('skips a repo with no tags at all, and NAMES the reason — never a silent drop', () => {
|
|
const r = planBackfill({ repos: [repo('jobbsok', [])] });
|
|
assert.equal(r.create.length, 0);
|
|
assert.equal(r.skip.length, 1);
|
|
assert.equal(r.skip[0].repo, 'jobbsok');
|
|
assert.match(r.skip[0].reason, /no tags/);
|
|
});
|
|
|
|
test('only the NEWEST tag is backfilled — an older tag without a release stays untouched', () => {
|
|
const r = planBackfill({ repos: [repo('config-audit', [tag('v6.0.0'), tag('v5.9.0'), tag('v5.8.0')], [])] });
|
|
assert.deepEqual(r.create.map(c => c.tag), ['v6.0.0']);
|
|
});
|
|
|
|
test('an excluded tag is reported as a documented EXCEPTION, not dropped silently', () => {
|
|
const r = planBackfill({ repos: [repo('ktg-plugin-marketplace', [tag('pre-polyrepo-archive', 'Archive of the monorepo')], [])] });
|
|
assert.equal(r.create.length, 0);
|
|
assert.equal(r.skip.length, 1);
|
|
assert.equal(r.skip[0].excluded, true);
|
|
assert.match(r.skip[0].reason, /archive/i);
|
|
});
|
|
|
|
test('the exclusion register names the repo AND the tag — it can never blanket-skip a repo', () => {
|
|
const entry = EXCLUDED_TAGS['ktg-plugin-marketplace'];
|
|
assert.ok(entry, 'the archive tag exception must be registered');
|
|
assert.ok(entry['pre-polyrepo-archive'], 'the exception is keyed by TAG, so a future real release in that repo is still backfilled');
|
|
const r = planBackfill({ repos: [repo('ktg-plugin-marketplace', [tag('v9.0.0')], [])] });
|
|
assert.equal(r.create.length, 1, 'a normal tag in an excluded repo is still a release');
|
|
});
|
|
|
|
test('the summary reports the DENOMINATOR, so a run that verified nothing cannot read as clean', () => {
|
|
const r = planBackfill({ repos: [repo('a', [tag('v1')], ['v1']), repo('b', [tag('v2')], []), repo('c', [])] });
|
|
assert.equal(r.total, 3);
|
|
assert.equal(r.tagged, 2);
|
|
});
|