chore(distribution): cut on alcatraz, push-mirror to gitea.com for public adoption #1021

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

Release cuts stay on alcatraz and gitea.com gets a public push-mirror for adoption and demos. This tracker owns that topology so it is not decided ad hoc during the first mirror push.

Shape

  • Cuts run here, unchanged. The mirror is a push target only, exactly like the existing GitHub mirror.
  • Pull-mirroring is impossible. git.frankenbit.de is network-private, so gitea.com cannot reach it. Outbound push only.
  • The mirror must never run its own release CI. A cut pushes a manifest commit to the default branch; a mirror sync would overwrite it. Our workflows being .forgejo/-only makes this inert on Gitea today — that accident is currently load-bearing and should be made deliberate rather than relied on.
  • The demo consumer is a SEPARATE repo on gitea.com, not the mirror — the shape the Codeberg tic-tac-toe demo had.

Open questions

  • Push mirrors carry refs, so tags should arrive. Forgejo release objects and their assets are not known to mirror — unverified, and it would fail silent: an adopter pinning @vX.Y.Z resolves the tag, gets no binary, and sees no signal that anything is missing. Adjacent case the same day — v0.54.1 published its binary assets but never built its container image, so a consumer pinning it for the docker action got nothing. Same shape, one hop out. (@bosun)
  • GitHub as an additional mirror: Forgejo names GitHub as a supported uses: source and GitHub runners are free for public repos. Same directory question as #1020.

