fix(release-decide): Layer 2 cut-safeguard uses list-and-filter with narrow window — switch to explicit PR lookup #240

Closed
opened 2026-06-28 18:33:59 +02:00 by quartermaster · 0 comments

What happened (empirical anchor)

The v0.17.0 cut on 2026-06-28 was supposed to fire on the merge of rolling PR #227. Instead, decide+act fell through to mode=update and re-prepped v0.17.0 as a new rolling PR (#239). Recovered by merging #239 (which had a newer issue_id so it landed within Layer 2's lookup window).

Root cause: forgejo_find_pr_by_merge_sha in scripts/lib/forgejo-api.sh queries GET /pulls?state=closed&sort=updated&limit=5 and filters client-side by merge_commit_sha. Two problems compound:

  1. Forgejo's sort=updated empirically sorts by issue_id descending, not by updated_at. So a PR that was created earlier but merged most-recently is positioned lower than throwaway/probe PRs with newer issue_ids.

  2. limit=5 was an unsafe optimization. Today's v0.16.0 → v0.17.0 cycle had 10+ PRs close between cuts (audit PRs + Engineer's batch + Pilot's fix + re-pin + throwaway probes). PR #227 fell to position 11 in the list — completely outside the lookup window. Layer 2 returned empty even after retries (the retry window assumes API indexing lag; here the data is simply not in the limit=5 result).

Source comment is honest about the typical-case optimization: "Queries the 5 most-recently-closed PRs (sufficient for typical merge cadence + bounded query cost)." Typical merge cadence has grown past 5.

Why this needs the substrate fix

Every future cycle with >4 closed PRs between cuts will hit this. The release-toolkit substrate's release-cadence has been climbing (sprints with 5-15 PRs between cuts are now typical). The current shape relies on an arbitrary window value — an "off-by-typical-cadence" footgun.

Three options

(3) Explicit PR lookup by commit SHA — PREFERRED

Forgejo may have a direct endpoint like GET /repos/{owner}/{repo}/commits/{sha}/pulls (GitHub has it). If available, this bypasses the list-and-filter entirely + is the substrate-honest answer (explicit query for what we want, no arbitrary window).

Recon needed: verify Forgejo 15.0.2 exposes such an endpoint OR equivalent. Check:

  • /repos/{owner}/{repo}/commits/{sha}/pulls (GitHub-parity)
  • /repos/{owner}/{repo}/git/commits/{sha}/pull (alternate path)
  • Whether the commits API includes associated PR info in the response

(1) Increase limit — FALLBACK

