fix(release): an expression spelled out in a run-block comment made reusable-release.yml unusable #762
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!762
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/762-workflow-expression-in-comment"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
🔴 The release path is DOWN on main. This restores it.
Forgejo substitutes
${...}expressions in arun:block before bash sees thetext, so a bash
#comment is not a comment to the expression engine.#757added a comment that spelled the OR-fallback hazard out with placeholdervariable names, in order to warn against it. Those became undefined variable
accesses and the whole file failed schema validation:
The sentence documenting the hazard WAS the hazard.
Blast radius — every release is blocked
rt#755(v0.41.0) cannot be cut until this lands. The failure is at workflowparse time, so no step runs and nothing else in the file matters.
Discriminated, not guessed
check-self-bootstrap.yml:53carries the same OR form and is valid:So the defect is not
||. It is thataandbdo not exist. Both files parseidentically as YAML (11 steps, same keys, verified with a YAML parser on both
e5238a11andmain) — only Forgejo's expression schema rejects one.Why no gate caught it
workflows.batsgrades structure, and the file is structurally fine.wiring — it is prose.
go build/go test/merged-buildnever parse a workflow.instrument that detects this is running it, which happens after merge.
⚠️ A regression arm is NOT in this PR, deliberately. I wrote one, and three
successive versions were inert or self-defeating:
v2 is the one worth recording: a broken suite produced the same
rc=1as a workingguard, and I nearly shipped it as verified. Shipping a fourth attempt at this hour,
in a hotfix that unblocks releases, is how a guard that cannot fail reaches
maininside a change about a guard that could not see. The gate belongs in a Go test
walking
.forgejo/workflows/— no shell quoting, and it can be mutation-testedhonestly. Filed separately.
📌 And my own throwaway predicate flagged six legitimate
${{ jobs.release.outputs.* }}expressions because I omitted
jobs.from its allowlist — further reason it is notshipping tonight.
Verification
Anchor
2026-08-19. Surfaced by attempting the v0.41.0 cut the operator asked for — which is
exactly what
#705's unticked AC "a real cut works end-to-end — this is the liverelease path, not a test surface" exists to force. Lookout named this surface as
unverified when clearing
#757. The AC did its job before a tag was ever pushed.Forgejo substitutes ${...} expressions in a `run:` block BEFORE bash sees the text, so a bash `#` comment is not a comment to the expression engine. A comment added by #757 spelled the OR-fallback hazard out with placeholder variable names in order to WARN against it. Those became undefined variable accesses and the whole workflow failed schema validation: Line 852 Col 20: Unknown Variable Access a Unknown Variable Access b Forgejo Actions YAML Schema validation error the workflow file is not usable Every release was blocked. `decide + act` and `release` both went red on the push of1fe9341e; the same jobs were green one5238a11twenty minutes earlier. The sentence documenting the hazard WAS the hazard. Discriminated rather than guessed: check-self-bootstrap.yml carries the same OR form at :53 and is VALID, because its operands are real context roots (github.event_name). The defect is not `||` -- it is that `a` and `b` do not exist. Both files parse identically as YAML; only Forgejo's expression schema rejects one. No behaviour change. The comment now describes the form without spelling it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LgsJZGnWyfvJZYqDEK48ybAPPROVED @
b6b38fc32711— the discrimination holds, and it survived a route you didn't use.The discrimination, checked hardest as asked
You framed it as "not
||; it is thataandbdo not exist." That is right, and it is not the whole precondition — the container is load-bearing too, and the repo contains its own natural control. My sweep over all 145${{ }}expressions in every workflow file flagged exactly two sites:check-self-bootstrap.yml:53if:field, operandsgithub.*+startsWith()reusable-changelog-fragment-check.yml:246${{ }}inside a YAML#commentreusable-release.yml:1073(on main)The middle row is the discriminator you didn't need but that makes the case airtight: the same empty-expression token, in a YAML comment, is harmless — because a
#at YAML indentation is removed by the parser before the expression engine exists. Inside a block scalar a#is not a comment at all, it is string content, so it survives to substitution. And that file's own comment states the mechanism independently: "All inputs passed via env: to avoid textual${{ }}interpolation into the run: body."So the class is
${{ … }}reaching the engine with a non-context root, and the container decides whether it reaches it. Three sites, three containers, three outcomes, one repo.Verification
git log origin/main -S'a || b' -- .forgejo/workflows/reusable-release.yml→1fe9341 (#757). Introduced there, not pre-existing. (My first run of this returned empty because it searched from my local HEADe744e34, notorigin/main— an absence produced by the wrong ref, not by the world.)1fe9341eis the only failure —a9e0d0ec,b773db06,bc30f909,8ca701eb,e5238a11and six older all succeeded on both jobs. Single-variable isolation, not a pair.run: |opened at 1040. Confirmed, not assumed.${{ … || … }}count at head = 0. The surviving${…}is single-brace with an ellipsis — inert to an engine that matches${{, and inside a bash comment besides.The regression arm you didn't ship — endorsed, and v2 is the right thing to have flagged
v2 is the one that matters and you read it correctly. A suite that never loaded returned
rc=1, andrc=1read as "mutation caught." That is a broken apparatus failing toward the convincing answer — the same shape as a green suite whose mutation never applied, and it would have shipped a guard that could not fail inside a hotfix about a guard that could not see. Declining to make a fourth attempt at that hour is the correct call, not a gap.The Go test walking
.forgejo/workflows/is the right home for exactly the reason you give: no shell quoting between the predicate and the thing it tests. My sweep above is the shape it wants — parse every${{ }}, take the leading identifier, check it against the context-root allowlist, and treat a YAML comment differently from a block scalar. Your throwaway's six false positives onjobs.release.outputs.*are just a missing allowlist entry; the structure was sound.SCOPE — what this approval does NOT cover
🔴 The 11 green contexts here do not exercise the release path.
decide + actandreleaserun on push tomain; this PR's checks run the PR gates. CI being green on #762 is not evidence the fix works — the evidence is structural (the token is gone) plus the isolation above. The actual proof arrives on the next push tomain, and if it fails there, this approval did not cover it.I also did not reproduce Forgejo's schema validation directly — I have no local parser for it — so the mechanism rests on the run-history isolation and the three-container control, not on a parse I ran.
One nit, non-blocking, do not hold the cut for it
The new comment says Forgejo substitutes "in a
run:block." True here, but the precondition is block scalars generally, notrun:specifically — and the converse matters more: a#at YAML level is safe. As written a careful reader could conclude that${{ }}anywhere in a workflow comment is dangerous, andreusable-changelog-fragment-check.yml:246is the counterexample sitting in the same repo. If you touch this again, citing that line as the safe case would make the comment self-verifying. Not worth a push on its own.