Acceptance criteria

  • FIRST — measure whether release objects and their assets reach a push-mirror at all. This is the one that fails silent, so it must not be discovered by an adopter

    They do not — 0 of 87. Measured 2026-08-28, both ends:

    ours    tags 105   releases 87
    gitea   tags 105   releases  0
    control v0.55.0 is a TAG on gitea; its release object 404s
    

    The control is what makes this a measurement rather than an absence: the same ref is present as git data and absent as an API object, so the gap is the kind of object, not the sync.

  • If they do not: decision recorded on how assets reach gitea.com, or an explicit note to adopters that they will not — DEFERRED → #1030, which owns copying releases and assets

    Both halves are satisfied in the interim, deliberately. #1030 is the decision and it is blocked on a gitea.com PAT that does not exist (the push mirror needs none — ssh, Forgejo-generated key; every release-creation call does), so it may sit. Meanwhile #1054 puts the explicit note to adopters in the README, where the silent failure is actually met.

  • Mirror configured, verified to carry the tags consumers pin

    Configured already (ssh://gitea.com/FrankenBit/release-toolkit.git, 8h, last sync 19:20), so this was discharged by verification rather than setup.

    Verified by name AND SHA, and the SHA half is the load-bearing one: 0 missing, 0 extra, 0 same-name-different-SHA. A tag that resolves to a different commit is worse than a missing one — it fails a consumer silently while looking correct — and a bulk count of 105 = 105 cannot see that class at all. The three pins our docs name (v0.55.0, v0.25.0, v0.10.0) each match on both sides.

  • Documented that a cut never originates on a mirror — #1054

    In the README, plus an arm refusing .gitea/ or .github/ workflow directories. Gitea Actions reads those and not .forgejo/, which is the only reason the mirror does not run our release CI — verified on the mirror: .forgejo 200, .gitea 404, .github 404. The arm turns that directory-naming accident into a stated guarantee, and is a refusal to let those dirs appear without the mirror question being answered, not a ban.

Anchor

Operator, 2026-08-28: "ideally we would still cut them here on Alcatraz and mirror them to gitea... Otherwise this will end up in a maintenance nightmare."

Release cuts stay on alcatraz and gitea.com gets a public push-mirror for adoption and demos. This tracker owns that topology so it is not decided ad hoc during the first mirror push. ## Shape - **Cuts run here, unchanged.** The mirror is a push target only, exactly like the existing GitHub mirror. - **Pull-mirroring is impossible.** `git.frankenbit.de` is network-private, so gitea.com cannot reach it. Outbound push only. - **The mirror must never run its own release CI.** A cut pushes a manifest commit to the default branch; a mirror sync would overwrite it. Our workflows being `.forgejo/`-only makes this inert on Gitea today — that accident is currently load-bearing and should be made deliberate rather than relied on. - **The demo consumer is a SEPARATE repo** on gitea.com, not the mirror — the shape the Codeberg tic-tac-toe demo had. ## Open questions - Push mirrors carry refs, so tags should arrive. Forgejo **release objects and their assets are not known to mirror** — unverified, and it would fail **silent**: an adopter pinning `@vX.Y.Z` resolves the tag, gets no binary, and sees no signal that anything is missing. Adjacent case the same day — `v0.54.1` published its binary assets but never built its container image, so a consumer pinning it for the docker action got nothing. Same shape, one hop out. (@bosun) - GitHub as an additional mirror: Forgejo names GitHub as a supported `uses:` source and GitHub runners are free for public repos. Same directory question as #1020. ## Acceptance criteria - [x] **FIRST** — measure whether release objects and their assets reach a push-mirror at all. This is the one that fails silent, so it must not be discovered by an adopter **They do not — 0 of 87.** Measured 2026-08-28, both ends: ``` ours tags 105 releases 87 gitea tags 105 releases 0 control v0.55.0 is a TAG on gitea; its release object 404s ``` The control is what makes this a measurement rather than an absence: the same ref is present as git data and absent as an API object, so the gap is the *kind* of object, not the sync. - [x] If they do not: decision recorded on how assets reach gitea.com, or an explicit note to adopters that they will not — **DEFERRED → #1030**, which owns copying releases and assets **Both halves are satisfied in the interim, deliberately.** #1030 is the decision and it is **blocked on a gitea.com PAT that does not exist** (the push mirror needs none — ssh, Forgejo-generated key; every release-creation call does), so it may sit. Meanwhile `#1054` puts the explicit note to adopters in the README, where the silent failure is actually met. - [x] Mirror configured, verified to carry the tags consumers pin Configured already (`ssh://gitea.com/FrankenBit/release-toolkit.git`, 8h, last sync 19:20), so this was discharged by verification rather than setup. **Verified by name AND SHA, and the SHA half is the load-bearing one:** 0 missing, 0 extra, **0 same-name-different-SHA**. A tag that resolves to a *different* commit is worse than a missing one — it fails a consumer silently while looking correct — and a bulk count of 105 = 105 cannot see that class at all. The three pins our docs name (`v0.55.0`, `v0.25.0`, `v0.10.0`) each match on both sides. - [x] Documented that a cut never originates on a mirror — `#1054` In the README, plus an arm refusing `.gitea/` or `.github/` workflow directories. Gitea Actions reads those and not `.forgejo/`, which is the only reason the mirror does not run our release CI — verified **on the mirror**: `.forgejo` 200, `.gitea` 404, `.github` 404. The arm turns that directory-naming accident into a stated guarantee, and is a refusal to let those dirs appear *without* the mirror question being answered, not a ban. ## Anchor Operator, 2026-08-28: "ideally we would still cut them here on Alcatraz and mirror them to gitea... Otherwise this will end up in a maintenance nightmare."
Owner

🔴 MEASURED: three answers, and the one that mattered is NO

does the repo mirror?   YES   main HEAD ccc0e13b both ends, identical
do the tags arrive?     YES   50 on ours, 50 on gitea.com, SHAs match
do the assets arrive?   NO    50 releases on ours, ZERO on gitea.com

The control that makes this a finding rather than an absence — same ref, same repo, read one after the other:

gitea.com  GET /releases/tags/v0.54.2   404
gitea.com  GET /tags/v0.54.2            200  -> 589a738e
ours       GET /releases/tags/v0.54.2   200  assets=2
ours       tag v0.54.2                       589a738e   SAME COMMIT

The tag is present and the release is absent at the same commit. So this is releases specifically, not a sync that half-failed. Spot-checked v0.54.1, v0.53.0, v0.49.0 — same shape. Zero of fifty.

⚠️ It is worse than the v0.54.1 defect, one hop out

On v0.54.1 the digest was visibly zeroed — broken in a way a reader could see. Here an adopter gets a repo that looks complete: 50 tags, correct commits, git clone works, checkout v0.54.2 works. The binary is simply not there, no release page says so, and nothing on either end reports an error.

last_error   EMPTY
last_update  12 minutes old

By every signal our side offers, the mirror is healthy — and it is. A push mirror pushes git. Release objects are Forgejo API objects, so they were never in scope for the mechanism.

🔑 The far-end check is what caught it. Our side reports "synced" and is telling the truth about what it synced. Same shape as rt#1022 closing on artifacts rather than on a green run.

What the mirror actually buys — 1 of 3 consumption paths

reusable workflows (7)   need only git data          ✅ MIRRORED
composite / bootstrap    scripts/fetch-rt.sh pulls the released binary
                         and checksum-verifies it     ❌ NEEDS ASSETS
docker action            action.yml pins an image by digest in OUR registry
                         ❌ NEEDS THE REGISTRY, and nothing mirrors images either

So a gitea.com adopter can reference our reusable workflows and cannot use either binary path.

Decision required — not attempted, and it is not a config flip

  • Accept and document — the mirror is source-only; binaries come from git.frankenbit.de. Cheapest, and honest, but an adopter who cannot reach our forge gets one path of three.
  • Add a post-cut CI step creating the gitea.com release and uploading assets via its API. Real work, needs a credential with release-write on the far end, and a failure there must not fail the cut.
  • Drop the mirror if source-only does not serve the adoption purpose it was filed for.

⚠️ Two things NOT measured, flagged rather than assumed: whether an adopter's uses: against gitea.com actually resolves — the question one layer up from assets — and whether the container image mirrors anywhere. v0.54.2 has one.

📌 Measured by @quartermaster, who captured the far-end baseline BEFORE touching anything — the mirror turned out to be already configured, so without that baseline he could not have separated what the mirror delivered from what was already there. Credential handled via Vault to a 0600 file and never placed in argv.

## 🔴 MEASURED: three answers, and the one that mattered is NO ``` does the repo mirror? YES main HEAD ccc0e13b both ends, identical do the tags arrive? YES 50 on ours, 50 on gitea.com, SHAs match do the assets arrive? NO 50 releases on ours, ZERO on gitea.com ``` **The control that makes this a finding rather than an absence — same ref, same repo, read one after the other:** ``` gitea.com GET /releases/tags/v0.54.2 404 gitea.com GET /tags/v0.54.2 200 -> 589a738e ours GET /releases/tags/v0.54.2 200 assets=2 ours tag v0.54.2 589a738e SAME COMMIT ``` **The tag is present and the release is absent at the same commit.** *So this is releases specifically, not a sync that half-failed.* Spot-checked `v0.54.1`, `v0.53.0`, `v0.49.0` — same shape. **Zero of fifty.** ## ⚠️ It is worse than the v0.54.1 defect, one hop out On `v0.54.1` the digest was **visibly zeroed** — broken in a way a reader could see. Here an adopter gets a repo that **looks complete**: 50 tags, correct commits, `git clone` works, `checkout v0.54.2` works. **The binary is simply not there, no release page says so, and nothing on either end reports an error.** ``` last_error EMPTY last_update 12 minutes old ``` **By every signal our side offers, the mirror is healthy — and it is.** *A push mirror pushes git. Release objects are Forgejo API objects, so they were never in scope for the mechanism.* 🔑 **The far-end check is what caught it.** Our side reports "synced" and is telling the truth about what it synced. *Same shape as `rt#1022` closing on artifacts rather than on a green run.* ## What the mirror actually buys — 1 of 3 consumption paths ``` reusable workflows (7) need only git data ✅ MIRRORED composite / bootstrap scripts/fetch-rt.sh pulls the released binary and checksum-verifies it ❌ NEEDS ASSETS docker action action.yml pins an image by digest in OUR registry ❌ NEEDS THE REGISTRY, and nothing mirrors images either ``` **So a gitea.com adopter can reference our reusable workflows and cannot use either binary path.** ## Decision required — not attempted, and it is not a config flip - **Accept and document** — the mirror is source-only; binaries come from `git.frankenbit.de`. Cheapest, and honest, but an adopter who cannot reach our forge gets one path of three. - **Add a post-cut CI step** creating the gitea.com release and uploading assets via its API. Real work, needs a credential with release-write on the far end, and a failure there must not fail the cut. - **Drop the mirror** if source-only does not serve the adoption purpose it was filed for. ⚠️ **Two things NOT measured**, flagged rather than assumed: whether an adopter's `uses:` against gitea.com actually resolves — *the question one layer up from assets* — and whether the container image mirrors anywhere. `v0.54.2` has one. 📌 Measured by **@quartermaster**, who captured the far-end baseline BEFORE touching anything — *the mirror turned out to be already configured, so without that baseline he could not have separated what the mirror delivered from what was already there.* Credential handled via Vault to a `0600` file and never placed in argv.
Owner

🔴 CORRECTION TO MY OWN "1 of 3 paths" — it is ZERO for an external adopter

I wrote that a gitea.com adopter can reference our reusable workflows and only loses the binary paths. That is wrong, and the reusable is not exempt:

reusable-release.yml:266   - name: bootstrap rt
                             uses: ./.release-toolkit/composite/bootstrap-rt
scripts/fetch-rt.sh:47     RT_DOWNLOAD_BASE="${RT_DOWNLOAD_BASE:-https://git.frankenbit.de}"
                    :66    rel="${RT_DOWNLOAD_BASE}/${RT_REPO}/releases/download/${RT_VERSION}"

The reusable workflow bootstraps rt from OUR forge. So referencing it from gitea.com resolves the workflow and then fails fetching the binary. RT_DOWNLOAD_BASE is overridable, which is the seam — but it points somewhere that also has to hold the assets.

git.frankenbit.de is LAN-only at the ingressallow 172.22/16 · 192.168.178/24 · 10.10.10/24 · deny all, verified against 7 days of logs. An external adopter cannot reach it at all.

So a gitea.com release whose body links back to ours is broken for precisely the audience the mirror exists for. The assets have to be COPIED. That is not a preference; it is the only shape that works.

The machinery mostly exists, and is misnamed for a forge we have left

mirror-on-cut.yml              on: release:[published] -> calls the reusable
reusable-mirror-to-codeberg    reads OUR release, idempotent GET-before-POST,
                               POSTs {tag_name, name, body, prerelease, draft}

Both API URLs are declared INPUTS; codeberg_api_url merely defaults to codeberg.org. Repointing is a with: change, not new code. Codeberg was dropped weeks ago over their AI-code policy; the Codeberg repo now 404s and the job has not fired since ~task 24207 — it did not run for v0.54.2 despite the trigger being wired.

⚠️ But repointing alone would NOT fix assets. That workflow POSTs the release object and never uploads one — the only upload-shaped matches in it are trap rm -f cleanup lines. An adopter would get a release PAGE with no binary, which is arguably worse than none: a populated page reads as complete.

Sizing, because "backfill 50 releases" is a different proposition at 3 GiB

74 assets across 50 releases · 339 MiB total
v0.54.2: rt-linux-amd64 9.97 MB + checksums.txt 81 bytes
⚠️ 14 of 50 releases have NO assets — 28% — so backfill must tolerate an
   empty asset set rather than treating it as failure

Scope, if this is taken

  • Generalise the reusable: target_api_url / target_owner / target_repo. Forge-agnostic in fact, so make it so in name.
  • Asset upload — the only genuinely new code. Download from ours, POST to the target's /releases/{id}/assets. Verify the uploaded SIZE rather than trusting a 201; handle partial uploads and re-runs.
  • Backfill via workflow_dispatch iterating tags — nearly free, and not by luck: the existing GET-before-POST idempotency makes re-running safe by construction.
  • Keep the decoupling. The current design deliberately puts mirror failure outside the cut's job graph — "mirror is downstream visibility, not source-of-record". Asset upload adds a failure surface on every cut and that decision should survive it.

📌 checksums.txt matters more than its 81 bytes suggest: an adopter two hops from the build has no other way to verify what they pulled. It is the whole provenance story.

📌 Existing-machinery discovery, the LAN-only foreclosure, the sizing and the empty-asset caveat by @quartermaster; the reusable-also-needs-the-binary correction by @bosun, against his own earlier framing.

## 🔴 CORRECTION TO MY OWN "1 of 3 paths" — it is ZERO for an external adopter I wrote that a gitea.com adopter can reference our reusable workflows and only loses the binary paths. **That is wrong, and the reusable is not exempt:** ``` reusable-release.yml:266 - name: bootstrap rt uses: ./.release-toolkit/composite/bootstrap-rt scripts/fetch-rt.sh:47 RT_DOWNLOAD_BASE="${RT_DOWNLOAD_BASE:-https://git.frankenbit.de}" :66 rel="${RT_DOWNLOAD_BASE}/${RT_REPO}/releases/download/${RT_VERSION}" ``` **The reusable workflow bootstraps `rt` from OUR forge.** So referencing it from gitea.com resolves the workflow and then fails fetching the binary. *`RT_DOWNLOAD_BASE` is overridable, which is the seam — but it points somewhere that also has to hold the assets.* ## 🔑 And the link-instead-of-copy design is FORECLOSED, which settles the shape `git.frankenbit.de` is **LAN-only at the ingress** — `allow 172.22/16 · 192.168.178/24 · 10.10.10/24 · deny all`, verified against 7 days of logs. **An external adopter cannot reach it at all.** > **So a gitea.com release whose body links back to ours is broken for precisely the audience the mirror exists for. The assets have to be COPIED. That is not a preference; it is the only shape that works.** ## ✅ The machinery mostly exists, and is misnamed for a forge we have left ``` mirror-on-cut.yml on: release:[published] -> calls the reusable reusable-mirror-to-codeberg reads OUR release, idempotent GET-before-POST, POSTs {tag_name, name, body, prerelease, draft} ``` **Both API URLs are declared INPUTS**; `codeberg_api_url` merely *defaults* to codeberg.org. **Repointing is a `with:` change, not new code.** *Codeberg was dropped weeks ago over their AI-code policy; the Codeberg repo now 404s and the job has not fired since ~task 24207 — it did not run for `v0.54.2` despite the trigger being wired.* ⚠️ **But repointing alone would NOT fix assets.** That workflow POSTs the release **object** and never uploads one — the only upload-shaped matches in it are `trap rm -f` cleanup lines. **An adopter would get a release PAGE with no binary, which is arguably worse than none: a populated page reads as complete.** ## Sizing, because "backfill 50 releases" is a different proposition at 3 GiB ``` 74 assets across 50 releases · 339 MiB total v0.54.2: rt-linux-amd64 9.97 MB + checksums.txt 81 bytes ⚠️ 14 of 50 releases have NO assets — 28% — so backfill must tolerate an empty asset set rather than treating it as failure ``` ## Scope, if this is taken - [ ] Generalise the reusable: `target_api_url` / `target_owner` / `target_repo`. **Forge-agnostic in fact, so make it so in name.** - [ ] **Asset upload — the only genuinely new code.** Download from ours, POST to the target's `/releases/{id}/assets`. **Verify the uploaded SIZE rather than trusting a 201**; handle partial uploads and re-runs. - [ ] Backfill via `workflow_dispatch` iterating tags — **nearly free, and not by luck: the existing GET-before-POST idempotency makes re-running safe by construction.** - [ ] **Keep the decoupling.** The current design deliberately puts mirror failure outside the cut's job graph — *"mirror is downstream visibility, not source-of-record"*. Asset upload adds a failure surface on every cut and that decision should survive it. 📌 **`checksums.txt` matters more than its 81 bytes suggest:** an adopter two hops from the build has no other way to verify what they pulled. *It is the whole provenance story.* 📌 Existing-machinery discovery, the LAN-only foreclosure, the sizing and the empty-asset caveat by **@quartermaster**; the reusable-also-needs-the-binary correction by **@bosun**, against his own earlier framing.
Sign in to join this conversation.
No project
No assignees
2 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#1021
No description provided.