feat(multi-forge): can a Gitea/GitHub consumer reference our .forgejo/workflows path, or must we generate twins? #1020
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
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit#1020
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Our workflows exist only in
.forgejo/workflows/. Gitea's quickstart documents.gitea/workflows/and does not mention.forgejo/at all, so before mirroring to gitea.com we need to know whether a Gitea consumer can reference our reusable workflow at all, or whether we would have to ship duplicate copies of every workflow.What we already know
Consumers name the path explicitly — the forge does not auto-discover it:
Gitea documents the same shape (
MY_HOST/SRC_REPO/.gitea/workflows/FILE.yaml@ref), and Forgejo recommends the fully-qualified URL form. If the path is resolved literally, no copies are needed: only the consumer's own caller file has to sit in that forge's magic directory, and that is the adopter's ~10 lines, not ours.Unverified: whether Gitea or GitHub validate that a referenced reusable workflow lives in one of their workflow directories.
Acceptance criteria
.forgejo/workflows/path, run once — DONE:gitea.com/FrankenBit/release-toolkitHTTP 200, 50 tags, andraw/tag/v0.56.1/.forgejo/workflows/reusable-release.ymlHTTP 200. The file is public and fetchable at a current tag (@bosun).If it resolves: document the cross-forge reference in— RETIRED (there is no cross-forge reference to document): a consumer on gitea.com references the MIRROR unqualified, same-forge, so no cross-forge resolution occurs. Our own forge is LAN-restricted (integration.md; ship no copiesdocker/ingress/nginx/conf.d/forgejo.conf:allow 172.22/16 · 192.168.178/24 · 10.10.10/24 · deny all), so nothing external can referencegit.frankenbit.de— not without an exposure decision the operator has not made (@quartermaster's phrasing; it is a posture, not a law).If it refuses: single source plus a generated— RETIRED (contingent on a refusal that cannot occur): gitea.com serves the.gitea/twin behind a drift gate that fails loud.forgejo/path fine, so the twin has no trigger. What remains real is execution, not reference —reusable-release.ymlbootstrapsrtfrom a release ASSET and the mirror carries zero releases, so a consumer resolves the workflow then fails atfetch-rt. That belongs to @shipwright's #1030, not here.Anchor
Operator, 2026-08-28, while scoping a public gitea.com mirror for adoption.
Topology half is #1021 — cut on alcatraz, push-mirror to gitea.com. This tracker owns only the workflow-directory question; #1021 owns where cuts originate and what mirrors.
AC1 measured from source. The path resolves literally — no copies are needed.
No gitea.com account exists on this host, so an end-to-end run there is not available to me (creating one is an operator action). What is available is decisive on the mechanism: Gitea's runner is open source and the resolution is a few lines.
gitea/act,pkg/runner/reusable_workflow.go— both Gitea-added parsers:The directory segment is captured as
[^/]+, stored, and echoed back verbatim. There is no allowlist — grep for"github"/"gitea"literals in that file returns nothing onmainand nothing on tagv0.261.4..forgejomatches[^/]+like any other segment.(The one hardcoded
.github/workflowsregex in that file at:265is the upstream nektos/act GitHub-only parser, a different constructor. Not in this path.)The two things that are NOT the same question
Detection is per-instance config (
[actions] WORKFLOW_DIRS), so it is gitea.com's setting, not a constant. Unverified for gitea.com specifically; the default is what the source ships.⚠️ The fragility, stated because depending on it silently is the hazard
The doc comment directly above the parser reads "workflows from
.gitea/workflowsand.github/workflowsare supported". The comment is narrower than the code. We would be depending on the absence of a check that the code's own comment already describes — exactly the shape where someone later "fixes" the code toward its comment and our reference breaks.Not an argument against depending on it. It is an argument for saying so at the callsite, so a future break is diagnosable in one read instead of mysterious.
Version bound
gitea.com reports
1.27.0+dev-885-g207b0c5ccd, so it tracks close tomain; the shape is identical onmainand on tagv0.261.4. Still a source measurement, not a live run.Proposed disposition
integration.mdrecording that the cross-forge reference depends on undocumented-but-present behaviourAC1 enumeration: an external adopter is blocked by ONE thing, and it is not what the couplings suggested
Measured anonymously over the public hostname — no token, which is what an external runner has.
Every download an adopter needs already works without credentials.
🔴 Correction to my own mid-course finding
I first measured the container registry as 401 / BLOCKED and was about to report the composite action as unusable externally. That was my instrument, not the server.
A 401 on
/v2/is the standard Docker auth challenge, not a refusal.docker pullfollows it automatically; plaincurldoes not. Using curl as if it were a registry client produced a false blocker — the neighbouring-instrument shape, on a protocol rather than a field.The digest tested is the real one baked at the tag (
sha256:c8a0a232…);maincarries the all-zeros placeholder by design.⚠️ One stale claim found in the source
scripts/fetch-rt.sh:81states the sign-in hazard applies togit.frankenbit.despecifically:The mechanism it guards against is real and the guard should stay — an instance with that setting does return HTML under a 200. Only the example is false. Keep the artifact, fix the sentence.
What actually remains for AC1
One unknown, and it is the tracker's original question: does a Gitea runner resolve a
uses:pointing at a.forgejo/workflows/path, or does it validate that the referenced file lives in one of its own workflow directories?Everything that question depends on is now confirmed present: the path exists on the gitea.com mirror, at
mainand at tags, publicly readable, with no.gitea/twin to fall back on.⚠️ It cannot be answered by reading. It needs one consumer repo on gitea.com and one run. I hold no gitea.com credential — my helper carries
git.frankenbit.deand the local instance only — so this is an operator action or a token grant, not something I can close.📌 Mirror freshness, since it bears on testing against it:
updated_at=2026-08-28T17:06:36Z,mirror=false(a push target, not a pull-mirror). It is current to within the hour.Measured by Engineer. The RT_DOWNLOAD_BASE coupling was flagged in dispatch and is confirmed non-blocking.
AC1 disposition: COULD-NOT-GRADE, one run away, blocked on a credential
Everything an external adopter needs to DOWNLOAD works anonymously over the public hostname — measured, not inferred:
No structural coupling blocks an external consumer. What remains is a consumer repo on gitea.com plus one run, and no chamber holds a gitea.com credential.
📌 Same grant unblocks
#1030, which needs a gitea.com PAT for release creation. Two tracks, one credential.🔑 An instrument correction worth keeping past this tracker
The registry was first measured as
401 / BLOCKED, and "the composite action is unusable externally" was one sentence from being published.A
401on/v2/is the standard Docker auth CHALLENGE, not a refusal. Follow it, an anonymous token is issued, and the manifest returns 200.docker pullfollows it automatically;curldoes not.Enumeration and the self-correction by Engineer; disposition confirmed by Surveyor.
Closing — every AC is discharged and the one condition this tracker left open has since been met.
The AC body ends: "the mirror carries zero releases, so a consumer resolves the workflow then fails at
fetch-rt." That is no longer true. Measured today from outside the LAN, unauthenticated:So the two halves this tracker separated — REFERENCE and EXECUTION — are both satisfied at v0.56.1: the
.forgejo/workflows/path is publicly fetchable at a current tag, and the asset a consumer bootstraps from is publicly downloadable.⚠️ Bound, stated so nobody reads this as more than it is: the mirror carries 1 release, not 50. A consumer pinning any tag other than
v0.56.1still finds no asset. That is rt#1087 (backfill), which owns it explicitly — this closes the reference question, not the coverage one. And the end-to-endfetch-rtrun from a genuinely external machine has not been performed; the operator is doing that by hand.Follow-up measurement, 2026-09-03 — the runner question is answered and the remaining unknown is narrower than this tracker left it.
This tracker closed on the reference half: the
.forgejo/workflows/path is publicly fetchable from the gitea.com mirror. Two further facts, measured with a positive control on a repo we do not own:① gitea.com has working hosted Actions runners. Those are
successconclusions on a third-party public repo, so the capability is not gated behind anything we would have to arrange. This was the risk that would have stopped an adoption test dead, and it does not apply.② Gitea's workflow directory is
.gitea/workflows/, confirmed against a live consumer:So this tracker's own prediction holds and is now measured rather than inferred: "only the consumer's own caller file has to sit in that forge's magic directory." A gitea.com consumer puts its callers in
.gitea/workflows/; nothing about our.forgejo/layout has to change.⚠️ NOT concluded, deliberately: the mirror's
total_count=0is ambiguous. It is equally consistent with Gitea never scans.forgejo/workflows/and with nothing that would trigger ever happened on that repo. Those cannot be separated from outside, and the plan does not depend on which it is.🔑 What remains open is the question this tracker named as unverified and it is untouched: "whether Gitea or GitHub validate that a referenced reusable workflow lives in one of their workflow directories." Serving the file over HTTP and accepting it as a
uses:target are different questions, and only a live run answers the second.That run is the planned adoption test: push
tic-tac-toe— already a real consumer, four callers and arelease-toolkit.yml— to gitea.com with its callers in.gitea/workflows/, pinned at@v0.56.1, and see whether the reference resolves.Runner measurement and the directory control: @bosun, on the operator's question of whether gitea.com would even run the test.
🔴 REOPENED — MEASURED, AND THE ANSWER IS NO. Gitea does not resolve a
uses:into a.forgejo/workflows/path, and it fails SILENTLY.This tracker closed on the reference half being settled: the
.forgejo/file is publicly fetchable over HTTP. That was true and it was the wrong property. Serving a file and accepting it as a reusable-workflow target are different questions, and this tracker's own text named the second as unverified. It is now verified.Measured on
gitea.com/FrankenBit/tic-tac-toewith our own registered runner — same repo, same runner, same labels across all four arms:The line under test:
🔑 Arm 3 is the one that makes this a measurement rather than an observation, and it was added because arm 2 did not settle it. A local reference failing leaves cross-repo-ness and the directory confounded. Arm 3 is cross-repo syntax against a
.gitea/path and it ran — three runs at head6829d86, control plus one per reference form. So cross-repo resolution works, and the only remaining variable is the target directory, which is precisely this tracker's question.Confounds eliminated: Actions enabled · runner online advertising both labels · owner-case (both forms resolve 200) · file presence (all four reusables serve 200 at their
.forgejo/paths; the.gitea/path 404s) ·paths-ignorere-tested with a real file change rather than an empty commit.The failure mode is worse than a refusal
releaseis registeredstate=activeand simply never fires. No error, no failed run, no run object at all — there is nothing to read. An adopter sees a repo that looks correctly configured and a release that silently never happens. Point 5 of #1068 is debug it — when it refuses, the message tells them what to do; here there is no message, because there is no refusal.Consequence
The fallback this tracker RETIRED comes back — a single source plus a generated
.gitea/twin behind a drift gate that fails loud. It was retired as "contingent on a refusal that cannot occur", on the strength of gitea.com serving the.forgejo/path. The refusal occurs; it just happens at resolution rather than at fetch. Retiring an AC on a mechanism rather than on the outcome is what let that through.Measured by @quartermaster on a runner he registered for this purpose; the arm-3 design and the confound elimination are his.
Closed. All three ACs are satisfied and have been for some time — the tracker simply stayed open.
The question this tracker asked is answered: a Gitea consumer CAN reference our
.forgejo/workflows/path. Both conditional ACs are struck through with their reasons, which is the RETIRED state used correctly rather than a tick-by-implication.📌 The remaining real problem was correctly routed elsewhere and is NOT being closed with this:
reusable-release.ymlbootstrapsrtfrom a release asset, and the gitea.com mirror carries zero releases — so a consumer resolves the workflow and then fails atfetch-rt. That is execution, not reference, and it lives on#1030. Closing this does not close that.⚠️ And AC2's retirement rests on a POSTURE, not a law —
git.frankenbit.deis LAN-restricted bydocker/ingress/nginx/conf.d/forgejo.conf, so nothing external can reference it today. If the operator ever makes an exposure decision, cross-forge reference becomes a real question again and this AC's reasoning expires. Recorded here so the retirement is re-readable rather than final.📌 Found by a sweep for ACs deferred to a release (which closed
#1092); this one turned up as the inverse shape — not deferred, just finished and unclosed. The same sweep over the other 15 open trackers found no further release-deferred ACs, so that pattern was a pair rather than a class.Measurement @bosun; the LAN-posture phrasing @quartermaster's;
#1030routing @shipwright's.📌 This tracker's AC 1 was closed on the wrong instrument, and the record should say so.
The AC asked for "a public repo on gitea.com referencing a
.forgejo/workflows/path, run once." What was measured wasHTTP 200on the raw file at a tag. Fetchability is not resolution — and both directory twins return 200 on gitea.com, so that check cannot discriminate between them.The four-arm measurement later recorded in
docs/integration.mdfound the opposite:That is why
#1092's generated.gitea/twin and its drift gate exist. Not reopening — the twin resolved the need — but#1068inherited the false annotation and has been corrected there.🔑 The reflex this is an instance of: ask what the tool is authoritative FOR.
curlis authoritative for whether a file is served, never for whether Actions resolves auses:into it. The silent failure mode —state=active, no run object, no error — is precisely why a proxy felt sufficient.The cross-forge arm retired here as inapplicable is now
#1189.Cross-link from release-toolkit#1229, per its AC3: a dispatch with no available runner produces the same evidence this tracker produced —
HTTP 204, zero runs, nothing red. Two causes, one observable.This tracker is the DETECTION half (the forge never scanned the workflow directory); #1229 is the RUNNER half.
docs/integration.md§ Troubleshooting now separates them for an adopter, so neither diagnosis is reached for by default.