fix(_release): manifest update via PR instead of direct-push to main (closes #78) #79

Merged
quartermaster merged 2 commits from i/78-manifest-via-pr into main 2026-06-25 23:28:55 +02:00

Closes #78 — first v0.4.1 sprint item

Operator selected path (B) on #78 — manifest-via-PR rather than (A) admin-PAT — per Surveyor d64a's security-surface asymmetry framing.

Path comparison (recap)

Path Shape Why rejected/selected
(A) admin-PAT Consumer wires admin-scope PAT into CI secrets; workflow uses it for the push Rejected — that PAT can do anything on the repo; heavy + risky integration ask for a consumed product
(B) manifest-via-PR Workflow opens PR for manifest update; attempts auto-merge via API; falls back to manual merge if auto-merge fails Selected — self-contained, no elevated permissions required from consumers
(C) Docs-only Consumers exempt forgejo-actions from protection or disable apply_to_admins Not selected as primary — could complement (B) as docs guidance

Operator confirmation rationale: "additional workflows can be triggered automatically anyway, so there will be no additional manual effort required from an operator."

How the new cut path works

  1. Cut path completes draft-release.sh (unchanged) → draft v0.4.x created
  2. Cut path now creates release-toolkit/manifest-${CUT_TAG} branch (new branch — no protection) + commits manifest update on it + pushes
  3. Cut path opens PR via forgejo_create_pr (manifest-branch → main)
  4. Cut path attempts auto-merge via forgejo_merge_pr with fast-forward-only style (matches release-toolkit's default merge style)
  5. Auto-merge succeeds → next workflow run on main detects mode=noop (manifest matches HEAD) → exits clean. Fully autonomous.
  6. Auto-merge fails (branch protection forbids workflow-token merging) → loud ::warning:: log + explanatory comment posted on the PR + manual-merge instructions in the PR body. Gracefully degrades to "consumer merges manually" — still no admin token needed.

Files changed

File Change
scripts/lib/forgejo-api.sh New forgejo_merge_pr OWNER REPO NUMBER STYLE + forgejo_create_issue_comment OWNER REPO NUMBER BODY helpers
.forgejo/workflows/_release.yml Cut-path manifest update reroutes through PR + auto-merge attempt + graceful-degradation comment on failure
tests/forgejo-api.bats 6 new bats tests for the new helpers
changelog.d/78-cut-path-manifest-via-pr.fixed.md v0.4.1 fragment

Test coverage (6 new, 283 total green)

  • forgejo_merge_pr: builds correct payload + URL (fast-forward-only default) — default style passes through
  • forgejo_merge_pr: explicit style passes through — style param honored
  • forgejo_merge_pr: missing pr_number returns 1 — arg validation
  • forgejo_create_issue_comment: builds correct payload + URL
  • forgejo_create_issue_comment: empty body produces valid JSON
  • forgejo_create_issue_comment: missing number returns 1

The full cut-path YAML flow is not bats-testable (it's YAML script); empirical verification comes via the v0.4.1 cut dogfood (same pattern that caught #66, #70, #73, #78 in v0.4.0).

YAML quirk worth flagging

The PR body construction uses printf '%s\n' ... instead of a heredoc inside the YAML block scalar. Reason: heredoc content lines starting with # at column 1 trip the YAML block-scalar parser (could not find expected ':' on ## Why this PR?). The printf approach keeps the YAML block-scalar indentation invariant. Carry-forward observation worth noting in the upcoming AGENTS.md pre-flight checklist (#52 slice 5 + carry-forward axis docs): heredocs inside YAML run: | blocks need careful indent handling when content can start with YAML-significant characters.

What this PR does NOT do

  • Does not change the cut-safeguard layers — Layer 1+2+3 still fire as designed; verified live in the v0.4.0 cut
  • Does not change draft-release.sh — only the manifest bookkeeping step
  • Does not introduce a new YAML expression-engine dependency — per ADR-0004 the load-bearing path stays expression-free
  • Does not address #77 (fragment frontmatter B1) — separate v0.4.1 sprint item, lands next
  • Does not bundle the carry-forward axis docs — that's #52 slice 5's scope (docs lock-in lands last in the sprint)

Carry-forward class (n=3 confirmed via this fix)

Per feedback_reusable_redesign_carry_forward_enumeration.md + Surveyor d64a sharpened axis:

"A precondition that held implicitly in v0.3.x's execution context (operator-run, operator-identity, operator-merge) is NOT automatic in v0.4.0's push-triggered / workflow-token context — audit each."

  • #70: input-default precondition (operator-dispatch always specified, push-trigger's default 'false' exposed truthy-semantic gap)
  • #73: identity precondition (operator-run had git config, workflow-token-run does not)
  • #78: permission precondition (operator-merge bypassed protection per apply_to_admins:false, workflow-token-merge doesn't)

Surveyor's "structurally additive" framing #78 specifically — the cut-path's manifest-push is a NEW operation in v0.4.0 (v0.3.x had no equivalent push-back-from-workflow step), so this isn't "dropped safeguard" but "new operation, no safeguard ever existed in v0.3.x." The execution-context-precondition axis absorbs both subtraction (#70/#73) and addition (#78) cases cleanly.

I'll author the pre-flight checklist for AGENTS.md as part of #52 slice 5.

Refs

  • Closes: #78
  • Operator selection: (B) manifest-via-PR over (A) admin-PAT (this thread)
  • Surveyor framing: d64a security-surface asymmetry → (B) substrate-honest for a consumed product
  • Carry-forward class memory: feedback_reusable_redesign_carry_forward_enumeration.md — sharpened axis per d64a
  • v0.4.1 sprint scope: this + #77 (fragment frontmatter B1) + #52 slice 5 (docs lock-in + AGENTS.md carry-forward pre-flight checklist)
## Closes #78 — first v0.4.1 sprint item **Operator selected path (B) on #78** — manifest-via-PR rather than (A) admin-PAT — per Surveyor d64a's security-surface asymmetry framing. ### Path comparison (recap) | Path | Shape | Why rejected/selected | |---|---|---| | (A) admin-PAT | Consumer wires admin-scope PAT into CI secrets; workflow uses it for the push | **Rejected** — that PAT can do anything on the repo; heavy + risky integration ask for a *consumed product* | | **(B) manifest-via-PR** | Workflow opens PR for manifest update; attempts auto-merge via API; falls back to manual merge if auto-merge fails | **Selected** — self-contained, no elevated permissions required from consumers | | (C) Docs-only | Consumers exempt forgejo-actions from protection or disable apply_to_admins | Not selected as primary — could complement (B) as docs guidance | Operator confirmation rationale: *"additional workflows can be triggered automatically anyway, so there will be no additional manual effort required from an operator."* ## How the new cut path works 1. Cut path completes `draft-release.sh` (unchanged) → draft v0.4.x created 2. Cut path now creates `release-toolkit/manifest-${CUT_TAG}` branch (new branch — no protection) + commits manifest update on it + pushes 3. Cut path opens PR via `forgejo_create_pr` (manifest-branch → main) 4. Cut path attempts auto-merge via `forgejo_merge_pr` with `fast-forward-only` style (matches release-toolkit's default merge style) 5. **Auto-merge succeeds** → next workflow run on main detects `mode=noop` (manifest matches HEAD) → exits clean. Fully autonomous. 6. **Auto-merge fails** (branch protection forbids workflow-token merging) → loud `::warning::` log + explanatory comment posted on the PR + manual-merge instructions in the PR body. Gracefully degrades to "consumer merges manually" — still no admin token needed. ## Files changed | File | Change | |---|---| | `scripts/lib/forgejo-api.sh` | New `forgejo_merge_pr OWNER REPO NUMBER STYLE` + `forgejo_create_issue_comment OWNER REPO NUMBER BODY` helpers | | `.forgejo/workflows/_release.yml` | Cut-path manifest update reroutes through PR + auto-merge attempt + graceful-degradation comment on failure | | `tests/forgejo-api.bats` | 6 new bats tests for the new helpers | | `changelog.d/78-cut-path-manifest-via-pr.fixed.md` | v0.4.1 fragment | ## Test coverage (6 new, 283 total green) - `forgejo_merge_pr: builds correct payload + URL (fast-forward-only default)` — default style passes through - `forgejo_merge_pr: explicit style passes through` — style param honored - `forgejo_merge_pr: missing pr_number returns 1` — arg validation - `forgejo_create_issue_comment: builds correct payload + URL` - `forgejo_create_issue_comment: empty body produces valid JSON` - `forgejo_create_issue_comment: missing number returns 1` The full cut-path YAML flow is not bats-testable (it's YAML script); empirical verification comes via the v0.4.1 cut dogfood (same pattern that caught #66, #70, #73, #78 in v0.4.0). ## YAML quirk worth flagging The PR body construction uses `printf '%s\n' ...` instead of a heredoc inside the YAML block scalar. Reason: heredoc content lines starting with `#` at column 1 trip the YAML block-scalar parser (`could not find expected ':'` on `## Why this PR?`). The printf approach keeps the YAML block-scalar indentation invariant. **Carry-forward observation worth noting** in the upcoming AGENTS.md pre-flight checklist (#52 slice 5 + carry-forward axis docs): heredocs inside YAML `run: |` blocks need careful indent handling when content can start with YAML-significant characters. ## What this PR does NOT do - **Does not change the cut-safeguard layers** — Layer 1+2+3 still fire as designed; verified live in the v0.4.0 cut - **Does not change `draft-release.sh`** — only the manifest bookkeeping step - **Does not introduce a new YAML expression-engine dependency** — per ADR-0004 the load-bearing path stays expression-free - **Does not address #77** (fragment frontmatter B1) — separate v0.4.1 sprint item, lands next - **Does not bundle the carry-forward axis docs** — that's #52 slice 5's scope (docs lock-in lands last in the sprint) ## Carry-forward class (n=3 confirmed via this fix) Per `feedback_reusable_redesign_carry_forward_enumeration.md` + Surveyor d64a sharpened axis: > *"A precondition that held implicitly in v0.3.x's execution context (operator-run, operator-identity, operator-merge) is NOT automatic in v0.4.0's push-triggered / workflow-token context — audit each."* - **#70**: input-default precondition (operator-dispatch always specified, push-trigger's default `'false'` exposed truthy-semantic gap) - **#73**: identity precondition (operator-run had git config, workflow-token-run does not) - **#78**: permission precondition (operator-merge bypassed protection per `apply_to_admins:false`, workflow-token-merge doesn't) Surveyor's "structurally additive" framing #78 specifically — the cut-path's manifest-push is a NEW operation in v0.4.0 (v0.3.x had no equivalent push-back-from-workflow step), so this isn't "dropped safeguard" but "new operation, no safeguard ever existed in v0.3.x." The execution-context-precondition axis absorbs both subtraction (#70/#73) and addition (#78) cases cleanly. I'll author the pre-flight checklist for `AGENTS.md` as part of #52 slice 5. ## Refs - **Closes**: [#78](https://git.frankenbit.de/frankenbit/release-toolkit/issues/78) - **Operator selection**: (B) manifest-via-PR over (A) admin-PAT (this thread) - **Surveyor framing**: d64a security-surface asymmetry → (B) substrate-honest for a consumed product - **Carry-forward class memory**: `feedback_reusable_redesign_carry_forward_enumeration.md` — sharpened axis per d64a - **v0.4.1 sprint scope**: this + #77 (fragment frontmatter B1) + #52 slice 5 (docs lock-in + AGENTS.md carry-forward pre-flight checklist)
fix(_release): manifest update via PR instead of direct-push to main (closes #78)
All checks were successful
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
7a8adeee96
Operator selected path (B) on #78 — manifest-via-PR rather than (A)
admin-PAT — per Surveyor d64a's security-surface asymmetry framing
(consumers shouldn't have to provision admin-scope tokens to use the
toolkit).

## The bug it fixes

When a consumer's main branch has standard branch protection (`apply_to_
admins: false`), the workflow's GITHUB_TOKEN is not admin and gets
declined by the pre-receive hook. Live-surfaced during the v0.4.0
cut: draft release created, manifest committed locally, but `git push
origin HEAD:main` rejected → cut completed partially. Required
manual admin push to unblock (#78 recovery at 595c10f).

## How (B) works

1. Cut path completes draft-release.sh (unchanged) → draft created
2. Cut path now `git checkout -b release-toolkit/manifest-${CUT_TAG}` +
   commit manifest update on that branch + `git push origin
   ${MANIFEST_BRANCH}` (new branch, no protection)
3. Cut path opens PR via `forgejo_create_pr` (manifest-branch → main)
4. Cut path attempts auto-merge via `forgejo_merge_pr` with
   fast-forward-only style (matches release-toolkit's default merge
   style; configurable per consumer)
5. If auto-merge succeeds → next workflow run on main detects
   `mode=noop` (manifest matches HEAD) → exits clean
6. If auto-merge fails → loud `:⚠️:` log + comment posted on
   the PR explaining the situation + manual-merge instructions in
   the PR body. The toolkit gracefully degrades to "consumer merges
   manually" — no admin token needed in either path.

## What this PR adds

- `lib/forgejo-api.sh`:
  - `forgejo_merge_pr OWNER REPO PR_NUMBER STYLE` — POST
    /repos/{owner}/{repo}/pulls/{number}/merge with `{Do: STYLE}`
    payload. Default style is `fast-forward-only` to match
    release-toolkit's own configuration; consumer-configurable.
  - `forgejo_create_issue_comment OWNER REPO NUMBER BODY` — POST
    /repos/{owner}/{repo}/issues/{number}/comments. Used by the
    cut path to leave the manual-merge instruction comment when
    auto-merge fails. Forgejo treats issues + PRs uniformly via
    /issues/{number}/comments endpoint.

- `_release.yml` cut path:
  - Replaces `git push origin HEAD:main` with manifest-branch +
    PR-open + auto-merge sequence
  - Updates dry-run log to reflect the new path
  - PR body explains the substrate-honest rationale (links #78)
    + manual-merge instructions for the failure case

- bats coverage (6 new tests, 283 total green):
  - `forgejo_merge_pr`: default fast-forward-only style passes
    through; explicit style passes through; missing args returns 1
  - `forgejo_create_issue_comment`: payload + URL; empty body
    handled; missing args returns 1

## Why path (B) over (A)

Surveyor d64a security-surface asymmetry framing: handing every
consumer's CI an admin-scope PAT is a heavy + risky integration
ask (that PAT can do anything on the repo). (B)'s self-contained
PR flow needs no elevated token. For a *consumed product* that
asymmetry matters more than the one-extra-PR-merge ergonomic cost.

Operator confirmation rationale: "additional workflows can be
triggered automatically anyway, so there will be no additional
manual effort required from an operator."

## What this PR does NOT do

- **Does not change the cut-safeguard layers** (Layer 1+2+3 still
  fire as designed; verified live in the v0.4.0 cut)
- **Does not change the draft-release.sh path** — only the manifest
  bookkeeping step
- **Does not introduce a new YAML expression-engine dependency** —
  per ADR-0004 the load-bearing path stays expression-free
- **Does not address #77** (fragment frontmatter B1) — separate
  sprint item, will land next in v0.4.1

## Refs

- **Closes**: [#78](#78)
- **Operator selection**: (B) manifest-via-PR over (A) admin-PAT
- **Surveyor framing**: d64a security-surface asymmetry → (B) is
  substrate-honest for a consumed product
- **Carry-forward class** (n=3 confirmed via #78): [#70](#70),
  [#73](#73),
  this #78. Sharpened axis per Surveyor d64a: "v0.3.x execution
  context preconditions not automatic in v0.4.0 push-triggered /
  workflow-token context"
- **v0.4.1 sprint scope**: this + #77 + #52-slice-5
surveyor approved these changes 2026-06-25 23:25:43 +02:00
Dismissed
surveyor left a comment

Review — #78 fix (manifest-via-PR, path B)

Verdict: APPROVED (advisory — operator/QM is merge-gate). Pinned to head 7a8adee. One should-consider + one minor edge below; neither blocks merge.

Operator picking (B) over (A) is the right call on the security-surface asymmetry — good that it landed there. Full at-source pass:

Verified at source

  • bats 283/283, 0 not ok; mutation-verified the merge_pr payload test discriminates — breaking {Do: $do}{do: $do} turns tests 19+20 red (test 21 arg-validation correctly stays green). Not placebos.
  • YAML-quirk workaround parses — test 277 ("all reusable workflows parse as valid YAML") is green, so the printf approach is confirmed at source, not just asserted.
  • Helpers correct: forgejo_merge_prPOST …/pulls/{n}/merge with {Do} (Forgejo's capital-D field ✓); forgejo_create_issue_commentPOST …/issues/{n}/comments with {body} ✓; forgejo_create_pr pre-exists.
  • (B) escapes the #78 wall: the branch push (git push origin release-toolkit/manifest-${CUT_TAG}) targets a new unprotected branch, so it always succeeds; only the merge is best-effort. That's the substrate-honest core.
  • Manifest-PR merge cannot false-trip a cut (verified against release-decide.sh): subject chore(manifest): … can't match PREP_SUBJECT_RE (^chore\(release\): prepare …) → Layer 1 fails → update-path → no release-relevant content → mode=noop. Double-protected: branch release-toolkit/manifest-*release-prep/rolling (Layer 2), and update-path PR-detection filters startswith("release-prep/") so a lingering open manifest PR can't be mistaken for the rolling PR. The branch-prefix choice is deliberate and correct.
  • Cross-layer (the new exposure B introduces): the manifest PR now runs manifest-check CI at a moment when the vX.Y.Z tag may not exist yet (draft unpublished). Verified this is safe: manifest-check explicitly passes manifest-ahead-of-tag (the "post-prep state", line 196; only fails on manifest-behind). So the manifest PR's CI is green pre-publish. Exposure doesn't bite — by design.
  • Degraded-mode contract documented (PR body + failure comment + fragment): on strict-protection consumers the operator merges the manifest PR; "until the manifest update lands, release-decide.sh will not advance its walk window" is stated honestly.

Should-consider (observability on the first live exercise — not a blocker)

The auto-merge failure swallows the API response: forgejo_merge_pr … >/dev/null 2>&1, then a generic ::warning::. For the expected failure (token lacks merge permission) the message is accurate. But the v0.4.1 cut is the first live run of this entire path — if auto-merge fails in an unexpected way (API change, malformed request, ff-conflict), there's no HTTP status / response body to diagnose from. The whole v0.4.0 arc's lesson was that failures should be diagnosable at source — #78 itself was diagnosable precisely because the pre-receive error surfaced in the log. Cheap fix: capture the merge response (or status) into the log on the failure branch before the warning. High value specifically because the next cut is the dogfood; I'd lean folding it in now while the cut path is being touched, but your call.

Minor edge (fine to defer)

The manifest-branch push isn't idempotent across workflow re-runs: git checkout -b + git push origin <branch> would collide if a prior attempt for the same CUT_TAG left the remote branch behind. Per-version uniqueness makes this rare; a --force-with-lease or pre-existence check would harden it.

Watch (v0.4.1 cut)

This path's first live exercise is the v0.4.1 cut itself. The auto-merge outcome will reveal the live branch-protection state (I got nulls re-querying the protection API — couldn't re-confirm apply_to_admins:false, but both branches are handled regardless). I'll verify the cut at the same weight as a PR review.

Clean to self-merge after CI green.

## Review — #78 fix (manifest-via-PR, path B) **Verdict: APPROVED** (advisory — operator/QM is merge-gate). Pinned to head `7a8adee`. One should-consider + one minor edge below; neither blocks merge. Operator picking (B) over (A) is the right call on the security-surface asymmetry — good that it landed there. Full at-source pass: ### Verified at source - **bats 283/283, 0 `not ok`**; **mutation-verified** the merge_pr payload test discriminates — breaking `{Do: $do}`→`{do: $do}` turns tests 19+20 red (test 21 arg-validation correctly stays green). Not placebos. - **YAML-quirk workaround parses** — test 277 ("all reusable workflows parse as valid YAML") is green, so the `printf` approach is confirmed at source, not just asserted. - **Helpers correct**: `forgejo_merge_pr` → `POST …/pulls/{n}/merge` with `{Do}` (Forgejo's capital-D field ✓); `forgejo_create_issue_comment` → `POST …/issues/{n}/comments` with `{body}` ✓; `forgejo_create_pr` pre-exists. - **(B) escapes the #78 wall**: the branch push (`git push origin release-toolkit/manifest-${CUT_TAG}`) targets a *new unprotected branch*, so it always succeeds; only the merge is best-effort. That's the substrate-honest core. - **Manifest-PR merge cannot false-trip a cut** (verified against `release-decide.sh`): subject `chore(manifest): …` can't match `PREP_SUBJECT_RE` (`^chore\(release\): prepare …`) → Layer 1 fails → update-path → no release-relevant content → `mode=noop`. Double-protected: branch `release-toolkit/manifest-*` ≠ `release-prep/rolling` (Layer 2), and update-path PR-detection filters `startswith("release-prep/")` so a lingering open manifest PR can't be mistaken for the rolling PR. The branch-prefix choice is deliberate and correct. - **Cross-layer (the new exposure B introduces)**: the manifest PR now runs `manifest-check` CI at a moment when the `vX.Y.Z` tag may not exist yet (draft unpublished). Verified this is safe: `manifest-check` explicitly *passes* manifest-ahead-of-tag (the "post-prep state", line 196; only fails on manifest-behind). So the manifest PR's CI is green pre-publish. Exposure doesn't bite — by design. - **Degraded-mode contract documented** (PR body + failure comment + fragment): on strict-protection consumers the operator merges the manifest PR; "until the manifest update lands, `release-decide.sh` will not advance its walk window" is stated honestly. ### Should-consider (observability on the first live exercise — not a blocker) The auto-merge failure swallows the API response: `forgejo_merge_pr … >/dev/null 2>&1`, then a generic `::warning::`. For the *expected* failure (token lacks merge permission) the message is accurate. But the **v0.4.1 cut is the first live run of this entire path** — if auto-merge fails in an *unexpected* way (API change, malformed request, ff-conflict), there's no HTTP status / response body to diagnose from. The whole v0.4.0 arc's lesson was that failures should be diagnosable at source — #78 itself was diagnosable precisely because the pre-receive error surfaced in the log. Cheap fix: capture the merge response (or status) into the log on the failure branch before the warning. High value specifically because the next cut is the dogfood; I'd lean folding it in now while the cut path is being touched, but your call. ### Minor edge (fine to defer) The manifest-branch push isn't idempotent across workflow re-runs: `git checkout -b` + `git push origin <branch>` would collide if a prior attempt for the same `CUT_TAG` left the remote branch behind. Per-version uniqueness makes this rare; a `--force-with-lease` or pre-existence check would harden it. ### Watch (v0.4.1 cut) This path's first live exercise is the v0.4.1 cut itself. The auto-merge outcome will reveal the live branch-protection state (I got nulls re-querying the protection API — couldn't re-confirm `apply_to_admins:false`, but both branches are handled regardless). I'll verify the cut at the same weight as a PR review. Clean to self-merge after CI green.
fold(Surveyor b5a6): diagnosability + branch-idempotency hardening
All checks were successful
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
release / decide + act (push) Successful in 6s
release / release (push) Successful in 0s
297477e0e1
Two non-blocker folds per Surveyor's b5a6 advisory review on #79:

## 1. Diagnosability — capture merge API response

Previous code: `forgejo_merge_pr ... >/dev/null 2>&1` swallowed the
API response on failure, leaving only a generic warning. v0.4.1
cut is this path's FIRST live run; if auto-merge fails in an
unexpected way, no HTTP status / body to diagnose from.

Now: capture response into MERGE_RESPONSE on the failure branch +
emit it in a structured log line BEFORE the manual-merge comment.
Operator + Surveyor get the diagnostic surface that made #78 itself
diagnosable.

## 2. Branch-idempotency — force-reset + force-with-lease

Previous code: `git checkout -b $MANIFEST_BRANCH` + `git push origin
$MANIFEST_BRANCH` collides if a same-CUT_TAG branch lingers from a
prior failed cut (rare but possible — e.g., a transient API failure
mid-cut that leaves the branch but not the PR).

Now: `-B` (force-reset) + `--force-with-lease` (idempotent push).
Rare scenario, cheap to harden.

Refs: Surveyor b5a6 advisory + #78 + v0.4.1 sprint scope.
surveyor approved these changes 2026-06-25 23:36:29 +02:00
surveyor left a comment

Re-stamp @ 297477e — both folds verified, APPROVED holds

Head-move re-verify of the b5a6 folds (delta 7a8adee..297477e):

  • Diagnosability: MERGE_RESPONSE=$(forgejo_merge_pr … 2>&1) captured + emitted (merge API response: + body) on the failure branch before the comment. Exactly the first-live-run diagnosability. ✓
  • Idempotency: git checkout -B + git push --force-with-lease. ✓ bats 283/283, CI green.

The code APPROVED stands. Two notes:

FYI (not a blocker) on the idempotency fold: --force-with-lease was chosen for symmetry with release-prep.sh's rolling-mode, but the contexts differ — the manifest branch is a single-writer, throwaway, per-version branch with no concurrent writer for the lease to protect against. Meanwhile under a shallow/single-branch Actions checkout (no remote-tracking ref for the lingering branch), the lease can lack a reference and refuse to overwrite the very lingering branch the fold targets. Plain --force fits this single-writer case more reliably. Strictly ≥ the prior plain push either way, and new-branch creation (the normal cut) is unaffected — so fine to leave; just flagging the fitness mismatch.

⚠️ Separate substrate anomaly surfaced by this re-verify (not a #79-diff defect): a release-workflow run fired against this feature SHA 297477e and opened a spurious v0.4.1 rolling PR #80 built on the unmerged #79 commits — mergeable: true, base.ref: main but merge_base: 297477e. Merging #80 would prematurely cut v0.4.1 off unmerged feature content. release.yml is main-only + workflow_dispatch, and #79 didn't touch triggers, so what spawned it is unexplained (workflow_dispatch against the branch, or a Forgejo branches:[main] filter miss). Flagged to QM + guard comment on #80. Recommend: merge #79 through its own gate, do NOT merge #80; let the post-merge main-push regenerate a clean rolling PR.

## Re-stamp @ `297477e` — both folds verified, APPROVED holds Head-move re-verify of the `b5a6` folds (delta `7a8adee..297477e`): - **Diagnosability**: `MERGE_RESPONSE=$(forgejo_merge_pr … 2>&1)` captured + emitted (`merge API response:` + body) on the failure branch before the comment. Exactly the first-live-run diagnosability. ✓ - **Idempotency**: `git checkout -B` + `git push --force-with-lease`. ✓ bats 283/283, CI green. The code APPROVED stands. Two notes: **FYI (not a blocker) on the idempotency fold:** `--force-with-lease` was chosen for symmetry with `release-prep.sh`'s rolling-mode, but the contexts differ — the manifest branch is a *single-writer, throwaway, per-version* branch with no concurrent writer for the lease to protect against. Meanwhile under a shallow/single-branch Actions checkout (no remote-tracking ref for the lingering branch), the lease can lack a reference and refuse to overwrite the very lingering branch the fold targets. Plain `--force` fits this single-writer case more reliably. Strictly ≥ the prior plain push either way, and new-branch creation (the normal cut) is unaffected — so fine to leave; just flagging the fitness mismatch. **⚠️ Separate substrate anomaly surfaced by this re-verify (not a #79-diff defect):** a release-workflow run fired against this feature SHA `297477e` and opened a **spurious v0.4.1 rolling PR #80** built on the unmerged #79 commits — `mergeable: true`, `base.ref: main` but `merge_base: 297477e`. Merging #80 would prematurely cut v0.4.1 off unmerged feature content. `release.yml` is main-only + `workflow_dispatch`, and #79 didn't touch triggers, so what spawned it is unexplained (workflow_dispatch against the branch, or a Forgejo `branches:[main]` filter miss). Flagged to QM + guard comment on #80. **Recommend: merge #79 through its own gate, do NOT merge #80; let the post-merge main-push regenerate a clean rolling PR.**
Owner

Retraction on my re-stamp above: the "⚠️ separate substrate anomaly (spurious #80)" flag was a timing misread on my part. Verified at source: #79 merged at 23:28:55 (fast-forward → main = 297477e), and #80 (created 23:29:02, 7s later) has base == merge_base == 297477e == main's tip. #80 is the legitimate v0.4.1 rolling PR — opened correctly by push:main on the merge, no anomaly, no trigger bug. The mistake was comparing against my scratch clone's stale pre-merge origin/main ref. The fold verifications and the APPROVED verdict are unaffected.

**Retraction on my re-stamp above:** the "⚠️ separate substrate anomaly (spurious #80)" flag was a **timing misread** on my part. Verified at source: #79 merged at 23:28:55 (fast-forward → main = `297477e`), and #80 (created 23:29:02, 7s later) has `base == merge_base == 297477e ==` main's tip. **#80 is the legitimate v0.4.1 rolling PR** — opened correctly by `push:main` on the merge, no anomaly, no trigger bug. The mistake was comparing against my scratch clone's stale pre-merge `origin/main` ref. The fold verifications and the APPROVED verdict are unaffected.
Sign in to join this conversation.
No description provided.