CD-driven version-tag mirror + per-release cleanup (supersedes #8) #9
Reference in New Issue
Block a user
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?
Motivation
Two coupled problems this addresses:
vh/skaldsong,vh/Worldtree, etc. pushes a new SHA-pinned image; they accumulate indefinitely. Snapshot 2026-05-23: ana-docker at 89% disk, 96 GB of reclaimable gitea registry blobs. Manually pruned (~113 GB recovered) but the pattern recurs.:<40-char-sha>tag — there are zero:v0.27.0-style tags in the registry. The registry cannot distinguish "release worth preserving" from "ephemeral SHA churn." Issue #8 proposed gitea-side cleanup rules with a keep-pattern for^v[0-9]+\.[0-9]+\.[0-9]+$, but the inputs that pattern matches don't exist yet.Why supersede #8
Issue #8 attacked symptom 1 (disk pressure) with per-owner cleanup rules and assumed version tags would be available as the protected set. With versioned tags absent from the registry, those rules can't do their intended job — they'd protect
latestand nothing else, deleting everything between releases. We need to fix the upstream tagging problem AND introduce offsite durability for releases. The CD-driven model below does both atomically, replacing the gitea-side rules entirely.Proposed shape: CD does the work on every release
When CI builds a commit that has a semver git tag, the same pipeline:
:<sha>+:v<version>+:latestin one buildx invocation.crane copy gitea.phasefinal.com/vh/<repo>:v<x> $MIRROR/<repo>:v<x>.:v*,:latest, and the current:<sha>; delete the rest via gitea'sDELETE /api/v1/packages/{owner}/container/{name}/{version}API.Non-versioned pushes (regular
maincommits) still publish:<sha>for CD's pull-and-recreate, but skip steps 2-3. Accumulation between releases is bounded by release cadence.Workflow sketch (each consumer repo's
.gitea/workflows/deploy.yaml):Sketch — real version needs pagination, retries, mirror-down handling.
DRY: reusable workflow
To avoid copying the 30-line block into N consumer repos, factor as a reusable workflow at
vh/ci-shared/.gitea/workflows/release-mirror.yamlthat each consumer repouses:from its own CI. One source of truth; one place to fix bugs / add features.Trade-offs vs scheduled cron approach
For the operator's fleet (4-6 Bifrost-consumer repos, all theirs, all on Gitea Actions), CD-driven is the cleaner shape.
Open questions
Offsite mirror target. Lean: Distribution registry (the OCI reference) on nh3-docker, NFS-backed to nh3-nas. Lightweight, OCI-compliant,
crane/dockerpush works natively. Alternatives: second Gitea instance (heavyweight),ghcr.io(free for public, $ for private), Cloudflare R2, ORcrane pullto OCI tarballs on nh3-nas (no registry — restorability needscrane pushfrom tarball later).Mirror-side cleanup: do versioned tags on the mirror live forever or also age out? "Preserve every versioned commit" implies forever. ~200 MB/image × ~weekly releases × N repos = ~10 GB/year/repo. Trivial for SAN-class storage.
Mirror auth: per-repo tokens (better blast radius) vs shared
MIRROR_TOKENsecret (single rotation point). Likely shared for v1, per-repo if a compromise scare materializes.Mirror push failures and CD coupling: should mirror+prune block the deploy step, or run in a
needs: deploypost-deploy job so a flaky deploy doesn't leave a half-state? Lean: post-deploy job (atomicity per release-as-a-whole, not per-step).Per-repo gitea token scope for the prune step: needs
write:packageat minimum (the existing tea token in this repo only haswrite:misc,write:notification,write:organization,write:issue,write:repository,write:user— no package scope). Mint a CI-only token with justread:package,write:packagescope.Sequencing
stacks/registry-mirror/. Infra-ops work.vh/ci-shared/.gitea/workflows/release-mirror.yaml. Could be infra-ops or skaldsong-dev — neutral.if: startsWith(github.ref, 'refs/tags/v')block invoking the shared workflow. One small PR per repo, owned by that repo's dev.GITEA_TOKENsecret to each consumer repo's Gitea Actions config.Acceptance
:v<x>+:latest+:<sha>in gitea;:v<x>in the mirror; all prior non-protected tags for that repo removed from gitea.Related