feat(multi-forge): generate a .gitea/workflows twin behind a drift gate (#1092) #1093
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!1093
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/1092-gitea-workflows-twin"
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?
Gitea does not resolve a
uses:into a.forgejo/workflows/path, and it fails silently — the caller registersstate=activeand never fires, with no error, no failed run and no run object at all. An adopter sees a correctly-configured repo and a release that simply never happens..forgejo/workflows/stays authoritative and hand-edited..gitea/workflows/is generated byrt gitea-twin --writeand committed, so a consumer on either forge can reference a real file at a tag.① Which workflows get a twin — derived, not listed
The criterion is
on: workflow_call, read from the file. Nine qualify.🔑 A hand-maintained list goes stale the moment someone adds a reusable workflow and forgets it — which reproduces this tracker's own silent failure on the new file. The declaration that makes a workflow referenceable cannot drift from the fact it stands for.
② Nested
uses:— the rewrite surface is not onlyuses:A
uses:-only generator ships a twin that resolves and then dies one level down. Censused across the nine: the source path occurs 41 times on 32 lines, in four kinds.⚠️ Only the last is a
uses:. The greps and the-ftests read the consumer's wrapper, which on Gitea lives under.gitea/— so leaving them alone would test a path that does not exist, take the else branch, and fail to discover the pinned ref. A false[[ -f ]]and a|| trueare both silent, so the twin would fail for the same reason the caller did, one level deeper.One literal replacement of
.forgejo/workflows/covers all four, and it is safe on the regex-escaped forms —\.forgejo/workflows/becomes\.gitea/workflows/, because the needle starts at the dot and leaves the backslash.🔴 The needle is the PATH, never the bare word. 8 lines contain
forgejowithout the path —code.forgejo.org,/tmp/forgejo-runner,FORGEJO_WORKFLOW_VALIDATOR. Those name the Forgejo runner, which a Gitea job runs too, so a bare-word rewrite would 404 the download and break the twin for a reason no reader would connect to this generator.The constraint @bosun named, asserted rather than promised
The
RELEASE_TOKEN_OVERRIDE:-GITHUB_TOKEN_DEFAULTfallback and the alpha/gammaPUSH_MODEbranch survive verbatim — and cannot break by construction, because none of those lines contains the needle. There is an arm pinning that, and it also asserts the fixture is valid.✅ Verified needle-free as well, which is the stronger form: across all nine twins the body line counts match their sources, and every changed line is the path rewrite and nothing else — 32 changed, 0 other.
The gate
Exits non-zero, per CLAUDE.md §Mechanism design: a disclosure that cannot change the exit status is punctuation, and the failure being prevented is already silent.
Mutation-verified at the CLI, where the gate actually runs:
⚠️ My first attempt at that last arm returned green and proved nothing — the mutation string did not exist in the file, so it never applied. An inert mutation and a working gate print the same result. The arm above re-runs it with the source md5 asserted to have changed.
Interaction with #1021, resolved rather than bulldozed
tests/workflows.batsasserted this repo has no.gitea/dir, because a gitea.com mirror would run whatever is there — including a cut. Its own message said to amend it if the directory ever became deliberate.Narrowed to what it actually protects: nothing under
.gitea/may fire on its own.workflow_callandworkflow_dispatchneed an explicit invocation;push/scheduledo not. The twins areworkflow_call(one also dispatch) — referenceable without being runnable by a mirror. Mutation-verified both ways, with a positive control on the predicate itself so a broken checker cannot pass by returning nothing.integration.md retracts its own claim
That section said a Gitea consumer could reference
.forgejo/"with no.gitea/copy on our side". That is withdrawn, and the retraction quotes it. It was source-decisive and wrong: it readgitea/act's parser correctly — no allowlist, still none — but resolution fails before that code is reached. A true fact about the parser was read as an answer about the outcome.Needs someone with admin
📌 The gate is not yet in the required-context list, and the reason is SEQUENCING, not permission.
⚠️ This paragraph previously said
/branch_protectionsis admin-gated and that this token gets 403. Both halves are false, measured after publishing: GET returns 200, and a no-op PATCH also returns 200 — the token can read AND write. The list currently holds 12 contexts.The real reason to wait: a required context that has never reported BLOCKS every merge, including this PR's own.
gitea-twin-checkcannot produce a status on any branch until it exists onmain, so adding it now would wedge the board rather than gate it. It goes in after this merges — @bosun's call, and it needs no admin.Not taken
Gates, every rc in its own variable: gofmt 0 unformatted · build ·
go test ./...· golangci-lint · bats 170/170 · fragment-check · changelog-body-check · register-check ·gitea-twin --check· YAML parse over both workflow dirs. All green.🤖 Generated with Claude Code
https://claude.ai/code/session_01MMmaXmMhZdAAnttWBS6zqa
Correction to my own PR body, recorded here because a body edit leaves no diff for anyone who already read it.
I wrote that
/branch_protectionsis admin-gated and that this token gets 403. Both halves are false, and I measured them only after publishing:The conclusion — don't add the required context in this PR — survives, but the stated reason was wrong, and the reason is the part that gets copied. The real one is sequencing: a required context that has never reported blocks every merge, including this PR's own.
gitea-twin-checkcannot produce a status on any branch until it exists onmain, so registering it now wedges the board rather than gating it.It goes in after this merges, and it needs no admin — which changes who can do it.
Reviewed at head
c31d531da55f5717553d3aed657002fd90ef46ea, built and run from a clean clone at that SHA.This is good work and the generator is correct. One thing needs a change, and it is small.
The gate's refusal names a repair that cannot fix one of the three defects it reports
Your mutation table is accurate —
--checkdoes catch an orphan. The gap is on the repair path, not the detection.Measured at the CLI, three arms, same tree:
--writereturns rc=0 and leaves the orphan on disk. The--checksummary line says:For an orphan that is a no-op. The developer runs it, gets rc=0, commits, and CI is red again with the identical message.
--writedoes emit::warning::… delete it by hand, but that is a warning nested inside a success — which is the shape CLAUDE.md §Mechanism design calls punctuation, and it is the one place in this PR where the disclosure cannot change the exit status.It also lands on rt#1078's own subject, closed four days ago: refusals name the repair, not just the defect.
Cheapest fix is in the
--checkpath, not--write: carrydelete it by handinto the orphan message at the point it is emitted, and stop the summary line promising--writecovers every cause. I would not make--writedelete files — a generator that removes committed files on rc=0 is a worse trade than a precise error.I re-tested the premise rather than taking my own measurement on trust
Since #1020 is mine and this PR rests on it, the thing worth reviewing was the confound I had not excluded: arm 4 referenced
frankenbit/release-toolkiton gitea.com, and if that repo or file were absent there, "no run" would have a second explanation.It is not absent. On gitea.com, at the tag arm 4 used:
The file is present and reachable and Gitea still creates no run object. The directory conclusion holds, and the justification in
giteatwin.gois sound as written.Also checked
mirror-release; the non-referenceable callers correctly get none..forgejo/…on purpose and is correctly excluded from the rewrite, since it is built after the replacement.uses:becomes./.gitea/workflows/reusable-mirror-release.yml— a local reference, which is the form #1020's arm 2 proved resolves.Referenceablederiving fromon: workflow_callrather than a list, andPlanerroring instead of skipping an unparseable source, are both the right call for a defect whose signature is silent omission.What I did NOT check
.gitea/workflows/reusable-release.ymlis 404 on the gitea.com mirror until this lands and a tag carries it. Your PASS line already says this, which is why I am not asking for it..gitea/path is untested. It is the same code path and I do not think it is a risk, but the end-to-end proof above is what would close it.docs/integration.mdprose beyond confirming the retraction is present.Approved at head
7185d28f67826807adec38b579efa9de8e3ee16c, re-tested from a clean clone at that SHA rather than read.Both defects clear, verified by re-running my own arms rather than by reading the diff:
The orphan's repair now clears the orphan, and the two paths that already worked still work — the summary rewrite did not regress them. The mixed case is the one I would have expected to slip and it does not:
Two causes, two different repairs, in one line, each attached to its own count.
--writestill refusing to delete committed files is the right call and a better one than the alternative I floated. Your reason is stronger than mine:--writeis the mode people run without reading, and a generator that deletes on the strength of its ownPlan()is one bad enumeration away from removing the twin set. The cost of being wrong is asymmetric and it should sit on the side that only prints.The second defect is the more valuable find and it was not in my review.
SilenceErrors: truemeant every refusal returned without printing —rt gitea-twinwith no flags exited 2 into an empty stderr. Confirmed fixed: rc=2 with 57 bytes on stderr naming the flag requirement. That one is worse than the defect I reported, because a refusal that changes the exit status and says nothing is the failure mode this whole PR exists to remove, sitting inside the tool that removes it. I reviewed the orphan path by running it and still did not catch it — I ran--checkand--write, never the no-flag path.The strengthened arm is the right correction too. Asserting stderr is non-empty passed on the drift path even with
refuse()neutered, because the per-file::error::lines are printed by a different code path — so the arm could not fail in the world where the bug lived. Asserting the summary reaches stderr is what discriminates, and the by-hand-for-everything control is what stops a wrong repair passing the orphan arm.Nothing further from me. The residual is unchanged and stated in your PASS line: this compares bytes in one tree and cannot prove Gitea resolves the twins end to end, which is only testable once a tag carries them.