If no better endpoint exists in Forgejo 15.0.2: change limit=5 to limit=50 (Forgejo's typical max per page). Still has a ceiling but covers all reasonable cycles.

(2) Paginate until found — REJECTED

More complex + unbounded cost; not preferred unless (3) is unavailable AND a single limit=50 page is also insufficient.

Implementation surface

  • scripts/lib/forgejo-api.sh: replace forgejo_find_pr_by_merge_sha body with direct lookup (option 3) or wider list (option 1)
  • tests/forgejo-api.bats: add a regression test that mocks the failure mode (the list-window misses the cut PR)
  • Possibly: update the function's docstring + the Layer 2 comment in scripts/release-decide.sh
  • Possibly: update docs/integration.md or AGENTS.md if any docs reference the cut-safeguard layers

Estimated scope: ~30 lines + bats coverage. Small Engineer-shape recon-before-build.

What this PR will NOT do

  • Will NOT change Layer 1 (subject-regex) or Layer 3 (release_author check)
  • Will NOT modify the retry budget for any future API-lag scenarios (the retry is still useful for true post-merge indexing lag, just not for the window-miss case this issue fixes)
  • Will NOT increase the integration.md doc surface unless the cut-safeguard documentation has explicit references that drift

Refs

  • Empirical anchor: v0.17.0 cut on 2026-06-28 (manual workaround via #239 merge after #227 missed Layer 2 window)
  • Sibling: scripts/release-decide.sh Layer 2 logic (caller of the to-be-fixed function)
  • v0.17.1 patch candidate

priority/high · size/S

## What happened (empirical anchor) The v0.17.0 cut on 2026-06-28 was supposed to fire on the merge of rolling PR #227. Instead, `decide+act` fell through to `mode=update` and re-prepped v0.17.0 as a new rolling PR (#239). Recovered by merging #239 (which had a newer issue_id so it landed within Layer 2's lookup window). **Root cause**: `forgejo_find_pr_by_merge_sha` in `scripts/lib/forgejo-api.sh` queries `GET /pulls?state=closed&sort=updated&limit=5` and filters client-side by `merge_commit_sha`. Two problems compound: 1. Forgejo's `sort=updated` empirically sorts by **issue_id descending**, not by `updated_at`. So a PR that was created earlier but merged most-recently is positioned lower than throwaway/probe PRs with newer issue_ids. 2. `limit=5` was an unsafe optimization. Today's v0.16.0 → v0.17.0 cycle had 10+ PRs close between cuts (audit PRs + Engineer's batch + Pilot's fix + re-pin + throwaway probes). PR #227 fell to position 11 in the list — completely outside the lookup window. Layer 2 returned empty even after retries (the retry window assumes API indexing lag; here the data is simply not in the limit=5 result). Source comment is honest about the typical-case optimization: *"Queries the 5 most-recently-closed PRs (sufficient for typical merge cadence + bounded query cost)."* Typical merge cadence has grown past 5. ## Why this needs the substrate fix **Every future cycle with >4 closed PRs between cuts will hit this**. The release-toolkit substrate's release-cadence has been climbing (sprints with 5-15 PRs between cuts are now typical). The current shape relies on an arbitrary window value — an "off-by-typical-cadence" footgun. ## Three options ### (3) Explicit PR lookup by commit SHA — PREFERRED Forgejo may have a direct endpoint like `GET /repos/{owner}/{repo}/commits/{sha}/pulls` (GitHub has it). If available, this bypasses the list-and-filter entirely + is the substrate-honest answer (explicit query for what we want, no arbitrary window). **Recon needed**: verify Forgejo 15.0.2 exposes such an endpoint OR equivalent. Check: - `/repos/{owner}/{repo}/commits/{sha}/pulls` (GitHub-parity) - `/repos/{owner}/{repo}/git/commits/{sha}/pull` (alternate path) - Whether the `commits` API includes associated PR info in the response ### (1) Increase limit — FALLBACK If no better endpoint exists in Forgejo 15.0.2: change `limit=5` to `limit=50` (Forgejo's typical max per page). Still has a ceiling but covers all reasonable cycles. ### (2) Paginate until found — REJECTED More complex + unbounded cost; not preferred unless (3) is unavailable AND a single limit=50 page is also insufficient. ## Implementation surface - `scripts/lib/forgejo-api.sh`: replace `forgejo_find_pr_by_merge_sha` body with direct lookup (option 3) or wider list (option 1) - `tests/forgejo-api.bats`: add a regression test that mocks the failure mode (the list-window misses the cut PR) - Possibly: update the function's docstring + the Layer 2 comment in `scripts/release-decide.sh` - Possibly: update `docs/integration.md` or `AGENTS.md` if any docs reference the cut-safeguard layers Estimated scope: ~30 lines + bats coverage. Small Engineer-shape recon-before-build. ## What this PR will NOT do - Will NOT change Layer 1 (subject-regex) or Layer 3 (release_author check) - Will NOT modify the retry budget for any future API-lag scenarios (the retry is still useful for true post-merge indexing lag, just not for the window-miss case this issue fixes) - Will NOT increase the integration.md doc surface unless the cut-safeguard documentation has explicit references that drift ## Refs - Empirical anchor: v0.17.0 cut on 2026-06-28 (manual workaround via #239 merge after #227 missed Layer 2 window) - Sibling: `scripts/release-decide.sh` Layer 2 logic (caller of the to-be-fixed function) - v0.17.1 patch candidate priority/high · size/S
Sign in to join this conversation.
No milestone
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#240
No description provided.