feat(release): file the Forgejo release object as part of a release
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>
This commit is contained in:
parent
328e92feb9
commit
ee2259f63f
5 changed files with 615 additions and 3 deletions
46
CLAUDE.md
46
CLAUDE.md
|
|
@ -73,8 +73,10 @@ their own Forgejo repositories under `https://git.fromaitochitta.com/open/`.
|
|||
orthogonal to `TAG-ANNOTATED`: it is the `-a` that makes the tag a real tag object, not the
|
||||
signature. Do not treat `tag.gpgsign` as the enforcement for annotation.
|
||||
- **Releasing a plugin (canonical path — `scripts/release-plugin.mjs`):** since the polyrepo split,
|
||||
a release is a TWO-repo act — tag the plugin repo AND bump the catalog `ref`. Forgetting the second
|
||||
step strands users on the old version (the exact drift this helper exists to prevent). Run
|
||||
a release is a THREE-part act — tag the plugin repo, bump the catalog `ref`, AND file the
|
||||
Forgejo **release object**. Forgetting the catalog `ref` strands users on the old version;
|
||||
forgetting the release object leaves the plugin's public releases page a version behind
|
||||
(both are drift this helper exists to prevent). Run
|
||||
`node scripts/release-plugin.mjs <plugin> [--version X.Y.Z]` — dry-run by default; it REFUSES unless
|
||||
`plugin.json` == README badge == the target version AND the `vX.Y.Z` tag exists, then prints the
|
||||
planned bump. Apply with `--write [--commit] [--push]`; `--create-tag --write` mints+pushes a missing
|
||||
|
|
@ -84,6 +86,46 @@ their own Forgejo repositories under `https://git.fromaitochitta.com/open/`.
|
|||
consistent version, `check-versions.mjs` is green by construction. Never hand-edit a `ref` or a
|
||||
README label for a release — use this. Pure planner + label reconciler + pre-flight/write step
|
||||
covered by `scripts/release-plugin.test.mjs`.
|
||||
- **The Forgejo RELEASE OBJECT is the third part of a release (added 2026-09-18, order
|
||||
`20260917T235642Z-730962924-from-from-ai-to-chitta`).** A pushed git tag is filed by Forgejo
|
||||
under `/tags`; **only an explicit release object appears under `/releases`**. Until this was
|
||||
added, `release-plugin.mjs` made a tag and nothing else — `grep releases scripts/*.mjs`
|
||||
(excluding `.test.`) returned **0**, against **6** hits for `tags` in the same file as the
|
||||
control that the query could find. Consequence, measured 2026-09-18 against the instance's
|
||||
own API: **24 repos in org `open`, 21 with at least one tag, and 11 of those 21 had NO release
|
||||
object for their newest tag** — llm-security's releases page said `v7.8.3` while the catalog
|
||||
pinned `v8.0.0`. That is the same tag-vs-published drift the catalog-`ref` bump exists to
|
||||
prevent, one surface further out, and it is what a human reads to answer "what version is out?".
|
||||
- **The release body is the tag's own message, VERBATIM, or empty.** Never generated prose —
|
||||
an invented release note is a claim about the release that nobody made. Read it with
|
||||
`%(contents:subject)` + `%(contents:body)`, **never `%(contents)`**: `~/.gitconfig` sets
|
||||
`tag.gpgsign` with `gpg.format ssh`, so `%(contents)` drags the whole
|
||||
`-----BEGIN SSH SIGNATURE-----` block into the release notes (verified on `llm-security`
|
||||
`v8.0.0`).
|
||||
- **It fires only on a run that PUBLISHES** (`--create-tag --write`, or `--push`). Filing a
|
||||
release object is itself a publish, so it must not ride along on a local `--write` and slip
|
||||
past the operator's one-shot push token. A non-publishing `--write` says so and names the
|
||||
backfill instead of going quiet.
|
||||
- **The step is synchronous (curl via `execFileSync`), like `check-versions.mjs`'s
|
||||
`checkHomepage`, and for a sharper reason:** `runRelease` is called from `main()` WITHOUT an
|
||||
`await` and returns the code `main()` hands to `process.exit`. An async step would return a
|
||||
Promise nobody awaits, so a rejected POST would surface as an unhandled rejection *after* the
|
||||
run had already exited 0 and called the release complete.
|
||||
- **`429` and the `502`/`503`/`504` gateway family are retried with backoff, never swallowed.**
|
||||
Both were seen live on 2026-09-18 — an unthrottled sweep of the 24 repos drew **17 HTTP
|
||||
429s**, and the first version of that sweep coerced every one of them to an empty list, so
|
||||
"verified nothing" and "verified everything, all clean" were indistinguishable. Anything
|
||||
else, including a `403` from a token without `write:repository`, is an ANSWER and is raised.
|
||||
- **The token reaches curl through a 0600 header file, never through `argv`** — `argv` is
|
||||
readable via `ps(1)`, and this token can write to every repository in the org.
|
||||
- **Backfill / retry: `scripts/backfill-forgejo-releases.mjs`** (dry-run by default, `--write`
|
||||
applies, `--repo <owner>/<name>` narrows). It reuses `release-plugin.mjs`'s own planner and API
|
||||
shell rather than reimplementing them, so the backlog and the release path cannot drift apart.
|
||||
It considers **only the newest tag** (that is the FERDIG-criterion the order set), and reports
|
||||
the denominator. **Documented exception:** `ktg-plugin-marketplace` `pre-polyrepo-archive` —
|
||||
an archive marker for the monorepo before the polyrepo split, not a release (non-semver, and
|
||||
no install path consumes it). The exclusion register is keyed by repo **AND tag**, never by
|
||||
repo alone, so that repo's next real release is still backfilled.
|
||||
- **`--create-tag --write` and `--push` each require the operator's push-approval token FIRST**
|
||||
(Q3, decided 2026-09-12): `~/.claude/hooks/pre-push-gate.sh` matches `git push` in command
|
||||
text and cannot see a push this script issues via `execFileSync` inside node — the script
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue