Merge pull request 'release: 0.4.1' (#190) from release/0.4.1 into main

Reviewed-on: heavy-duty/ceremony#190
Reviewed-by: kimi-reviewer-andresmgsl <andres+4@heavyduty.builders>
Reviewed-by: grok-reviewer-andresmgsl <andres+3@heavyduty.builders>
This commit is contained in:
andres 2026-08-04 16:07:39 +00:00
commit 0003fa1d1c
7 changed files with 118 additions and 104 deletions

View file

@ -28,7 +28,7 @@ on:
env:
# A called workflow arrives without its repository. Keep this literal pin
# aligned with the ceremony release consumed by callers (issue #9 D3).
CEREMONY_SELF_REF: "0.4.0"
CEREMONY_SELF_REF: "0.4.1"
jobs:
scope:

View file

@ -129,7 +129,7 @@ env:
# `ref:` accepts ${{ env }}; `uses:` strings do not — which is why the
# shared logic arrives as script files via checkout, not as inner `uses:`
# references.
CEREMONY_SELF_REF: "0.4.0"
CEREMONY_SELF_REF: "0.4.1"
VERSION_SOURCE: ${{ inputs.version-source }}
jobs:

View file

@ -9,6 +9,96 @@ Entries arrive as fragments — one `changelog.d/<issue>.md` per PR, never
an edit to this file — and the release PR assembles them into the next
section here (`bin/changelog-assemble`, #112).
## 0.4.1 — 2026-08-04
### Added
- `forge_release_exists`, `forge_commit_pulls`, `forge_tag_create`,
`forge_release_create` and `forge_pr_create` on both backends, so the
release path names no client (#191).
- The forgejo backend serves one PR object at `/commits/{sha}/pull` where
GitHub serves an array at `/pulls`; both verbs emit the array shape, so
the call site carries one expression (#191).
- Forgejo creates tags at `POST /tags` — it serves `/git/refs` GET-only,
so GitHub's ref-POST would have 404'd there forever (#191).
- `forgejo_api_base` refuses when `REPO` is empty. Every verb interpolates
it and every call reaches the network through there, so `repos//…`
whose 404 reads as "no release" and "no PRs" — is now impossible (#191).
- Release asset names are percent-encoded. The hook contract permits any
filename, and the name travels as a query value: a space made curl reject
the URL and `&`/`#`/`+`/`%` silently renamed the asset (#191).
- `lib/forge.sh` — the forge selector: `forge_detect` names the forge from
the runner's own environment, `forge_client` names the client it needs, and
`forge_preflight` refuses loudly before any sweep when the two disagree
(#188).
- The reconcilers and `labels-scope` run that preflight first, so a
GitHub-shaped client on a Forgejo instance is a named refusal instead of a
sweep that reads nothing and reports success (#188).
- `lib/closes_references.sh` — the closing-keyword parser, sibling of
`refs_references`, so "which issues does this PR close" is answered from a
PR body rather than from GitHub's GraphQL API (#188).
- `lib/forge-github.sh` and `lib/forge-forgejo.sh` — one call surface, two
backends, selected by `forge_select`; no forge branching at the call sites
(#188).
- The forgejo backend proves each paginated gather complete against the
server's `x-total-count` and refuses loudly when it cannot — a missing
header is a refusal, not a pass (#188).
### Changed
- `docs/CONSUMERS.md`'s artifact-hook recovery no longer tells operators to
run `gh release create` by hand — on a Forgejo runner there is no `gh`.
It names the forge-neutral tag-door path first, with both clients shown
(#191).
- `issueflow-reconcile` gathers open and merged PRs over REST instead of
`gh api graphql`. Forgejo serves no GraphQL at all, so the two queries were
replaced rather than translated; both forges return `number` and `body`
from `/pulls` in the same shape (#188).
- `forge_api` owns the page size, because each forge silently ignores the
other's parameter: `per_page=100` reads 30 items on Forgejo and `limit=100`
reads 30 on GitHub, both HTTP 200. No call site names one (#188).
- Outstanding review requests are derived from the reviews on the current head
rather than from `requested_reviewers`, which Forgejo never clears — read
raw there, a PR would sit at `state:bots-reviewing` forever (#188).
### Fixed
- The release doors run on a Forgejo consumer. `lib/facts.sh` and
`release.yml` gathered and published through `gh`, which the runner image
does not ship, so the merge door read `labeled=no` for a correctly
labeled ceremony PR and the tag door died at the publish (#191).
- A release fact that could not be read is no longer reported as a definite
`no`. A completed read finding no label is still `no` and still
fail-closed; a read that did not complete refuses and emits no fact
(#191).
- `labels-scope` no longer fails to compile its jq program on jq 1.6, which
the Forgejo runner image ships: `label` is a reserved word in jq's grammar,
so `$label` is a syntax error there and every scope derivation died before
reading the config (#188).
- `labels-reconcile` and `labels-scope` no longer exit 0 on a Forgejo
consumer having read zero facts — measured on `heavy-duty/rig`, where the
sweep printed `reconciled.` over an empty PR list and scope reported "no
labeler.yml" for a file that exists (#188).
- `forge_timeline` projects Forgejo's label events (`.type` / `.body` /
`.user.login`) into the GitHub shape (`.event` / `.actor.login`) so the
ruling ladder reads the same board on both forges (#188).
- `forge_pr_activity` no longer calls `/pulls/{n}/comments` on Forgejo
(HTTP 404); inline review comments come from reviews with
`comments_count > 0` (#188).
- CI installs shellcheck before linting, matching actionlint — the Forgejo
runner image does not ship it (#188).
## 0.4.0 — 2026-07-29
### Added

View file

@ -1 +1 @@
0.4.1-dev
0.4.1

View file

@ -1,56 +0,0 @@
### Added
- `lib/forge.sh` — the forge selector: `forge_detect` names the forge from
the runner's own environment, `forge_client` names the client it needs, and
`forge_preflight` refuses loudly before any sweep when the two disagree
(#188).
- The reconcilers and `labels-scope` run that preflight first, so a
GitHub-shaped client on a Forgejo instance is a named refusal instead of a
sweep that reads nothing and reports success (#188).
- `lib/closes_references.sh` — the closing-keyword parser, sibling of
`refs_references`, so "which issues does this PR close" is answered from a
PR body rather than from GitHub's GraphQL API (#188).
- `lib/forge-github.sh` and `lib/forge-forgejo.sh` — one call surface, two
backends, selected by `forge_select`; no forge branching at the call sites
(#188).
- The forgejo backend proves each paginated gather complete against the
server's `x-total-count` and refuses loudly when it cannot — a missing
header is a refusal, not a pass (#188).
### Changed
- `issueflow-reconcile` gathers open and merged PRs over REST instead of
`gh api graphql`. Forgejo serves no GraphQL at all, so the two queries were
replaced rather than translated; both forges return `number` and `body`
from `/pulls` in the same shape (#188).
- `forge_api` owns the page size, because each forge silently ignores the
other's parameter: `per_page=100` reads 30 items on Forgejo and `limit=100`
reads 30 on GitHub, both HTTP 200. No call site names one (#188).
- Outstanding review requests are derived from the reviews on the current head
rather than from `requested_reviewers`, which Forgejo never clears — read
raw there, a PR would sit at `state:bots-reviewing` forever (#188).
### Fixed
- `labels-scope` no longer fails to compile its jq program on jq 1.6, which
the Forgejo runner image ships: `label` is a reserved word in jq's grammar,
so `$label` is a syntax error there and every scope derivation died before
reading the config (#188).
- `labels-reconcile` and `labels-scope` no longer exit 0 on a Forgejo
consumer having read zero facts — measured on `heavy-duty/rig`, where the
sweep printed `reconciled.` over an empty PR list and scope reported "no
labeler.yml" for a file that exists (#188).
- `forge_timeline` projects Forgejo's label events (`.type` / `.body` /
`.user.login`) into the GitHub shape (`.event` / `.actor.login`) so the
ruling ladder reads the same board on both forges (#188).
- `forge_pr_activity` no longer calls `/pulls/{n}/comments` on Forgejo
(HTTP 404); inline review comments come from reviews with
`comments_count > 0` (#188).
- CI installs shellcheck before linting, matching actionlint — the Forgejo
runner image does not ship it (#188).

View file

@ -1,39 +0,0 @@
### Fixed
- The release doors run on a Forgejo consumer. `lib/facts.sh` and
`release.yml` gathered and published through `gh`, which the runner image
does not ship, so the merge door read `labeled=no` for a correctly
labeled ceremony PR and the tag door died at the publish (#191).
- A release fact that could not be read is no longer reported as a definite
`no`. A completed read finding no label is still `no` and still
fail-closed; a read that did not complete refuses and emits no fact
(#191).
### Added
- `forge_release_exists`, `forge_commit_pulls`, `forge_tag_create`,
`forge_release_create` and `forge_pr_create` on both backends, so the
release path names no client (#191).
- The forgejo backend serves one PR object at `/commits/{sha}/pull` where
GitHub serves an array at `/pulls`; both verbs emit the array shape, so
the call site carries one expression (#191).
- Forgejo creates tags at `POST /tags` — it serves `/git/refs` GET-only,
so GitHub's ref-POST would have 404'd there forever (#191).
- `forgejo_api_base` refuses when `REPO` is empty. Every verb interpolates
it and every call reaches the network through there, so `repos//…`
whose 404 reads as "no release" and "no PRs" — is now impossible (#191).
- Release asset names are percent-encoded. The hook contract permits any
filename, and the name travels as a query value: a space made curl reject
the URL and `&`/`#`/`+`/`%` silently renamed the asset (#191).
### Changed
- `docs/CONSUMERS.md`'s artifact-hook recovery no longer tells operators to
run `gh release create` by hand — on a Forgejo runner there is no `gh`.
It names the forge-neutral tag-door path first, with both clients shown
(#191).

View file

@ -82,13 +82,32 @@ door has.
## Run 1 — against `9a229ee`, before the fix (FAILED)
Recorded in full because the failure is the reason #191 exists.
Recorded because the failure is the reason #191 exists — including its
deviations, which no later success retires.
| # | probe | result |
|---|---|---|
| 1 | merge-door ceremony | ❌ `decide` refused: `labeled=no` for a PR that *was* labeled and *was* merged |
| 5 | tag door | ❌ `gh: command not found` at *publish the release* |
| 6 | mismatched tag | ✅ refused, creating nothing |
Where: disposable private repo `heavy-duty/ceremony-drill-0.4.1` — a
different consumer from run 2's, archived at the end with the operator's
delete pending. Candidate ref
`cluade-reviewer-andresmgsl/ceremony@drill-0.4.1` (`f148255`), parent
`9a229ee`, its only extra commit rewriting both `CEREMONY_SELF_REF` carriers
to that SHA.
**Deviation, disclosed and not retired by run 2:** run 1's scratch repo was
flipped **public for roughly 8 minutes** to read job logs — Forgejo's web log
route 404s for a token-authenticated private repo and the `/api/v1` log
routes 404 outright — then restored to private and archived. That is a real
departure from `drills/README.md`'s "scratch **private** repo", and it stays
in the record. Run 2 did not repeat it: it read no logs at all, which is why
every run-2 row is a repository-state measurement.
| # | probe | run | result |
|---|---|---|---|
| 1 | merge-door ceremony | 12 (and 7, at `0.4.1`) | ❌ **FAILED**`decide` refused; no tag, no release, main left bare |
| 2 | `-dev` push no-op | 9, and the fixture push | ✅ green no-op, nothing created |
| 3 | bare version, no `release` label | — | ⚠️ not run independently; probe 1 refused through exactly this path, because the label fact read `no` |
| 4 | re-run of a completed ceremony | — | ⚠️ **not reachable** — no ceremony ever completed to re-run |
| 5 | tag door | 14 | ❌ **FAILED**`gh: command not found` at *publish the release*; no release |
| 6 | mismatched tag | 16 | ✅ `tag '9.9.9' does not match the tree's version '0.4.2' — creating nothing` |
Root cause, both doors: `lib/facts.sh` and `release.yml` spoke `gh`, which
the runner image does not ship. `facts.sh` read *any* failure as a definite