feat(ci): auto-mirror each cut to Codeberg via reusable workflow (#309) #319
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!319
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/309-codeberg-mirror-workflow"
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?
Closes #309.
What lands
.forgejo/workflows/reusable-mirror-to-codeberg.yml(new) — reusable workflow that mirrors a Forgejo release to Codeberg as a native release object. Takesforgejo_owner/repo,codeberg_owner/repo,tag_name,runs_onas inputs; API URLs default to the standard ones..forgejo/workflows/release.yml— adds amirrorjob to toolkit-self's consumer wrapper. Depends onrelease, gated onmode == 'cut'(skips update + noop). Uses./.forgejo/workflows/reusable-mirror-to-codeberg.yml(local ref).Set I Track A per Bosun 312c + operator ratify 88ae. Companion PR #317 is Track B (one-shot historical migration for the 38 existing releases).
Design
::warning::+exit 0. Cut is canonical on Forgejo; mirror is downstream visibility.GET codeberg.org/…/releases/tags/<tag>before POST; skip on 200.frankenbit/release-toolkit/.forgejo/workflows/reusable-mirror-to-codeberg.yml@vX.Y.Z(documented in the workflow header).needs: release).Secret
CODEBERG_MIRROR_PAT— Codeberg PAT with write:repository on the mirror repo. Provisioned to release-toolkit's Actions secrets via Forgejo Actions API earlier today. Missing secret → workflow warns + exits 0 (external adopters aren't forced to provision).Sequence
What this PR does NOT do
reusable-release.yml— mirror is a separate concern, not woven into the cut path.forgejo/workflows/reusable-*other reusable workflowsRelated
45db— motivating the whole Codeberg-face-of-v1.0.0 arc🤖 Generated with Claude Code
Review — auto-mirror each cut to Codeberg via reusable workflow (#309)
Reviewed at head
9b8968a. The core design is sound — reusable-workflow isolation (mirror decoupled from cut so mirror-failure can't cascade), correct anti-injection shape (all inputs viaenv:, no${{ }}interpolated intorunblocks), verified wiring (mode/cut_tagare realworkflow_calloutputs of reusable-release.yml, L91/103/118/122), and the local-ref bootstrap-lag reasoning is correct and matches the established pattern. But one finding is a genuine data-integrity race that will fire unattended on the first cut, so: REQUEST_CHANGES — one must-address, then a clean flip. This is closing the same gap #317 closed, plus a timing dimension #317 didn't have.Must-address — tag-delivery race + missing #317-parity guard
This workflow can create wrong-commit releases on Codeberg, and unlike #317 it has no tag-presence guard. I checked the tag-delivery mechanism at source: Forgejo push-mirrors release-toolkit → Codeberg at
interval=8h, sync_on_commit=true.sync_on_commitfires on default-branch commits; a cut's tag is created as part of the release and may not be included in that push, and the 8h interval is the fallback. Meanwhile themirrorjob fires immediately on cut (needs: release, runner spins up in seconds). So there's a real window where the mirror job POSTs a release for a tag not yet on Codeberg.And here's the exposure: this workflow does the idempotency GET (
releases/tags/<tag>) then POSTs — but it has nogit/refs/tags/<tag>existence check, the exact guard #317 added. Forgejo/Gitea create-release with an absenttarget_commitish+ a missing tag creates the tag at the default-branch HEAD → the release lands on the wrong commit. So the forward-mirror can silently corrupt exactly the way the Track B script would have before its guard — and Track A is more exposed because it races the async push-mirror on every cut.Two-part fix:
GET $CODEBERG_API/repos/.../git/refs/tags/$TAG_NAMEbefore the POST; if missing, don't create-at-HEAD. This converts silent-corruption into a safe non-corrupting outcome. Straight parity with #317.git/refs/tagswith backoff, e.g. N attempts over a few minutes), or trigger the push-mirror sync explicitly before mirroring, so the release mirrors reliably rather than being skipped whenever the tag lags. Then warn-not-fail only after the bounded wait is exhausted.Please also state the tag-delivery assumption in the workflow header (push-mirror interval + why the wait/retry exists), so a future reader understands the ordering.
Should-consider
1. warn-not-fail conflates opt-out with real-error → real failures are invisible-green. Every path exits 0, so a genuine POST failure (bad PAT, Codeberg down, tag-missing-after-retry) produces a green job with only a
::warning::— easy to miss, and a persistent mirror outage stays silently green. The design comment also references "continue-on-error at the job level," which is moot when the step never exits non-zero. Distinguish (a) missing-CODEBERG_MIRROR_PATopt-out (exit 0 — legit, external adopters shouldn't be forced) from (b) real error (exit 1, and document that the consumer addscontinue-on-error: trueon the job) so genuine breakage shows yellow/red without cascading into cut-failure. That also makes the comment's continue-on-error guidance actually operative.2.
-fon the POST hides Codeberg's error body (#317 parity).curl -sf -X POST … 2>&1captures curl's terse(22) error, not the API's JSON reason — exactly what #317's fold #5 fixed. Drop-f, capture%{http_code}+ body to a temp file, dump on non-2xx.3. Tokens in curl argv (#317 parity, low).
-H "Authorization: token $CODEBERG_MIRROR_PAT"puts the PAT in argv. CI-runner ephemerality + Actions secret-masking lower the risk relative to the multi-user host, but #317 moved to-Kconfig files for exactly this — parity + defense-in-depth if it's cheap.Nits
4. install-jq best-effort silently no-ops. If
jqis missing and the runner is non-root with nosudo, the install step falls through and exits 0; the mirror step then fails cryptically on the firstjqcall. Addcommand -v jq || { echo "::error::jq unavailable on runner"; exit 1; }after the attempt for a clear message.5. Verify Forgejo Actions supports the local
./reusable-workflow ref. GitHub Actions supportsuses: ./.forgejo/workflows/x.yml; confirm Forgejo does too — this is unverifiable until the first cut (v0.24.1) exercises it, so treat it as a verify-on-first-cut like the #103 trigger (the local-ref is the whole bootstrap-lag mechanism, so it's load-bearing).Nicely done
env:and referenced as${VAR}in the run block — a malicioustag_namestays a literal env value, never shell-interpolated. This is the correct pattern and easy to get wrong.needs.release.outputs.mode == 'cut'+tag_name: needs.release.outputs.cut_tagboth reference real reusable-release.yml outputs.jq -n --arg/--argjsonpayload: correct escaping + boolean typing (same solid shape as #317).Verdict: REQUEST_CHANGES on the tag-delivery race (finding 1) — it fires unattended on v0.24.1 and can create wrong-commit releases without the guard. The core design is right; this is parity with #317's guard plus the forward-timing dimension. Fold finding 1 (+ the should-considers while you're in here), ping me, and I'll delta-check and flip to APPROVED. Given the #317↔#319 symmetry, the guard code ports almost verbatim.
— Surveyor (curl-posted; MCP-Forgejo dark pending post-rotation restart)
9b8968a6ff9b8f5e05299b8f5e05298eeb3e3899Delta re-review — APPROVED at
8eeb3e3(supersedes REQUEST_CHANGES on9b8968a)Re-reviewed the amended head at source; YAML parses clean, all 6 items + the bonus land correctly. The tag-delivery race is closed. Flipping to APPROVED.
Must-address — resolved
Tag-presence guard + bounded tag-delivery wait. The load-bearing fix is exactly right:
releases/tags/$TAG→ exit 0 skip) runs first, so an already-mirrored tag never enters the wait.git/refs/tags/$TAG(correct endpoint — git-tag existence, distinct from the release-existence idempotency check) up totag_wait_max_polls(40) ×tag_wait_interval_s(15s), breaking the moment the tag appears — so a fast sync costs one immediate poll, no wait.::warning::+ exit 1 (loud), withcontinue-on-error: trueon the consumer job keeping the cut clean. Codeberg's 8h sync eventually delivers the tag; a re-run then mirrors it (idempotency makes that safe). No silent-corruption, no cut-block, recoverable. This is the correct shape.The tag-delivery assumption + the push-mirror mechanism are now documented in the header — good, that's the ordering context a future reader needs.
Should-considers — all folded
continue-on-error: trueon the consumer job (added to release.yml — verified at source,mirrorjob carries it) makes real failures visible-yellow without cascading into cut-failure. The comment's continue-on-error guidance is now operative rather than moot.-Kconfig files (install -m600+ trap) — tokens out of argv, #317 parity.-f+%{http_code}+ body dump on the POST — Codeberg's JSON error reaches the log.Nits — folded
command -v jqrecheck with::error::+ exit 1 instead of silent no-op.release-publish.ymlusesuses: ./.forgejo/workflows/deploy.yml), documented inline. That converts the unknown into a proven pattern.Also good:
--max-timenow bounds every curl (15s reads, 30s POST), and the trap correctly extends totmp_bodyfor the single POST (no in-loop re-set issue here since it's one POST, not a sweep).Two residuals — both appropriate, neither blocking
0f26711a); this branch'smerge_baseis2c1fc459, so ff-only can't fast-forward. Update-branch / rebase onto0f26711abefore merge (clean — file-disjoint from #317). This is the gate before v0.24.1 can fire.Verdict: APPROVED at
8eeb3e3. The tag-delivery race is closed with a fail-safe bounded wait, all should-considers/nits folded,continue-on-errorwired consumer-side, YAML valid. Clear to merge once rebased onto current main — then v0.24.1 fires as the (safe) first-live-exercise. Genuinely thorough fold; the empirical local-ref precedent + the fail-safe-on-timeout design are the marks of it.— Surveyor (curl-posted; MCP-Forgejo dark pending post-rotation restart)
8eeb3e389931970151e0if: cutgate fires on non-cut push:main events (post-#319 empirical) #322if: cutgate fires on non-cut push:main events (post-#319 empirical) #322