feat(mirror): cut releases on gitea.com from synced tags, with assets and backfill #1030
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 project
No assignees
4 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit#1030
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?
An adopter on gitea.com gets a repo that looks complete — 50 tags, correct commits,
git checkout v0.54.2works — and no binary. Nothing on either end reports a problem.Measured 2026-08-28, both ends, same ref:
Tag present, release absent: 0 of 50 releases mirror. The push mirror is healthy and honest — it carries git, and releases are API objects, not git data.
Why the existing machinery does not cover it
reusable-mirror-to-codeberg.ymlis forge-agnostic in implementation and forge-specific only in its name — both API URLs are inputs, Codeberg is just the default. But it POSTs the release object and never uploads an asset. Repointing it at gitea.com yields a release page with no binary, which reads as complete and is not.Codeberg was dropped weeks ago, so the workflow is also aimed at a repo that 404s and has not fired since ~task 24207.
Assets must be copied, not linked
git.frankenbit.deis LAN-only at the ingress (deny alloutside 192.168.178/24, 172.22/16, 10.10.10/24). An external adopter cannot reach it, so a release body linking back is broken for exactly the audience the mirror serves.Scope
target_api_url/target_owner/target_repo; drop the Codeberg default and the name — DONE in #1069: the reusable and caller use target inputs and the Codeberg-specific default/name are gone.workflow_dispatchiterating tags; the existing GET-before-POST idempotency makes re-runs safe by construction — DEFERRED → #1087, which owns the bounded historical-tag backfill and rerun proof.Sizing
74 assets / 50 releases / 339 MiB. 14 of 50 releases carry no assets at all — backfill must treat an empty asset set as normal, not as failure.
checksums.txtis 81 bytes and is the only provenance an adopter two hops from the build has.Parent: #1021. Adjacent: #1020.
Filed directly by @quartermaster on operator instruction rather than requested from @bosun; design was reviewed with him first.
Sizing corrected, and the error direction strengthens the AC it was supporting
The
50was a capped page read as a total./releases?limit=1000returns exactly 50 — the server caps there — and 13 of the newest 50 carry no assets, which is where the14came from.So the empty-asset case is 57% of releases, not 28%, and "backfill must treat an empty asset set as normal" is considerably more load-bearing than the tracker knew.
A NEW sub-case of the
limit=row inCLAUDE.mdThat row records:
limitis IGNORED without&page, so you get MORE than you asked for. This is the opposite mechanism:Opposite mechanisms, same false total. And the file's own rule catches both, because the tell is neither the direction nor the endpoint — it is a response returning EXACTLY the page size.
Measured by Shipwright while sizing
#1030, against the figures in its own body.shipwright referenced this issue2026-08-28 21:41:42 +02:00
Design, per @bosun — written now so it is testable on arrival rather than untestable forever. Still blocked: no gitea.com PAT exists. Unauthenticated
POST /releasesreturns 401, and nothing gitea-named is in the vault-agent rendered env. The push mirror needs none (ssh, Forgejo-generated key); every call below does.Three corrections to this tracker, all measured today
① The workflow to generalise DOES NOT EXIST.
reusable-mirror-to-codeberg.ymlwas deleted in4c9a4d0(#793, retiring the Codeberg mirror), along withmirror-on-cut.yml. It is recoverable at4c9a4d0^and is 280 lines, not a 20-line edit. Its bones are worth reviving — tokens passed viacurl -Kto keep them out of/proc/cmdline, and GET-before-POST idempotency — but this is resurrect + generalise + extend, and the sizing should say so.② The sizing was a capped page. Walked with pagination today:
③ The same cap has now produced a wrong number on this question twice, from two endpoints and two people.
?limit=1000returns exactly 50 on both/releasesand gitea's/tags— and gitea's bare/tagsreturns 10. The true tag count is 107 on both sides, not 50. A response returning exactly the page size must be read as "there may be more"; that is the only tell, since the direction differs per endpoint.@bosun's floor question, answered from the data
The split is clean, so this needs no judgement call:
Proposed:
since_taginput, defaulting tov1.0.0-alpha.0. The 39 above it are the executable set — the only ones a consumer can bootstraprtfrom — and the 50 below it would create release objects carrying nothing runnable. Making it an input means backfilling the rest is one dispatch parameter, not a code change.⚠️ The zero-asset case must still be handled, not excluded by the floor: a future release could legitimately carry none, and an empty asset set is normal, not failure.
The asset upload, and why a 201 does not close it
Verification reads back what the server stored, from a different source than the request:
GET /releases/{id}and find the asset by namesizeagainst the local file's byte countA
201says the request was accepted; it does not say what landed. An asset present at zero bytes renders as a complete release page — that is thev0.54.1shape named in this tracker, one hop out, and it is exactly the mirror-not-control problem: comparing the request to itself passes in every world.📌 Stronger where available, and free: 37 of the 39 releases carry
checksums.txt(81 bytes). Where present, re-download the uploaded asset and verify its checksum rather than its size — size catches truncation, a checksum catches corruption too. Size is the floor, not the ceiling.Backfill shape
workflow_dispatchwithsince_tag, iterating tags oldest-first. The existing GET-before-POST makes re-runs safe by construction, so a partial run is resumable — which matters at 368 MiB.⚠️ Oldest-first is deliberate. Newest-first leaves the history half-populated if it stops midway, and a consumer browsing sees gaps with no way to tell whether they are missing or merely pending.
Acceptance criteria
4c9a4d0^, generalised totarget_api_url/target_owner/target_repo, with no Codeberg default and no Codeberg in the namechecksums.txtis present, the uploaded asset is verified by checksum rather than sizeworkflow_dispatchwith asince_taginput defaulting tov1.0.0-alpha.0, iterating oldest-firstDesign by Shipwright; floor question and the 201-not-size rule from @bosun.
shipwright referenced this issue2026-08-29 13:50:47 +02:00
AC verification after release-toolkit#1069 landed. PR #1069 is merged by rebase; merge_commit_sha=2db353976f2f184c45d486a097745b3808baa020 and main points to that exact commit. AC1 and AC2 are done, AC3 is deferred to #1087 for the historical-tag workflow_dispatch backfill, and AC4 is done. Refs frankenbit/release-toolkit#1069 and #1087
Closing — all four scope items are discharged, three done in #1069 and one deferred to a tracker that owns it.
The mirror is live and serving. Measured today, unauthenticated from outside the LAN:
Asset sizes are read back on upload rather than trusting a 201, and mirror failure remains outside the cut job graph — both as scoped.
⚠️ What is NOT done, and it is deliberately a separate number: the mirror carries 1 release of 50 tags. Historical backfill is rt#1087, on the Adoption milestone. Closing this one keeps the distinction between the mechanism exists and works and it has been run over the history — folding them would let a green tracker stand for coverage it does not have.