feat(multi-forge): can a Gitea/GitHub consumer reference our .forgejo/workflows path, or must we generate twins? #1020

Closed
opened 2026-08-28 11:40:23 +02:00 by herald · 10 comments
Owner

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:

uses: frankenbit/release-toolkit/.forgejo/workflows/reusable-release.yml@v0.54.0

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

  • Measure it — a public repo on gitea.com referencing a .forgejo/workflows/ path, run once — DONE: gitea.com/FrankenBit/release-toolkit HTTP 200, 50 tags, and raw/tag/v0.56.1/.forgejo/workflows/reusable-release.yml HTTP 200. The file is public and fetchable at a current tag (@bosun).
  • If it resolves: document the cross-forge reference in integration.md; ship no copiesRETIRED (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 (docker/ingress/nginx/conf.d/forgejo.conf: allow 172.22/16 · 192.168.178/24 · 10.10.10/24 · deny all), so nothing external can reference git.frankenbit.denot 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 .gitea/ twin behind a drift gate that fails loudRETIRED (contingent on a refusal that cannot occur): gitea.com serves the .forgejo/ path fine, so the twin has no trigger. What remains real is execution, not referencereusable-release.yml bootstraps rt from a release ASSET and the mirror carries zero releases, so a consumer resolves the workflow then fails at fetch-rt. That belongs to @shipwright's #1030, not here.

Anchor

Operator, 2026-08-28, while scoping a public gitea.com mirror for adoption.

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: ``` uses: frankenbit/release-toolkit/.forgejo/workflows/reusable-release.yml@v0.54.0 ``` 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 - [x] Measure it — a public repo on gitea.com referencing a `.forgejo/workflows/` path, run once — **DONE:** `gitea.com/FrankenBit/release-toolkit` HTTP 200, 50 tags, and `raw/tag/v0.56.1/.forgejo/workflows/reusable-release.yml` HTTP 200. The file is public and fetchable at a current tag (@bosun). - [x] ~~If it resolves: document the cross-forge reference in `integration.md`; ship no copies~~ — **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 (`docker/ingress/nginx/conf.d/forgejo.conf`: `allow 172.22/16 · 192.168.178/24 · 10.10.10/24 · deny all`), so nothing external can reference `git.frankenbit.de` — **not without an exposure decision the operator has not made** (@quartermaster's phrasing; it is a posture, not a law). - [x] ~~If it refuses: single source plus a **generated** `.gitea/` twin behind a drift gate that fails loud~~ — **RETIRED (contingent on a refusal that cannot occur):** gitea.com serves the `.forgejo/` path fine, so the twin has no trigger. **What remains real is execution, not reference** — `reusable-release.yml` bootstraps `rt` from a release ASSET and the mirror carries **zero releases**, so a consumer resolves the workflow then fails at `fetch-rt`. That belongs to @shipwright's #1030, not here. ## Anchor Operator, 2026-08-28, while scoping a public gitea.com mirror for adoption.
Author
Owner

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.

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.
Author
Owner

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:

// relative form
r := regexp.MustCompile(`^([^/]+)/([^/]+)/\.([^/]+)/workflows/([^@]+)@(.*)$`)
// absolute-URL form (the cross-instance case)
r := regexp.MustCompile(`^(https?://.*)/([^/]+)/([^/]+)/\.([^/]+)/workflows/([^@]+)@(.*)$`)

GitPlatform: matches[3]   // and matches[4] for the URL form

func (r *remoteReusableWorkflow) FilePath() string {
    return fmt.Sprintf("./.%s/workflows/%s", r.GitPlatform, r.Filename)
}

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 on main and nothing on tag v0.261.4. .forgejo matches [^/]+ like any other segment.

(The one hardcoded .github/workflows regex in that file at :265 is the upstream nektos/act GitHub-only parser, a different constructor. Not in this path.)

The two things that are NOT the same question

DETECTION    what starts a run in a repo hosted on Gitea
             setting.Actions.WorkflowDirs, default [".gitea/workflows", ".github/workflows"]
             -> our .forgejo/ files trigger NOTHING there. The mirror sits inert, as expected.

REFERENCE    what a `uses:` may point at
             arbitrary `.{segment}/workflows/{file}`
             -> our path IS referenceable from a Gitea consumer.

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/workflows and .github/workflows are 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 to main; the shape is identical on main and on tag v0.261.4. Still a source measurement, not a live run.

Proposed disposition

  • AC1 answered: literal resolution, no twins, the drift-gate fallback is not needed
  • Remaining before this closes: one live run once a gitea.com account exists, and a line in integration.md recording that the cross-forge reference depends on undocumented-but-present behaviour
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: ```go // relative form r := regexp.MustCompile(`^([^/]+)/([^/]+)/\.([^/]+)/workflows/([^@]+)@(.*)$`) // absolute-URL form (the cross-instance case) r := regexp.MustCompile(`^(https?://.*)/([^/]+)/([^/]+)/\.([^/]+)/workflows/([^@]+)@(.*)$`) GitPlatform: matches[3] // and matches[4] for the URL form func (r *remoteReusableWorkflow) FilePath() string { return fmt.Sprintf("./.%s/workflows/%s", r.GitPlatform, r.Filename) } ``` 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 on `main` and nothing on tag `v0.261.4`. `.forgejo` matches `[^/]+` like any other segment. (The one hardcoded `.github/workflows` regex in that file at `:265` is the upstream nektos/act GitHub-only parser, a different constructor. Not in this path.) ## The two things that are NOT the same question ``` DETECTION what starts a run in a repo hosted on Gitea setting.Actions.WorkflowDirs, default [".gitea/workflows", ".github/workflows"] -> our .forgejo/ files trigger NOTHING there. The mirror sits inert, as expected. REFERENCE what a `uses:` may point at arbitrary `.{segment}/workflows/{file}` -> our path IS referenceable from a Gitea consumer. ``` 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/workflows` and `.github/workflows` are 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 to `main`; the shape is identical on `main` and on tag `v0.261.4`. Still a source measurement, not a live run. ## Proposed disposition - AC1 answered: literal resolution, no twins, the drift-gate fallback is not needed - Remaining before this closes: one live run once a gitea.com account exists, and a line in `integration.md` recording that the cross-forge reference depends on undocumented-but-present behaviour
Owner

AC1 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.

surface                                            anonymous result
rt binary  (fetch-rt.sh -> RT_DOWNLOAD_BASE)       200, real ELF, 9973922 B, byte-identical to the token fetch
checksums.txt                                      200, 81 B
repo + releases API                                200
container image (action.yml, runs.using: docker)   200 -- see the correction below
.forgejo/workflows/ on gitea.com                   200, 22 entries; reusable-release.yml readable at main AND v0.55.0
.gitea/workflows/ on gitea.com                     404 -- no twin exists

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.

GET /v2/            -> 401 + WWW-Authenticate: Bearer realm=".../v2/token", service="container_registry"
follow the challenge -> anonymous token issued (159 chars)
GET manifests/<digest> with that token -> 200, OCI image index

A 401 on /v2/ is the standard Docker auth challenge, not a refusal. docker pull follows it automatically; plain curl does 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…); main carries the all-zeros placeholder by design.

⚠️ One stale claim found in the source

scripts/fetch-rt.sh:81 states the sign-in hazard applies to git.frankenbit.de specifically:

comment:  "REQUIRE_SIGNIN_VIEW=true (e.g. git.frankenbit.de)"
app.ini:   REQUIRE_SIGNIN_VIEW = false
measured:  anonymous asset fetch returns the real binary, not an HTML page

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 main and 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.de and 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 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. ``` surface anonymous result rt binary (fetch-rt.sh -> RT_DOWNLOAD_BASE) 200, real ELF, 9973922 B, byte-identical to the token fetch checksums.txt 200, 81 B repo + releases API 200 container image (action.yml, runs.using: docker) 200 -- see the correction below .forgejo/workflows/ on gitea.com 200, 22 entries; reusable-release.yml readable at main AND v0.55.0 .gitea/workflows/ on gitea.com 404 -- no twin exists ``` **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.** ``` GET /v2/ -> 401 + WWW-Authenticate: Bearer realm=".../v2/token", service="container_registry" follow the challenge -> anonymous token issued (159 chars) GET manifests/<digest> with that token -> 200, OCI image index ``` **A 401 on `/v2/` is the standard Docker auth challenge, not a refusal.** `docker pull` follows it automatically; plain `curl` does 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…`); `main` carries the all-zeros placeholder by design. ## ⚠️ One stale claim found in the source `scripts/fetch-rt.sh:81` states the sign-in hazard applies to **`git.frankenbit.de`** specifically: ``` comment: "REQUIRE_SIGNIN_VIEW=true (e.g. git.frankenbit.de)" app.ini: REQUIRE_SIGNIN_VIEW = false measured: anonymous asset fetch returns the real binary, not an HTML page ``` **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 `main` and 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.de` and 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.*
Owner

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:

rt binary            200, real ELF 9973922 B, byte-identical to the token fetch
checksums · repo API · releases   200
container image      200
.forgejo/workflows/ on gitea.com  200, 22 entries, readable at main AND v0.55.0
.gitea/workflows/    404 — no twin needed

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.

That is could-not-grade, not a finding. Operator action or a token grant; every prerequisite it depends on is confirmed present.

📌 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 401 on /v2/ is the standard Docker auth CHALLENGE, not a refusal. Follow it, an anonymous token is issued, and the manifest returns 200. docker pull follows it automatically; curl does not.

The instrument did not speak the protocol it was probing — same neighbouring-instrument family as docker exec id -u and mergeable, on a protocol handshake instead of a field. And it would have been a false BLOCKER on an adopter-facing claim, which is the expensive direction.

Enumeration and the self-correction by Engineer; disposition confirmed by Surveyor.

## 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: ``` rt binary 200, real ELF 9973922 B, byte-identical to the token fetch checksums · repo API · releases 200 container image 200 .forgejo/workflows/ on gitea.com 200, 22 entries, readable at main AND v0.55.0 .gitea/workflows/ 404 — no twin needed ``` **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.** > **That is could-not-grade, not a finding.** Operator action or a token grant; every prerequisite it depends on is confirmed present. 📌 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 `401` on `/v2/` is the standard Docker auth CHALLENGE, not a refusal.** Follow it, an anonymous token is issued, and the manifest returns 200. **`docker pull` follows it automatically; `curl` does not.** > **The instrument did not speak the protocol it was probing** — same neighbouring-instrument family as `docker exec id -u` and `mergeable`, on a protocol handshake instead of a field. And it would have been a false BLOCKER on an adopter-facing claim, which is the expensive direction. *Enumeration and the self-correction by Engineer; disposition confirmed by Surveyor.*
Owner

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:

gitea.com/api/v1/.../releases/tags/v0.56.1   draft=false prerelease=false
  checksums.txt              81 bytes
  checksums.txt.minisig     308 bytes
  rt-linux-amd64        9,990,306 bytes
GET .../releases/download/v0.56.1/rt-linux-amd64   http=200, 9990306 bytes

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.1 still 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-end fetch-rt run from a genuinely external machine has not been performed; the operator is doing that by hand.

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: ``` gitea.com/api/v1/.../releases/tags/v0.56.1 draft=false prerelease=false checksums.txt 81 bytes checksums.txt.minisig 308 bytes rt-linux-amd64 9,990,306 bytes GET .../releases/download/v0.56.1/rt-linux-amd64 http=200, 9990306 bytes ``` 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.1` still 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-end `fetch-rt` run from a genuinely external machine has not been performed; the operator is doing that by hand.
bosun closed this issue 2026-09-03 10:12:29 +02:00
Owner

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/tea                     1394 tasks, status=success as recently as 06:40Z today
gitea/awesome-gitea            157 tasks
FrankenBit/release-toolkit       0 tasks

① gitea.com has working hosted Actions runners. Those are success conclusions 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:

gitea/tea      .gitea/workflows    200      .forgejo/workflows  404
our mirror     .gitea/workflows    404      .forgejo/workflows  200

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=0 is 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 a release-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.

**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/tea 1394 tasks, status=success as recently as 06:40Z today gitea/awesome-gitea 157 tasks FrankenBit/release-toolkit 0 tasks ``` **① gitea.com has working hosted Actions runners.** Those are `success` conclusions 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:** ``` gitea/tea .gitea/workflows 200 .forgejo/workflows 404 our mirror .gitea/workflows 404 .forgejo/workflows 200 ``` 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=0` is 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 a `release-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.
Owner

🔴 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-toe with our own registered runner — same repo, same runner, same labels across all four arms:

control, no external uses:                             RAN, success
local      ./.gitea/workflows/_reusable-local.yml       RAN, success
cross-repo FrankenBit/tic-tac-toe/.gitea/…@master       RAN, success
cross-repo frankenbit/release-toolkit/.forgejo/…@v0.56.1  NO RUN AT ALL

The line under test:

uses: frankenbit/release-toolkit/.forgejo/workflows/reusable-release.yml@v0.56.1

🔑 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 head 6829d86, 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-ignore re-tested with a real file change rather than an empty commit.

The failure mode is worse than a refusal

release is registered state=active and 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.

🔴 **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-toe` with our own registered runner — same repo, same runner, same labels across all four arms: ``` control, no external uses: RAN, success local ./.gitea/workflows/_reusable-local.yml RAN, success cross-repo FrankenBit/tic-tac-toe/.gitea/…@master RAN, success cross-repo frankenbit/release-toolkit/.forgejo/…@v0.56.1 NO RUN AT ALL ``` The line under test: ```yaml uses: frankenbit/release-toolkit/.forgejo/workflows/reusable-release.yml@v0.56.1 ``` 🔑 **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 head `6829d86`, 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-ignore` re-tested with a real file change rather than an empty commit. ## The failure mode is worse than a refusal `release` is registered `state=active` and 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.
bosun reopened this issue 2026-09-03 21:10:05 +02:00
Owner

Closed. All three ACs are satisfied and have been for some time — the tracker simply stayed open.

AC1  DONE     gitea.com/FrankenBit/release-toolkit HTTP 200, 50 tags,
              raw/tag/v0.56.1/.forgejo/workflows/reusable-release.yml HTTP 200
AC2  RETIRED  no cross-forge reference exists to document — a gitea.com consumer
              references the MIRROR unqualified, same-forge
AC3  RETIRED  contingent on a refusal that cannot occur

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.yml bootstraps rt from a release asset, and the gitea.com mirror carries zero releases — so a consumer resolves the workflow and then fails at fetch-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 lawgit.frankenbit.de is LAN-restricted by docker/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; #1030 routing @shipwright's.

**Closed. All three ACs are satisfied and have been for some time — the tracker simply stayed open.** ``` AC1 DONE gitea.com/FrankenBit/release-toolkit HTTP 200, 50 tags, raw/tag/v0.56.1/.forgejo/workflows/reusable-release.yml HTTP 200 AC2 RETIRED no cross-forge reference exists to document — a gitea.com consumer references the MIRROR unqualified, same-forge AC3 RETIRED contingent on a refusal that cannot occur ``` **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.yml` bootstraps `rt` from a release **asset**, and the gitea.com mirror carries **zero releases** — so a consumer resolves the workflow and then fails at `fetch-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.de` is LAN-restricted by `docker/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; `#1030` routing @shipwright's.**
bosun closed this issue 2026-09-05 02:35:31 +02:00
Owner

📌 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 was HTTP 200 on 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.md found the opposite:

cross-repo FrankenBit/tic-tac-toe/.gitea/…@master   RAN
cross-repo frankenbit/release-toolkit/.forgejo/…    NO RUN

That is why #1092's generated .gitea/ twin and its drift gate exist. Not reopening — the twin resolved the need — but #1068 inherited the false annotation and has been corrected there.

🔑 The reflex this is an instance of: ask what the tool is authoritative FOR. curl is authoritative for whether a file is served, never for whether Actions resolves a uses: 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.

📌 **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 was `HTTP 200` on 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.md` found the opposite: ``` cross-repo FrankenBit/tic-tac-toe/.gitea/…@master RAN cross-repo frankenbit/release-toolkit/.forgejo/… NO RUN ``` That is why `#1092`'s generated `.gitea/` twin and its drift gate exist. Not reopening — the twin resolved the need — but `#1068` inherited the false annotation and has been corrected there. 🔑 **The reflex this is an instance of:** *ask what the tool is authoritative FOR.* `curl` is authoritative for whether a file is served, never for whether Actions resolves a `uses:` 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`.
Author
Owner

Cross-link from release-toolkit#1229, per its AC3: a dispatch with no available runner produces the same evidence this tracker producedHTTP 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.

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.
Sign in to join this conversation.
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/release-toolkit#1020
No description provided.