path-gamma consumer never received a manifest PR — benign by fallback, silent by design? #661

Closed
opened 2026-08-06 11:20:30 +02:00 by engineer · 3 comments
Owner

Observation, not a diagnosis

frankenbit/purser completed one full cut (v0.1.0, release job green, release published
2026-08-05 22:38). It is a path-gamma consumer — no RELEASE_TOOLKIT_TOKEN, so
RELEASE_TOKEN_OVERRIDE is empty:

reusable-release.yml:458  if [[ -n "${RELEASE_TOKEN_OVERRIDE:-}" ]]; then PUSH_MODE=alpha; else PUSH_MODE=gamma; fi
reusable-release.yml:814  # Path (gamma): existing manifest-via-PR mechanism (#78).
reusable-release.yml:815  echo "manifest update: path (gamma) via PR (no RELEASE_TOOLKIT_TOKEN
                          - graceful-degradation per ADR-0007)"

So gamma delivers the manifest by PR. purser has no manifest, and no manifest PR ever existed.

manifest at tag v0.1.0 / on main / any candidate path   404
manifest PRs                                            ZERO
  ⇒ established UNFILTERED: all 20 PRs listed by title, not grepped
    (a title filter cannot prove absence; the grep would also have matched
     "chore(release): update manifest" had one existed)

Why it matters

The absence is currently benign — release-decide.sh:221 bootstraps LAST_VERSION from the newest
stable tag, and that path works. But it is benign by fallback, not by design: a gamma consumer is
supposed to receive a manifest PR and never did, so it has been running on the bootstrap path since
adoption without anyone noticing.

It surfaced only because purser hit #417 (orphan CHANGELOG) and three of us had to reason about what
"update the manifest" means for a repo that has none.

What I did NOT check

  • The v0.1.0 run's logs — whether the gamma branch was reached, errored, or no-op'd. That is the
    discriminator and I have not read it.
  • Whether the manifest is expected to ride inside the rolling PR (ADR-0004) rather than a separate
    one, in which case the question becomes why it was absent from #17/#21.

So this is "the delivery did not happen", not "the code is broken". Those need different fixes and
I am not asserting the second.

Suggested first step

Read the v0.1.0 release run's log around the manifest step. If gamma skipped silently, that is a
graceful-degradation path degrading past the point of usefulness — a consumer that never gets a
manifest gets no signal that it did not.

Related: #660 (the recovery doc's manifest wording), #78 (the gamma mechanism), ADR-0007.

## Observation, not a diagnosis `frankenbit/purser` completed one full cut (v0.1.0, release job green, release published 2026-08-05 22:38). It is a **path-gamma** consumer — no `RELEASE_TOOLKIT_TOKEN`, so `RELEASE_TOKEN_OVERRIDE` is empty: ``` reusable-release.yml:458 if [[ -n "${RELEASE_TOKEN_OVERRIDE:-}" ]]; then PUSH_MODE=alpha; else PUSH_MODE=gamma; fi reusable-release.yml:814 # Path (gamma): existing manifest-via-PR mechanism (#78). reusable-release.yml:815 echo "manifest update: path (gamma) via PR (no RELEASE_TOOLKIT_TOKEN - graceful-degradation per ADR-0007)" ``` **So gamma delivers the manifest by PR. purser has no manifest, and no manifest PR ever existed.** ``` manifest at tag v0.1.0 / on main / any candidate path 404 manifest PRs ZERO ⇒ established UNFILTERED: all 20 PRs listed by title, not grepped (a title filter cannot prove absence; the grep would also have matched "chore(release): update manifest" had one existed) ``` ## Why it matters The absence is currently benign — `release-decide.sh:221` bootstraps `LAST_VERSION` from the newest stable tag, and that path works. But it is benign **by fallback**, not by design: a gamma consumer is supposed to receive a manifest PR and never did, so it has been running on the bootstrap path since adoption without anyone noticing. It surfaced only because purser hit #417 (orphan CHANGELOG) and three of us had to reason about what "update the manifest" means for a repo that has none. ## What I did NOT check - **The v0.1.0 run's logs** — whether the gamma branch was reached, errored, or no-op'd. That is the discriminator and I have not read it. - Whether the manifest is expected to ride inside the rolling PR (ADR-0004) rather than a separate one, in which case the question becomes why it was absent from `#17`/`#21`. **So this is "the delivery did not happen", not "the code is broken".** Those need different fixes and I am not asserting the second. ## Suggested first step Read the v0.1.0 release run's log around the manifest step. If gamma skipped silently, that is a graceful-degradation path degrading past the point of usefulness — a consumer that never gets a manifest gets no signal that it did not. Related: #660 (the recovery doc's manifest wording), #78 (the gamma mechanism), ADR-0007.
Author
Owner

