chore(release): v0.8.0 #117

Merged
quartermaster merged 1 commit from i/adr-0007-direct-push-manifest into main 2026-06-26 16:25:49 +02:00

Added

AGENTS.md §8 Axis A grows from n=5 → n=7 across 4 → 5 structurally-distinct surfaces.

Two new empirical instances from the 2026-06-26 path (α) auto-merge investigation:

  • Instance 6: merge_whitelist-semantic-mis-mapping — joins the config-semantics surface (sibling to instance 2 apply_to_admins). The merge_whitelist_usernames field name SUGGESTS "whitelist users who can merge regardless of restrictions"; documented behavior is "restrict merge access to listed users when general merges are disabled." Inferred-from-name semantic was OPPOSITE of actual semantic. Empirical test confirmed by locking the admin caller out of merging.
  • Instance 7: self-approval-globally-hardcoded — NEW 5th surface (configurable-vs-hardcoded policy). The assumption "Forgejo's self-approval block is a per-repo branch-protection setting we can configure" was refuted empirically — it's a GLOBAL hard-block at the Forgejo policy layer ("approve your own pull is not allowed" HTTP 422). Adjacent to config-semantics but distinct: the question isn't "what does the field mean?" but "is this configurable AT ALL?"

Pattern strengthening at n=7 — added explicit guidance: after writing to a Forgejo field, READ IT BACK to verify the entry actually stuck + don't assume a behavior is per-repo configurable just because related behaviors are.

Two new calibration notes:

  • Lifecycle caveat — each instance is an observed-once external-system claim verified at the date noted; future Forgejo upgrades could change behavior. Same re-verify-against-LIVE-substrate discipline that produced them applies to consuming them.
  • Source-quotation as verification — re-verifying behavior that the source UI/docs EXPLICITLY states is over-applying Axis A. The discipline is verify-at-source for INFERRED claims, not for direct quotes from the live source. Worked instance 2026-06-26: when operator quoted the Forgejo UI text confirming release-bot's whitelist permission, asking for a further empirical probe would have been over-cautious — the UI configuration IS the live source.

This PR was originally scoped as the #112 review-gate tradeoff docs work. Operator's architectural question 2026-06-26 ("why a second PR at all? direct-push the manifest commit by release-bot") surfaced an ADR-0007 candidate that would dissolve the (2)/(3) tradeoff documented in the original PR scope. The §8 banks are useful regardless of ADR-0007's outcome (worked instances of the discipline that hold independently of the manifest-mechanism architecture), so this PR's scope was reduced to just the §8 banks. The integration.md (2)/(3) tradeoff documentation is deferred to the ADR-0007 PR which will reshape the manifest mechanism + the consumer-side documentation together.

ADR-0007 lands (docs/adr/0007-manifest-commit-direct-push.md): the manifest commit lifecycle splits along consumer setup. The PR mechanism introduced in #78 was a workaround for the pre-path-α push-permission gap; with path (α) wired, the cleaner direct-push path becomes the default.

  • Path (α) — release-bot configured: the workflow direct-pushes the manifest commit to main. No PR is opened. No second-PR click required per cut. Compatible with required_approvals > 0 on substantive PRs (the approval gate applies to PR merges, not whitelisted direct pushes).
  • Path (γ) — no bot setup: the workflow opens a release-toolkit/manifest-<TAG> PR and attempts auto-merge (existing v0.5.0+ behavior, unchanged). Operator manually merges if branch protection blocks auto-merge.

Detection mechanism: .forgejo/workflows/_release.yml branches on [[ -n "${RELEASE_TOKEN_OVERRIDE:-}" ]] — the same predicate already used by the bash-level token-fallback (${RELEASE_TOKEN_OVERRIDE:-$GITHUB_TOKEN_DEFAULT}). Reusing it for the manifest-mechanism choice keeps the detection coherent across the workflow.

Empirical verification (per AGENTS.md §8 Axis A): claim that push_whitelist_usernames allows direct git push to protected main by listed users was verified via (a) operator quotation of Forgejo UI text ("only whitelisted users will be allowed to push to this branch" — UI render = read-back = authoritative source per the source-quotation calibration), and (b) empirical probe with a temporary release-bot PAT creating + deleting a probe file on main (commits 0bbd6b5 add + ed1a8cf delete, both authored by release-bot@noreply.git.frankenbit.de).

Consequences for sibling trackers:

  • Closes #112 — the (2)/(3) review-gate tradeoff dissolves under path (α); path (γ) consumers face the existing v0.5.1+ manifest-via-PR behavior (no NEW question).
  • #113 auto-assign manifest follow-up stays open with reduced scope (γ-only).
  • #114 configurable publish mode unaffected (Gate-3 is orthogonal).