Remedy verdict — LIVE and unchanged

path (gamma) manifest-via-PR is intact — reusable-release.yml:814-822
  MANIFEST_BRANCH="release-toolkit/manifest-${CUT_TAG}"
  git checkout -B "$MANIFEST_BRANCH"  (-B + --force-with-lease hardening present)

The mechanism is present and unchanged, which sharpens rather than weakens this issue: the code
that should open the PR exists, and a gamma consumer has still never received one.
So the
hypothesis space is the branch runs and the PR-open step fails silently, not the path was removed.

The remedy as filed — read the v0.1.0 run log around the manifest step — is still the correct and
cheapest next action
, and still unrun.

🔗 See #637: that is the alpha arm of the same subsystem regressing. Both fail silently, both
branch off :458. Neither has the negative control #637's AC4 asks for.

## Remedy verdict — **LIVE and unchanged** ``` path (gamma) manifest-via-PR is intact — reusable-release.yml:814-822 MANIFEST_BRANCH="release-toolkit/manifest-${CUT_TAG}" git checkout -B "$MANIFEST_BRANCH" (-B + --force-with-lease hardening present) ``` The mechanism is present and unchanged, which sharpens rather than weakens this issue: **the code that should open the PR exists, and a gamma consumer has still never received one.** So the hypothesis space is *the branch runs and the PR-open step fails silently*, not *the path was removed*. **The remedy as filed — read the v0.1.0 run log around the manifest step — is still the correct and cheapest next action**, and still unrun. 🔗 See `#637`: that is the **alpha** arm of the same subsystem regressing. Both fail silently, both branch off `:458`. Neither has the negative control `#637`'s AC4 asks for.
Author
Owner

Answered. The cause is #637's defect seen from the gamma side, and it was fixed in v0.36.0

The suggested first step — read the v0.1.0 run log around the manifest step — is done. It was the discriminator, as filed.

Finding the log, because the obvious read was wrong

release job task 20755 has an empty log_filename in Forgejo's action_task table, and its neighbours have logs. That reads as a deleted log and it is not one:

job name              tasks   with no log
release                29        29          <- the WRAPPER job, `uses:` the reusable
check                 108       108          <- likewise
decide + act           29         0          <- the job that EXECUTES
lint + build + test   121         0

A uses: caller job is a container with no execution and therefore no log. The v0.1.0 log is decide + act, task 20754.

The log line, and it is unambiguous

[rt decide] manifest absent; bootstrapping from latest stable tag
push-whitelist pre-flight (path-gamma):
[preflight-push-whitelist] path-gamma (PR-mediated manifest) -> no direct-push -> whitelist precondition N/A; skipping
manifest update: already at HEAD_SHA=828d97f3... (last cut for v0.1.0) - idempotent skip

The gamma branch was never reached. The idempotency precheck skipped first, and the step exited 0. So the answer to "was gamma reached, errored, or no-op'd" is: it was never reached.

Why the precheck skipped against a manifest that does not exist

Because in v0.35.0 it read the working tree:

v0.35.0   PRECHECK_DECISION=$(rt manifest-precheck "$MANIFEST_PATH_INPUT" "$HEAD_SHA")   <- the workspace file
main      … git show "FETCH_HEAD:${MANIFEST_PATH_INPUT}" … rt manifest-precheck "$PRECHECK_SRC"   <- the remote branch tip

rt release runs above this step and writes the manifest itself. So by the time the guard ran, the workspace file recorded HEAD_SHA, and the guard read its own run's write and skipped its own run's work. The workspace copy then died with the runner: never committed, never PR'd, and changelog.d-style consumption meant nothing surfaced it.

This is the same defect as #637, and the identifiers close the window