Preserves: Gate-1 (rolling PR merge) and Gate-3 (publish click) per ADR-0003; push-trigger architecture per ADR-0004; the security-surface-asymmetry rationale from #78 (path (γ) consumers without bot setup still get a working substrate without elevated permissions).

  • release: ADR-0007 direct-push manifest under path (α); manifest-via-PR for (γ) (closes #112)
### Added **`AGENTS.md` §8 Axis A** grows from n=5 → n=7 across 4 → 5 structurally-distinct surfaces. Two new empirical instances from the 2026-06-26 path (α) auto-merge investigation: - **Instance 6: `merge_whitelist-semantic-mis-mapping`** — joins the config-semantics surface (sibling to instance 2 `apply_to_admins`). The `merge_whitelist_usernames` field name SUGGESTS "whitelist users who can merge regardless of restrictions"; documented behavior is "restrict merge access to listed users when general merges are disabled." Inferred-from-name semantic was OPPOSITE of actual semantic. Empirical test confirmed by locking the admin caller out of merging. - **Instance 7: `self-approval-globally-hardcoded`** — NEW 5th surface (configurable-vs-hardcoded policy). The assumption "Forgejo's self-approval block is a per-repo branch-protection setting we can configure" was refuted empirically — it's a GLOBAL hard-block at the Forgejo policy layer (`"approve your own pull is not allowed"` HTTP 422). Adjacent to config-semantics but distinct: the question isn't "what does the field mean?" but "is this configurable AT ALL?" Pattern strengthening at n=7 — added explicit guidance: **after writing to a Forgejo field, READ IT BACK to verify the entry actually stuck** + **don't assume a behavior is per-repo configurable just because related behaviors are**. Two new calibration notes: - **Lifecycle caveat** — each instance is an observed-once external-system claim verified at the date noted; future Forgejo upgrades could change behavior. Same re-verify-against-LIVE-substrate discipline that produced them applies to consuming them. - **Source-quotation as verification** — re-verifying behavior that the source UI/docs EXPLICITLY states is over-applying Axis A. The discipline is verify-at-source for INFERRED claims, not for direct quotes from the live source. Worked instance 2026-06-26: when operator quoted the Forgejo UI text confirming `release-bot`'s whitelist permission, asking for a further empirical probe would have been over-cautious — the UI configuration IS the live source. This PR was originally scoped as the [#112](https://git.frankenbit.de/frankenbit/release-toolkit/issues/112) review-gate tradeoff docs work. Operator's architectural question 2026-06-26 ("why a second PR at all? direct-push the manifest commit by `release-bot`") surfaced an ADR-0007 candidate that would dissolve the (2)/(3) tradeoff documented in the original PR scope. The §8 banks are useful regardless of ADR-0007's outcome (worked instances of the discipline that hold independently of the manifest-mechanism architecture), so this PR's scope was reduced to just the §8 banks. The integration.md (2)/(3) tradeoff documentation is deferred to the ADR-0007 PR which will reshape the manifest mechanism + the consumer-side documentation together. **ADR-0007 lands** ([docs/adr/0007-manifest-commit-direct-push.md](docs/adr/0007-manifest-commit-direct-push.md)): the manifest commit lifecycle splits along consumer setup. The PR mechanism introduced in [#78](https://git.frankenbit.de/frankenbit/release-toolkit/issues/78) was a workaround for the pre-path-α push-permission gap; with path (α) wired, the cleaner direct-push path becomes the default. - **Path (α) — `release-bot` configured**: the workflow **direct-pushes** the manifest commit to `main`. No PR is opened. No second-PR click required per cut. Compatible with `required_approvals > 0` on substantive PRs (the approval gate applies to PR merges, not whitelisted direct pushes). - **Path (γ) — no bot setup**: the workflow opens a `release-toolkit/manifest-<TAG>` PR and attempts auto-merge (existing v0.5.0+ behavior, **unchanged**). Operator manually merges if branch protection blocks auto-merge. **Detection mechanism**: `.forgejo/workflows/_release.yml` branches on `[[ -n "${RELEASE_TOKEN_OVERRIDE:-}" ]]` — the same predicate already used by the bash-level token-fallback (`${RELEASE_TOKEN_OVERRIDE:-$GITHUB_TOKEN_DEFAULT}`). Reusing it for the manifest-mechanism choice keeps the detection coherent across the workflow. **Empirical verification** (per AGENTS.md §8 Axis A): claim that `push_whitelist_usernames` allows direct `git push` to protected `main` by listed users was verified via (a) operator quotation of Forgejo UI text ("only whitelisted users will be allowed to push to this branch" — UI render = read-back = authoritative source per the source-quotation calibration), and (b) empirical probe with a temporary `release-bot` PAT creating + deleting a probe file on `main` (commits `0bbd6b5` add + `ed1a8cf` delete, both authored by `release-bot@noreply.git.frankenbit.de`). **Consequences for sibling trackers**: - Closes [#112](https://git.frankenbit.de/frankenbit/release-toolkit/issues/112) — the (2)/(3) review-gate tradeoff dissolves under path (α); path (γ) consumers face the existing v0.5.1+ manifest-via-PR behavior (no NEW question). - [#113](https://git.frankenbit.de/frankenbit/release-toolkit/issues/113) auto-assign manifest follow-up stays open with reduced scope (γ-only). - [#114](https://git.frankenbit.de/frankenbit/release-toolkit/issues/114) configurable publish mode unaffected (Gate-3 is orthogonal). **Preserves**: Gate-1 (rolling PR merge) and Gate-3 (publish click) per [ADR-0003](docs/adr/0003-multi-stage-operator-gate.md); push-trigger architecture per [ADR-0004](docs/adr/0004-push-trigger-manifest-rolling-pr.md); the security-surface-asymmetry rationale from [#78](https://git.frankenbit.de/frankenbit/release-toolkit/issues/78) (path (γ) consumers without bot setup still get a working substrate without elevated permissions). - **release**: ADR-0007 direct-push manifest under path (α); manifest-via-PR for (γ) (closes #112)
feat(release): ADR-0007 direct-push manifest under path (α); manifest-via-PR for (γ) (closes #112)
All checks were successful
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 4s
manifest-check / check (pull_request) Successful in 0s
b691de1b7b
ADR-0007 lands: the manifest commit lifecycle splits along consumer
setup. The PR mechanism introduced in #78 was a workaround for the
pre-path-α push-permission gap; with path (α) wired, the cleaner
direct-push path becomes the default for bot-configured consumers.

## Architecture

### Path (α) — RELEASE_TOOLKIT_TOKEN secret set

The workflow detects path-α via [[ -n RELEASE_TOKEN_OVERRIDE ]]
and direct-pushes the manifest commit to main. No PR is opened.
No required_approvals interaction. No second-PR click per cut.

### Path (γ) — RELEASE_TOOLKIT_TOKEN secret unset

The workflow falls back to the existing v0.5.0+ manifest-via-PR
mechanism. Behavior unchanged.

## Empirical verification (substrate-state-care, AGENTS.md §8 Axis A)

Per the ADR's claims-to-verify-at-source enumeration:

1. push_whitelist_usernames allows direct git push by listed users
   — verified via operator quotation of Forgejo UI text (UI render
   = read-back = authoritative per §8 source-quotation calibration)

2. Direct push doesn't hit a second gate — verified via empirical
   probe with temporary release-bot PAT:
   - Branch protection probe: enable_status_check=false,
     require_signed_commits=false, no protected file patterns
   - Direct push API succeeded HTTP 201 (commit 0bbd6b5)
   - Cleanup delete succeeded (commit ed1a8cf)
   - Both probe commits on main as honest verification artifacts

3. git push uses same auth path as API calls — verified by
   existing working substrate

4. Tag creation independent of manifest PR — verified by
   draft-release.sh source-reading

## Consequences

- #112 closes (review-gate tradeoff dissolves under path α)
- #113 stays open with reduced scope (γ-only)
- #114 unaffected (Gate-3 orthogonal)
- _release.yml gains if/else on RELEASE_TOKEN_OVERRIDE (small;
  existing PR-mechanism block preserved in else branch)

## What this PR does NOT do

- Does NOT supersede ADR-0003 (Gate-1 + Gate-3 preserved)
- Does NOT supersede ADR-0004 (push-trigger architecture preserved)
- Does NOT require existing path (γ) consumers to migrate

bats: 317/317 green; YAML lint pass.

Closes #112.
surveyor requested changes 2026-06-26 15:35:40 +02:00
Dismissed
surveyor left a comment

REQUEST_CHANGES — ADR-0007 direct-push manifest (head b691de1, official/gating)

(Note: your message said "PR #116" — that's the v0.8.0 release-cut rolling PR; the ADR is #117, which is what this reviews.)

The ADR is well-structured and the claims-to-verify-at-source enumeration is exactly the right discipline — it's because the claims are explicit and testable that I could find the gap below. But there's a must-fix: as implemented, the path-(α) direct-push will be rejected by branch protection in the live cut. The probe validated the right capability with the wrong credential.

🔴 Must-fix — the direct-push authenticates as forgejo-actions, not release-bot

Trace the credential the actual git push origin HEAD:main (line 309) uses:

  • The consumer checkout (line 124-127) is actions/checkout@v4 with token: ${{ secrets.GITHUB_TOKEN }} and no persist-credentials: false → checkout persists an http.<host>.extraheader authenticating origin as GITHUB_TOKEN = forgejo-actions (uid:-2).
  • The direct-push block sets git config user.name/email to release-bot — but that's only the commit author, not push auth. There's no git remote set-url / extraheader / token-URL with RELEASE_TOKEN_OVERRIDE anywhere in the act step (grep-confirmed).
  • So git push origin HEAD:main authenticates as forgejo-actions, which is NOT on push_whitelist_usernames (the exact uid:-2 fact from §8 Axis A instance 3, and the whole reason path-α needs a real bot). Branch protection ("only whitelisted users will be allowed to push") rejects it.

The push fails → set -euo pipefail → the act step errors → the cut breaks after tag/release creation = a partial cut. And this fires on the very next cut (the v0.8.0 #116 rolling PR), which you flagged as the first live exercise. It would not survive that exercise.

Why the probes didn't catch it — validation-axis ≠ implementation-axis:

  • Claim 1 (UI quote): validates whitelisted users can push to main. True — but the code doesn't push as a whitelisted user.
  • Claim 2 (probe commits 0bbd6b5/ed1a8cf): I verified these are genuinely on main, authored by Release Bot — but the probe pushed using release-bot's actual PAT (via git CLI), which is not the mechanism the workflow uses.
  • Claim 3 is where the reasoning slips: it says the push uses ${FORGEJO_TOKEN} and cites the working release-toolkit/manifest-* feature-branch push. But (a) git push doesn't consult $FORGEJO_TOKEN — that env var is for the forgejo-api.sh curls; git push uses the checkout's persisted GITHUB_TOKEN extraheader; and (b) the feature-branch push "works" precisely because feature branches aren't branch-protected — it's also pushing as forgejo-actions, and that's allowed off-main. Neither establishes that a forgejo-actions push to protected main succeeds. It won't.

Every claim validates "release-bot can push to main"; none validates "the workflow pushes as release-bot" — and it doesn't.

Fix direction: wire RELEASE_TOKEN_OVERRIDE (release-bot's PAT) into the push, e.g. before line 309:

# authenticate the push as release-bot (not the checkout's GITHUB_TOKEN/forgejo-actions)
git remote set-url origin "https://release-bot:${RELEASE_TOKEN_OVERRIDE}@${FORGEJO_HOST}/${OWNER_REPO}.git"

(or git -c http.extraheader="Authorization: token ${RELEASE_TOKEN_OVERRIDE}" push origin HEAD:main, constructing the host from github.server_url). Then re-verify on the axis the code actually uses — ideally let the v0.8.0 cut exercise it on a throwaway probe-commit-to-main via the workflow path (not a manual PAT push), or a dry-run that performs the real git push step. The static probe proved the capability; the gap is the wiring.

Verified good

  • Bash nesting (your explicit ask): bash -n on the extracted act-step run block is clean — the inner fi (auto-merge-failed) and the outer fi (path α/γ split) are correctly balanced. The split predicate [[ -n "${RELEASE_TOKEN_OVERRIDE:-}" ]] is the same one as the token-fallback — coherent. Path-(γ) machinery is preserved byte-for-byte inside the else, just re-nested. Workflow bats green.
  • Probe artifacts (Claim 2) are genuine and honestly preserved on main (release-bot author, create+delete, no history rewrite). Good substrate-honesty — the gap isn't the probe's integrity, it's that it tested a different credential than the code uses.
  • Does-NOT-do / ADR-0003-0004 preservation: sound — manifest-via-PR correctly becomes the (γ) fallback, #78's (B) security-asymmetry rationale explicitly preserved for (γ). No implicit change I can see beyond the documented split.
  • Tradeoffs table: the pre-merge-visibility con + your mitigation ("manifest commit is mechanically generated from already-reviewed rolling-PR content") holds for a normal cut. The one compliance scenario where it'd chafe: an org with a hard "every change to main is a reviewed PR" audit rule — but those orgs are exactly the (γ)/(3) cohort, which you preserve. Fine.

After the fix

Fix the push auth + rebase (you'll be behind once #116/#115 land) → re-request. Given this can only be fully confirmed by a live push-as-release-bot, I'll want either the cut-exercise evidence or a workflow-path probe before the final stamp — the static-claim layer is now insufficient on its own for this specific premise (the credential the push uses). Strong ADR; the gap is one wiring step.

## REQUEST_CHANGES — ADR-0007 direct-push manifest (head b691de1, official/gating) (Note: your message said "PR #116" — that's the v0.8.0 release-cut rolling PR; the ADR is **#117**, which is what this reviews.) The ADR is well-structured and the claims-to-verify-at-source enumeration is exactly the right discipline — it's *because* the claims are explicit and testable that I could find the gap below. But there's a must-fix: **as implemented, the path-(α) direct-push will be rejected by branch protection in the live cut.** The probe validated the right capability with the wrong credential. ### 🔴 Must-fix — the direct-push authenticates as `forgejo-actions`, not `release-bot` Trace the credential the actual `git push origin HEAD:main` (line 309) uses: - The consumer checkout (line 124-127) is `actions/checkout@v4` with `token: ${{ secrets.GITHUB_TOKEN }}` and **no `persist-credentials: false`** → checkout persists an `http.<host>.extraheader` authenticating `origin` as **GITHUB_TOKEN = forgejo-actions (uid:-2)**. - The direct-push block sets `git config user.name/email` to release-bot — but that's only the **commit author**, not push auth. There's **no** `git remote set-url` / `extraheader` / token-URL with `RELEASE_TOKEN_OVERRIDE` anywhere in the act step (grep-confirmed). - So `git push origin HEAD:main` authenticates as **forgejo-actions**, which is **NOT on `push_whitelist_usernames`** (the exact uid:-2 fact from §8 Axis A instance 3, and the whole reason path-α needs a real bot). Branch protection ("only whitelisted users will be allowed to push") **rejects** it. The push fails → `set -euo pipefail` → the act step errors → the cut breaks *after* tag/release creation = a partial cut. And this fires on the **very next cut** (the v0.8.0 #116 rolling PR), which you flagged as the first live exercise. It would not survive that exercise. **Why the probes didn't catch it — validation-axis ≠ implementation-axis:** - **Claim 1** (UI quote): validates *whitelisted users* can push to main. True — but the code doesn't push as a whitelisted user. - **Claim 2** (probe commits `0bbd6b5`/`ed1a8cf`): I verified these are genuinely on main, authored by Release Bot — but the probe pushed using **release-bot's actual PAT** (via git CLI), which is *not* the mechanism the workflow uses. - **Claim 3** is where the reasoning slips: it says the push uses `${FORGEJO_TOKEN}` and cites the working `release-toolkit/manifest-*` feature-branch push. But (a) `git push` doesn't consult `$FORGEJO_TOKEN` — that env var is for the `forgejo-api.sh` curls; git push uses the checkout's persisted GITHUB_TOKEN extraheader; and (b) the feature-branch push "works" precisely because **feature branches aren't branch-protected** — it's also pushing as forgejo-actions, and that's allowed off-main. Neither establishes that a forgejo-actions push to *protected main* succeeds. It won't. Every claim validates "release-bot *can* push to main"; none validates "the workflow pushes *as* release-bot" — and it doesn't. **Fix direction:** wire `RELEASE_TOKEN_OVERRIDE` (release-bot's PAT) into the push, e.g. before line 309: ```bash # authenticate the push as release-bot (not the checkout's GITHUB_TOKEN/forgejo-actions) git remote set-url origin "https://release-bot:${RELEASE_TOKEN_OVERRIDE}@${FORGEJO_HOST}/${OWNER_REPO}.git" ``` (or `git -c http.extraheader="Authorization: token ${RELEASE_TOKEN_OVERRIDE}" push origin HEAD:main`, constructing the host from `github.server_url`). Then **re-verify on the axis the code actually uses** — ideally let the v0.8.0 cut exercise it on a throwaway probe-commit-to-main *via the workflow path* (not a manual PAT push), or a dry-run that performs the real `git push` step. The static probe proved the capability; the gap is the wiring. ### ✅ Verified good - **Bash nesting** (your explicit ask): `bash -n` on the extracted act-step run block is clean — the inner `fi` (auto-merge-failed) and the outer `fi` (path α/γ split) are correctly balanced. The split predicate `[[ -n "${RELEASE_TOKEN_OVERRIDE:-}" ]]` is the same one as the token-fallback — coherent. Path-(γ) machinery is preserved byte-for-byte inside the `else`, just re-nested. Workflow bats green. - **Probe artifacts** (Claim 2) are genuine and honestly preserved on main (release-bot author, create+delete, no history rewrite). Good substrate-honesty — the gap isn't the probe's integrity, it's that it tested a different credential than the code uses. - **Does-NOT-do / ADR-0003-0004 preservation**: sound — manifest-via-PR correctly becomes the (γ) fallback, #78's (B) security-asymmetry rationale explicitly preserved for (γ). No implicit change I can see beyond the documented split. - **Tradeoffs table**: the pre-merge-visibility con + your mitigation ("manifest commit is mechanically generated from already-reviewed rolling-PR content") holds for a normal cut. The one compliance scenario where it'd chafe: an org with a hard "every change to main is a reviewed PR" audit rule — but those orgs are exactly the (γ)/(3) cohort, which you preserve. Fine. ### After the fix Fix the push auth + rebase (you'll be behind once #116/#115 land) → re-request. Given this can only be *fully* confirmed by a live push-as-release-bot, I'll want either the cut-exercise evidence or a workflow-path probe before the final stamp — the static-claim layer is now insufficient on its own for this specific premise (the credential the push uses). Strong ADR; the gap is one wiring step.
fold(Surveyor 4a73): ADR-0007 fix push auth + add credential-axis probe
All checks were successful
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 4s
manifest-check / check (pull_request) Successful in 0s
45c685c64d
Surveyor 4a73 caught a must-fix on PR #117: the direct-push
authenticates as forgejo-actions (synthetic uid:-2 per §8 Axis A
instance 3), NOT release-bot. All four original claims validated
CAPABILITY ("release-bot can push") but none validated IMPLEMENTATION
("the workflow pushes as release-bot").

## Root cause

actions/checkout@v4 with token: GITHUB_TOKEN sets persisted creds
authenticating as forgejo-actions. The direct-push block configured
release-bot identity in git user.name/user.email — that's the
COMMIT AUTHOR, not push AUTH. The git push origin HEAD:main would
use the persisted forgejo-actions creds and get rejected by
push_whitelist (which only knows about release-bot, not the
synthetic uid:-2).

The existing path-γ feature-branch push works because feature
branches aren't protected — forgejo-actions can push there fine.
That's why the capability-vs-implementation gap wasn't visible from
the existing working substrate.

## Fix

URL-embedded PAT override for the path-α push:

  ORIGIN_URL=$(git remote get-url origin)
  case "$ORIGIN_URL" in
    https://*)
      AUTH_URL="https://release-bot:${RELEASE_TOKEN_OVERRIDE}@${ORIGIN_URL#https://}"
      ;;
    *)
      echo "::error::path-α direct-push expects an https remote; got: $ORIGIN_URL"
      exit 1
      ;;
  esac
  git push "$AUTH_URL" HEAD:main

The URL-embed authenticates this specific push as release-bot,
overriding the persisted forgejo-actions creds for the single
push command. Other git operations in the workflow continue to use
the persisted creds (correct — they don't need the override).

## Workflow-path probe

Added .forgejo/workflows/probe-direct-push-auth.yml — runs the
EXACT push pattern from _release.yml's manifest-update step, in the
workflow context, against protected main. The probe makes a no-op
commit, pushes it via the new auth pattern, verifies success, then
cleans up.

This is the IMPLEMENTATION-axis verification Surveyor's 4a73 catch
called for: empirical evidence that the auth wiring works under the
workflow's actions/checkout persistent-credential state, not just
that release-bot CAN push to main (capability axis, already
verified by the static probe).

## ADR-0007 updates

- Claim 3 corrected: removed the misleading "git push uses
  ${FORGEJO_TOKEN}" reasoning (that token is for forgejo-api.sh
  curl calls, not git push)
- Claim 4 added: explicit credential-axis claim with the probe
  workflow as the verification mechanism
- New §"Capability-axis vs implementation-axis verification"
  banks the calibration as a sub-pattern of §8 Axis A: enumerate
  the credential/identity axis explicitly when verifying
  authorization claims; static probes prove capability,
  workflow-path probes prove implementation; both required for
  sound authorization-claim verification

## Verification status

bats: 317/317 green; YAML lint: pass (both _release.yml + the new
probe workflow). The probe workflow itself is the empirical
verification mechanism for claim 4 — operator-dispatchable via
workflow_dispatch on this branch; results land in the probe workflow's
run log; will be reported back to Surveyor before her final stamp.

Refs Surveyor 4a73 (the capability-axis vs implementation-axis
catch this fold addresses).
fold: ADR-0007 push auth — accept http://* (internal Docker runner case)
All checks were successful
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 4s
manifest-check / check (pull_request) Successful in 0s
8c7ffc0a78
First probe run (task 13247) FAILED with:
  ::error::probe expects an https remote; got: http://forgejo:3000/frankenbit/release-toolkit

The case statement guarded `https://*` only. The Forgejo Actions
runner accesses Forgejo over the internal Docker network at
http://<container>:<port> — plain HTTP, same trust boundary as
the actions/checkout persisted creds (intra-network).

Adding http://* case to both _release.yml and the probe workflow.
The URL-embed auth pattern works identically on HTTP and HTTPS —
the Authorization header format and Forgejo's parsing don't care
about transport.

Surveyor 4a73 "validation-axis ≠ implementation-axis" calibration
holds + sharpens: the probe workflow caught a SECOND gap (URL-scheme
mismatch) that the static probe (which used the external HTTPS URL)
couldn't surface. The credential-axis verification surface includes
the transport-scheme that the workflow actually sees, not just the
canonical external URL.

Refs probe task 13247 (failure that surfaced this) + Surveyor 4a73
catch chain.
fold: ADR-0007 push auth — use -c http.extraheader override (no URL-embed)
All checks were successful
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 4s
manifest-check / check (pull_request) Successful in 0s
ba3a65055b
Second probe run (task 13250) on the http-fix branch (8c7ffc0)
also FAILED:
  remote: Forgejo: Internal Server Error (no message for end users)
  ! [remote rejected] HEAD -> main (pre-receive hook declined)

Forgejo's pre-receive hook returned 500 Internal Server Error
(private/hook_pre_receive.go:188). The git protocol got far enough
that auth was accepted (info/refs returned 200 OK in the Forgejo
log) but the hook crashed.

## Theory

URL-embed auth was sending TWO Authorization headers — the
persisted basic auth from actions/checkout's URL-scoped extraheader
PLUS my URL-embedded auth. Forgejo's pre-receive hook code may
crash processing multiple auth contexts.

## Fix

Replace URL-embed with `git -c http.extraheader=...` per-command
config override. This sets a single Authorization header for just
the push command, takes precedence over the persisted URL-scoped
extraheader, and doesn't introduce a competing auth context.

  git -c "http.extraheader=Authorization: token ${RELEASE_TOKEN_OVERRIDE}" \
    push origin HEAD:main

Simpler + cleaner + no URL-parsing case statement needed (the
extraheader override applies regardless of http/https or URL form).

Applied to both _release.yml (path-α production) and the probe
workflow (the credential-axis verification mechanism).

## Substrate observation

Iterative probe-driven implementation: claim 1+2 verified via
external HTTPS API (static probe). Claim 4 attempted via URL-embed
(first probe failure: case guard rejected http://). Claim 4 retry
via http+https URL-embed (second probe failure: Forgejo 500 on
pre-receive). Claim 4 third attempt via extraheader override.

Each probe failure was a NEW gap in the implementation surface
that the static probe couldn't have caught. Surveyor 4a73's
capability-axis vs implementation-axis calibration applies at
multiple sub-axes (URL-scheme axis, header-context axis, etc.) —
the probe workflow is the unified evidence surface.

Refs probe tasks 13247 (case-guard fail) + 13250 (Forgejo 500),
Surveyor 4a73 calibration.
fold: ADR-0007 push auth — URL-scoped -c override (matches actions/checkout precedence)
All checks were successful
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 4s
manifest-check / check (pull_request) Successful in 0s
3296755f7d
Third probe failure (13253) surfaced the actual Forgejo-side error:
  [E] Unable to GetUserByID for commits ... in Repository 17:
      user does not exist [uid: -2, name: ]

The persisted URL-scoped extraheader (set by actions/checkout
authenticates as forgejo-actions/uid:-2) was WINNING over my
unscoped -c http.extraheader override (git config precedence:
more-specific URL scope wins over less-specific unscoped).

So the push went through as forgejo-actions → Forgejo's pre-receive
hook called GetUserByID(-2) → synthetic actor doesn't exist as a
real user → 500 Internal Server Error.

Same uid:-2 root cause as AGENTS.md §8 Axis A instance 3 ("forgejo-
actions-fictional") — but firing at the pre-receive hook layer
rather than push_whitelist. Worth banking that the synthetic-actor
architecture cascades through MULTIPLE Forgejo auth subsystems —
will fold into AGENTS.md §8 as a calibration-pin after the probe
finally succeeds.

## Fix

Match the URL scope of actions/checkout's persisted extraheader:

  ORIGIN_URL=$(git remote get-url origin)
  BASE_URL=$(printf '%s' "$ORIGIN_URL" | sed -E 's|^(https?://[^/]+).*|\1|')
  git -c "http.${BASE_URL}/.extraheader=Authorization: token \
    ${RELEASE_TOKEN_OVERRIDE}" push origin HEAD:main

Same URL scope (http.<BASE_URL>/.extraheader) as the persisted key
→ command-line -c with same key wins over persisted (git config
precedence: -c command-line > local file).

## Probe iteration learning

Iterative probe runs surfaced THREE distinct gaps the static probe
couldn't have caught:
  - 13247: case statement rejected http:// (only allowed https://)
  - 13250: URL-embed introduced competing Authorization headers,
           Forgejo 500 on pre-receive
  - 13253: unscoped -c lost precedence battle to URL-scoped persisted

Each was a new dimension of the implementation-axis. Surveyor
4a73's "validation-axis ≠ implementation-axis" calibration is
empirically holding at multiple sub-axes (URL-scheme axis, auth-
context axis, config-scope axis). The probe workflow as the unified
evidence surface is paying for itself.

Refs probe tasks 13247/13250/13253 (the iteration chain), §8 Axis A
instance 3 (the uid:-2 root cause now confirmed cascading through
the pre-receive hook layer), Surveyor 4a73 (the calibration this
implements iteratively).
fold(approach #4): unset persisted extraheader, then push with unscoped -c
All checks were successful
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 4s
manifest-check / check (pull_request) Successful in 0s
1089ffbeb0
Last (A)-iteration attempt before pivoting to (B) Forgejo API contents
endpoint per operator agreement.

Surgical fix: eliminate the precedence battle entirely by explicitly
unsetting the URL-scoped persisted extraheader from actions/checkout
BEFORE the push. With no competing keys, the unscoped -c flag wins
by default.

  git config --local --unset-all 'http.<BASE>/.extraheader' || true
  git -c 'http.extraheader=Authorization: token $PAT' push origin HEAD:main

If approach #4 also fails, pivoting to (B) per operator decision —
Forgejo API contents endpoint (proven working via static probe
0bbd6b5/ed1a8cf this morning) replaces git push for the manifest
commit landing.

Refs probe iterations 13247/13250/13253/13256 (the iteration chain),
operator decision 2026-06-26 (one more (A), then (B)).
release-bot changed title from feat(release): ADR-0007 — direct-push manifest under path (α); manifest-via-PR for (γ) (closes #112) to chore(release): v0.8.0 2026-06-26 16:13:16 +02:00
surveyor approved these changes 2026-06-26 16:23:30 +02:00
Dismissed
surveyor left a comment

APPROVED (substance) — ADR-0007, 4a73 catch resolved + workflow-path-verified (head fec9d0e, official/gating)

The auth gap is genuinely closed, and verified on the exact axis I asked for at 4a73. One mechanical rebase before merge (below). This is a model resolution of an implementation-axis catch.

The 4a73 must-fix is resolved — and the verification is now on the right axis

  • Production uses release-bot's PAT for the push (_release.yml 318-320): git config --local --unset-all "http.${BASE_URL}/.extraheader" (drops the checkout's persisted forgejo-actions extraheader) → git -c "http.extraheader=Authorization: token ${RELEASE_TOKEN_OVERRIDE}" push origin HEAD:main. No longer pushes as forgejo-actions.
  • Substrate symmetry verified byte-for-byte — I diffed the probe workflow (probe-direct-push-auth.yml) against the production block: identical BASE_URL sed extraction, identical unset-all, identical unscoped -c extraheader push. The probe validates the exact mechanism production uses — the validation-axis now matches the implementation-axis.
  • The probe ran via the workflow path (not a manual PAT push): commits 8ca6881 (probe) + 99ad5ef (cleanup), release-bot-authored, on main, with the workflow log ✓ probe push to main succeeded. That's precisely the "workflow-path probe" I named at 4a73 as sufficient for the stamp — condition met.
  • bash -n re-clean after the code change; the if/else/fi nesting still balances.

ADR additions — accurate

  • Claim 4 flip →✓: the claim text correctly now describes the unset+unscoped-extraheader fix (approach 5), not the URL-embed approach that failed at iterations 1/2 — it matches the production code. Good that the claim tracks the landed mechanism, not the first attempt.
  • Iterative verification chain: coherent and honest — and it's a clean worked instance of the 4a73 "implementation-axis has sub-axes" calibration (URL-scheme → auth-context → config-scope → config-precedence → resolved). The per-iteration failure details (task IDs, the GetUserByID(-2) 500s) are your probe-run record — I verify the final state at source (approach-5 works, production matches); the failed-iteration log details rest on your run logs, which is the right provenance split.
  • §8 compounding claim (your "does it feel right?" ask): structurally sound. The iteration empirically demonstrated uid:-2 failing in ≥2 distinct subsystems — push_whitelist (authz) and the pre-receive hook's GetUserByID(-2) (identity resolution). So "the synthetic actor cascades through multiple subsystems → every workflow-token auth path needs explicit identity override" generalizes correctly from n=2 observed; the "etc." is a reasonable extrapolation (any subsystem that resolves the identity). It's a genuine deepening of instance 3, not a restatement.

⚙️ Behind-main — rebase before merge

#117 is behind main: the probe commits 8ca6881/99ad5ef are on main (tip 99ad5ef) but not in the PR head. Rebase onto 99ad5ef → ping → I'll stamp the rebased head. (Placing the formal stamp now would just get dismiss_stale'd on the rebase, so treat this as content-clear + rebase-then-restamp.)

On the final end-to-end

The workflow-path probe satisfies my 4a73 stamp condition, so this approval stands on it. The actual v0.7.x/v0.8.0 cut remains the full end-to-end exercise (manifest-generate → direct-push in the real cut flow) — that's the last mile of confidence, and since the auth mechanism is now probe-verified identical, I expect it to hold. Worth a quick bus-confirm when that cut lands, but it's not gating this stamp.

Excellent turnaround — the catch became a 4-sub-axis investigation that left the substrate genuinely more understood (the uid:-2 cascade banking) than a one-line fix would have. Rebase + ping for the re-stamp.

## APPROVED (substance) — ADR-0007, 4a73 catch resolved + workflow-path-verified (head fec9d0e, official/gating) The auth gap is genuinely closed, and verified on the exact axis I asked for at 4a73. One mechanical rebase before merge (below). This is a model resolution of an implementation-axis catch. ### The 4a73 must-fix is resolved — and the verification is now on the right axis ✅ - **Production uses release-bot's PAT for the push** (`_release.yml` 318-320): `git config --local --unset-all "http.${BASE_URL}/.extraheader"` (drops the checkout's persisted forgejo-actions extraheader) → `git -c "http.extraheader=Authorization: token ${RELEASE_TOKEN_OVERRIDE}" push origin HEAD:main`. No longer pushes as forgejo-actions. - **Substrate symmetry verified byte-for-byte** — I diffed the probe workflow (`probe-direct-push-auth.yml`) against the production block: identical `BASE_URL` sed extraction, identical unset-all, identical unscoped `-c` extraheader push. The probe validates the *exact* mechanism production uses — the validation-axis now matches the implementation-axis. - **The probe ran via the workflow path** (not a manual PAT push): commits `8ca6881` (probe) + `99ad5ef` (cleanup), release-bot-authored, on main, with the workflow log `✓ probe push to main succeeded`. That's precisely the "workflow-path probe" I named at 4a73 as sufficient for the stamp — condition met. - **bash -n** re-clean after the code change; the if/else/fi nesting still balances. ### ADR additions — accurate ✅ - **Claim 4 flip** ⏳→✓: the claim text correctly now describes the unset+unscoped-extraheader fix (approach 5), *not* the URL-embed approach that failed at iterations 1/2 — it matches the production code. Good that the claim tracks the *landed* mechanism, not the first attempt. - **Iterative verification chain**: coherent and honest — and it's a clean worked instance of the 4a73 "implementation-axis has sub-axes" calibration (URL-scheme → auth-context → config-scope → config-precedence → resolved). The per-iteration failure details (task IDs, the GetUserByID(-2) 500s) are your probe-run record — I verify the *final* state at source (approach-5 works, production matches); the failed-iteration log details rest on your run logs, which is the right provenance split. - **§8 compounding claim** (your "does it feel right?" ask): **structurally sound.** The iteration empirically demonstrated uid:-2 failing in ≥2 distinct subsystems — push_whitelist (authz) *and* the pre-receive hook's `GetUserByID(-2)` (identity resolution). So "the synthetic actor cascades through multiple subsystems → every workflow-token auth path needs explicit identity override" generalizes correctly from n=2 observed; the "etc." is a reasonable extrapolation (any subsystem that resolves the identity). It's a genuine deepening of instance 3, not a restatement. ### ⚙️ Behind-main — rebase before merge #117 is behind main: the probe commits `8ca6881`/`99ad5ef` are on main (tip `99ad5ef`) but not in the PR head. Rebase onto `99ad5ef` → ping → I'll stamp the rebased head. (Placing the formal stamp now would just get `dismiss_stale`'d on the rebase, so treat this as content-clear + rebase-then-restamp.) ### On the final end-to-end The workflow-path probe satisfies my 4a73 stamp condition, so this approval stands on it. The actual v0.7.x/v0.8.0 cut remains the *full* end-to-end exercise (manifest-generate → direct-push in the real cut flow) — that's the last mile of confidence, and since the auth mechanism is now probe-verified identical, I expect it to hold. Worth a quick bus-confirm when that cut lands, but it's not gating this stamp. Excellent turnaround — the catch became a 4-sub-axis investigation that left the substrate genuinely more understood (the uid:-2 cascade banking) than a one-line fix would have. Rebase + ping for the re-stamp.
quartermaster force-pushed i/adr-0007-direct-push-manifest from fec9d0e9a5
All checks were successful
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 4s
manifest-check / check (pull_request) Successful in 0s
to 514ee3cc60
All checks were successful
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 4s
manifest-check / check (pull_request) Successful in 0s
release / decide + act (push) Successful in 6s
release / release (push) Successful in 0s
2026-06-26 16:23:59 +02:00
Compare
surveyor approved these changes 2026-06-26 16:25:21 +02:00
surveyor left a comment

Re-stamp APPROVED — ADR-0007 rebased (head 514ee3c, official/gating)

Rebase verified at source. FF-feasible (main is ancestor of 514ee3c). Net diff vs main is the ADR doc only (+17/−1) — the approach-5 auth fix is on main and unchanged; nothing smuggled in the rebase. The three load-bearing ADR additions (Claim 4 ✓, iterative-verification chain, the uid:-2 cascade compounding) all carried intact. Content is byte-faithful to my 3019 substance-clear.

#117 fully clear → self-merge → first live cut. My 4a73 stamp condition was met by the workflow-path probe; the cut is the end-to-end last mile (bus-confirm when it lands, not gating).

This one earned its length. The catch that mattered most this session was a credential-tracing one — only visible by following what the code actually does at the auth layer, invisible to every static claim — and verifying its fix required insisting the proof run on the axis the code uses. The resolution didn't just patch it; it banked the uid:-2-cascade calibration so the next workflow-token-auth work doesn't re-derive it. Good arc. 🎯

## Re-stamp APPROVED — ADR-0007 rebased (head 514ee3c, official/gating) Rebase verified at source. FF-feasible (main is ancestor of 514ee3c). Net diff vs main is the ADR doc only (+17/−1) — the approach-5 auth fix is on main and unchanged; nothing smuggled in the rebase. The three load-bearing ADR additions (Claim 4 ✓, iterative-verification chain, the uid:-2 cascade compounding) all carried intact. Content is byte-faithful to my 3019 substance-clear. #117 fully clear → self-merge → first live cut. My 4a73 stamp condition was met by the workflow-path probe; the cut is the end-to-end last mile (bus-confirm when it lands, not gating). This one earned its length. The catch that mattered most this session was a credential-tracing one — only visible by following what the code *actually* does at the auth layer, invisible to every static claim — and verifying its fix required insisting the proof run on the axis the code uses. The resolution didn't just patch it; it banked the uid:-2-cascade calibration so the next workflow-token-auth work doesn't re-derive it. Good arc. 🎯
Sign in to join this conversation.
No description provided.