purser v0.1.0 ran      reusable-release.yml@v0.35.0        (release.yml:41 at 828d97f3)
fix a4553a1 (PR#640)   2026-08-01, first tag v0.36.0
v0.35.0 contains a4553a1?   NO      v0.36.0 contains it?   YES

So it is not a gamma-specific bug. The precheck sits above the alpha/gamma split, so both paths were affected by one commit:

#637   ALPHA side, release-toolkit itself   no release-bot manifest commit for v0.34.0 / v0.35.0
#661   GAMMA side, purser                   no manifest PR, ever

Two consumers, two paths, two independent observations, one cause.

purser is already repinned

release.yml today reads reusable-release.yml@v0.36.0, so purser carries the fix. No code change is indicated by this tracker.

What is NOT resolved, and it is the part worth keeping

purser still has no manifest, because the missed write was never retried, and nothing anywhere says so. That is this tracker's own framing — a consumer that never gets a manifest gets no signal that it did not — and it survives the fix.

The gamma PR-open itself is fail-loud (::error::failed to open manifest PR + exit 1), so the remaining hole is not there. It is the absence of a post-condition: a cut can complete with no manifest landed and no manifest PR open, and exit 0.

That is exactly #637's AC3 and AC4. The two trackers converge on one piece of work rather than two, and I would rather build it once — writing that up on #637.

Method note against myself

Two instrument errors on the way here, both self-caught, both worth naming because they nearly produced findings:

  1. A nested clone's origin/main is the PARENT's local main, not the parent's origin/main. Mine was one commit stale, and the composed section came back missing a fragment that is plainly on main — one sentence from "prep silently drops fragments".
  2. log_filename empty read as "the log was deleted." It means the job never executed anything. The DB had the answer; the filesystem did not.
## Answered. The cause is **#637's defect seen from the gamma side**, and it was fixed in v0.36.0 The suggested first step — *read the v0.1.0 run log around the manifest step* — is done. It was the discriminator, as filed. ### Finding the log, because the obvious read was wrong `release` job task **20755** has an **empty `log_filename`** in Forgejo's `action_task` table, and its neighbours have logs. That reads as a deleted log and it is not one: ``` job name tasks with no log release 29 29 <- the WRAPPER job, `uses:` the reusable check 108 108 <- likewise decide + act 29 0 <- the job that EXECUTES lint + build + test 121 0 ``` **A `uses:` caller job is a container with no execution and therefore no log.** The v0.1.0 log is `decide + act`, task `20754`. ### The log line, and it is unambiguous ``` [rt decide] manifest absent; bootstrapping from latest stable tag push-whitelist pre-flight (path-gamma): [preflight-push-whitelist] path-gamma (PR-mediated manifest) -> no direct-push -> whitelist precondition N/A; skipping manifest update: already at HEAD_SHA=828d97f3... (last cut for v0.1.0) - idempotent skip ``` **The gamma branch was never reached.** The idempotency precheck skipped first, and the step exited 0. So the answer to *"was gamma reached, errored, or no-op'd"* is: **it was never reached.** ### Why the precheck skipped against a manifest that does not exist Because in `v0.35.0` it read the **working tree**: ``` v0.35.0 PRECHECK_DECISION=$(rt manifest-precheck "$MANIFEST_PATH_INPUT" "$HEAD_SHA") <- the workspace file main … git show "FETCH_HEAD:${MANIFEST_PATH_INPUT}" … rt manifest-precheck "$PRECHECK_SRC" <- the remote branch tip ``` `rt release` runs **above** this step and writes the manifest itself. So by the time the guard ran, the workspace file recorded `HEAD_SHA`, and **the guard read its own run's write and skipped its own run's work.** The workspace copy then died with the runner: never committed, never PR'd, and `changelog.d`-style consumption meant nothing surfaced it. ### This is the same defect as #637, and the identifiers close the window ``` purser v0.1.0 ran reusable-release.yml@v0.35.0 (release.yml:41 at 828d97f3) fix a4553a1 (PR#640) 2026-08-01, first tag v0.36.0 v0.35.0 contains a4553a1? NO v0.36.0 contains it? YES ``` **So it is not a gamma-specific bug.** The precheck sits **above** the alpha/gamma split, so both paths were affected by one commit: ``` #637 ALPHA side, release-toolkit itself no release-bot manifest commit for v0.34.0 / v0.35.0 #661 GAMMA side, purser no manifest PR, ever ``` Two consumers, two paths, two independent observations, one cause. ### ✅ purser is already repinned `release.yml` today reads `reusable-release.yml@v0.36.0`, so purser carries the fix. **No code change is indicated by this tracker.** ### What is NOT resolved, and it is the part worth keeping purser still has **no manifest**, because the missed write was never retried, and *nothing anywhere says so*. That is this tracker's own framing — *a consumer that never gets a manifest gets no signal that it did not* — and it survives the fix. The gamma PR-open itself is fail-loud (`::error::failed to open manifest PR` + `exit 1`), so the remaining hole is not there. It is the **absence of a post-condition**: a cut can complete with no manifest landed and no manifest PR open, and exit 0. **That is exactly #637's AC3 and AC4.** The two trackers converge on one piece of work rather than two, and I would rather build it once — writing that up on #637. ### Method note against myself Two instrument errors on the way here, both self-caught, both worth naming because they nearly produced findings: 1. **A nested clone's `origin/main` is the PARENT's local `main`, not the parent's `origin/main`.** Mine was one commit stale, and the composed section came back missing a fragment that is plainly on `main` — one sentence from *"prep silently drops fragments"*. 2. **`log_filename` empty read as "the log was deleted."** It means the job never executed anything. The DB had the answer; the filesystem did not.
Author
Owner

Closing — the residual landed in #741 (merged at dac08dd)

This tracker was answered in comment 95834: purser's v0.1.0 ran reusable-release.yml@v0.35.0, whose manifest precheck read the working tree. rt release writes the manifest there first, so the guard read its own run's write and skipped — every time, silently, exit 0. Regression commit 6547d9c (mine), fixed by a4553a1 in v0.36.0; purser had already repinned.

The residual was the missing post-condition, and it is the same object as #637 AC3/AC4 — one mechanism, two trackers, opposite paths. That is why this stayed open after the cause was named: the precheck fix stopped the guard reading its own write, but nothing yet refused a cut that finished having written no bookkeeping at all.

#741 adds it, verified on main at dac08dd:

post-condition = step 10 of 11, a SEPARATE step
  → the precheck's `exit 0` cannot reach past it
asserts: manifest records HEAD_SHA  OR  a manifest PR is open
  0 landed / PR open   ·   1 CONVICTED   ·   2 could-not-grade

Gamma-specific, since that is this tracker's path: an open manifest PR passes, because it is path gamma's terminal state — the bookkeeping lands on merge. The gate cannot tell you whether that merge will happen, and its pass message says so rather than leaving it implicit. What it now refuses is the case this tracker reported: a cut finishing with neither a landed manifest nor an open PR, and exiting 0.

Closing as delivered. #637 carries the ticked ACs and the fuller scope note (comment 96426).

## Closing — the residual landed in `#741` (merged at `dac08dd`) This tracker was **answered** in comment 95834: purser's `v0.1.0` ran `reusable-release.yml@v0.35.0`, whose manifest precheck read the **working tree**. `rt release` writes the manifest there first, so the guard read its own run's write and skipped — every time, silently, exit 0. Regression commit `6547d9c` (mine), fixed by `a4553a1` in `v0.36.0`; purser had already repinned. **The residual was the missing post-condition**, and it is the same object as `#637` AC3/AC4 — one mechanism, two trackers, opposite paths. That is why this stayed open after the cause was named: the precheck fix stopped the guard reading its own write, but nothing yet refused a cut that finished having written no bookkeeping at all. `#741` adds it, verified on `main` at `dac08dd`: ``` post-condition = step 10 of 11, a SEPARATE step → the precheck's `exit 0` cannot reach past it asserts: manifest records HEAD_SHA OR a manifest PR is open 0 landed / PR open · 1 CONVICTED · 2 could-not-grade ``` **Gamma-specific, since that is this tracker's path:** an open manifest PR **passes**, because it is path gamma's terminal state — the bookkeeping lands on merge. The gate cannot tell you whether that merge will happen, and its pass message says so rather than leaving it implicit. What it now refuses is the case this tracker reported: a cut finishing with **neither** a landed manifest **nor** an open PR, and exiting 0. Closing as delivered. `#637` carries the ticked ACs and the fuller scope note (comment 96426).
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/release-toolkit#661
No description provided.