From 57c6fddfc363410c579d42bd64d01f5d328e19a2 Mon Sep 17 00:00:00 2001 From: Mix Date: Fri, 9 Oct 2026 16:25:34 +0300 Subject: [PATCH] release: 1.9.5: GHCR release fix: keep per-arch images, sweep orphaned versions (CI only) CI-only cut. The release workflow deleted the X.Y.Z-amd64 / X.Y.Z-arm64 package versions after the manifest merge; those versions are the images the X.Y.Z manifest points at, so pulling 1.3.0 failed with a missing manifest. No product code changes. Fixed: - release.yml: the per-arch tags stay in GHCR. GHCR cannot drop a tag without deleting the image behind it. - release.yml: the merge job now removes only untagged package versions that no tagged manifest references (leftovers of publishing a version again); any failed lookup aborts the step before anything is deleted. - development.md release description and version table, image pin 1.9.5 in compose, README, guide. Full detail: CHANGELOG.md (1.9.5). Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 45 ++++++++++++++++++++--------------- CHANGELOG.md | 13 ++++++++++ README.md | 2 +- deploy/docker-compose.yml | 2 +- docs/development.md | 17 ++++++++----- docs/guide.md | 2 +- 6 files changed, 53 insertions(+), 28 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8c3eba6..0136cd5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,9 +11,9 @@ name: release # own native runner; a merge job then combines them into the one manifest tag # documented in deploy/docker-compose.yml. "test, then push" (not push-by-digest # then test) is deliberate: it means the bytes that get tagged are exactly the -# bytes that passed e2e. Per-arch tags are pushed only so imagetools can -# assemble the multi-arch manifest; merge removes them from GHCR so operators -# see a single version tag. +# bytes that passed e2e. The per-arch tags stay in GHCR next to the version +# tag: they are the images the multi-arch manifest points at, and GHCR cannot +# drop a tag without deleting the image behind it. on: release: types: [published] @@ -177,28 +177,35 @@ jobs: backoff=$((backoff * 2)) done - - name: Remove per-arch tags from GHCR - # Side-effect tags for imagetools assembly only — not part of the public - # version surface (deploy/docker-compose.yml pins X.Y.Z, not X.Y.Z-amd64). - # imagetools has no "rm" subcommand; delete via the GitHub Packages API. + - name: Remove orphaned untagged versions from GHCR + # Re-publishing a version moves its tags to new manifests and leaves the + # old ones behind as untagged "sha256:..." package versions. Delete only + # those that no tagged manifest references: the images behind a + # multi-arch tag are package versions too, and deleting one breaks pulls + # of that tag. Any failed lookup aborts the step before anything is + # deleted. env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail - version="${{ needs.prepare.outputs.version }}" + image="ghcr.io/${{ github.repository }}" owner="${{ github.repository_owner }}" pkg="${{ github.event.repository.name }}" api="/users/${owner}/packages/container/${pkg}/versions" - for suffix in amd64 arm64; do - tag="${version}-${suffix}" - mapfile -t ids < <(gh api "$api" --paginate \ - --jq ".[] | select([.metadata.container.tags[]] | index(\"${tag}\")) | .id") - if [ "${#ids[@]}" -eq 0 ]; then - echo "no GHCR package version for tag ${tag}" + versions=$(gh api "${api}?per_page=100" --paginate \ + --jq '.[] | [.id, .name, (.metadata.container.tags | length)] | @tsv') + keep=$(mktemp) + while IFS=$'\t' read -r id digest ntags; do + [ "$ntags" -gt 0 ] || continue + echo "$digest" >> "$keep" + raw=$(docker buildx imagetools inspect --raw "${image}@${digest}") + jq -r '.manifests[]?.digest' <<<"$raw" >> "$keep" + done <<<"$versions" + while IFS=$'\t' read -r id digest ntags; do + [ "$ntags" -eq 0 ] || continue + if grep -qxF "$digest" "$keep"; then continue fi - for id in "${ids[@]}"; do - echo "deleting GHCR package version ${id} (tag ${tag})" - gh api -X DELETE "${api}/${id}" - done - done + echo "deleting orphaned GHCR package version ${id} (${digest})" + gh api -X DELETE "${api}/${id}" + done <<<"$versions" diff --git a/CHANGELOG.md b/CHANGELOG.md index 008e44e..982cb39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version ## [Unreleased] +## [1.9.5] - 2026-10-09 + +CI-only cut: the release workflow no longer breaks the multi-arch image it +just published. No product code changes. + +### Fixed + +- ci (GHCR): the release no longer deletes the `X.Y.Z-amd64` / `X.Y.Z-arm64` + package versions after the manifest merge. Those versions are the images the + `X.Y.Z` manifest points at, so `docker pull ghcr.io/mixeme/selfpost:1.3.0` + failed with a missing manifest. The per-arch tags now stay; the merge job + instead removes untagged versions that no tagged manifest references. + ## [1.9.4] - 2026-10-09 Plan-only cut, owner-requested: the redesign goes first as `2.0.0` with a diff --git a/README.md b/README.md index c97218e..295f0aa 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ docker run --rm -d --name selfpost-try \ -e SELFPOST_HOSTNAME=mail.local.test \ -e PANEL_COOKIE_SECURE=false \ -v selfpost-try-data:/data \ - ghcr.io/mixeme/selfpost:1.9.4 + ghcr.io/mixeme/selfpost:1.9.5 ``` **Get the setup URL** (pick one): diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 9893039..0436038 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -21,7 +21,7 @@ services: selfpost: - image: ghcr.io/mixeme/selfpost:1.9.4 + image: ghcr.io/mixeme/selfpost:1.9.5 restart: unless-stopped environment: SELFPOST_HOSTNAME: "${SELFPOST_HOSTNAME:?set the mail/panel hostname, e.g. mail.example.com}" diff --git a/docs/development.md b/docs/development.md index 40d6b18..5872324 100644 --- a/docs/development.md +++ b/docs/development.md @@ -107,7 +107,7 @@ than the default «meaningful step» rule in [§ Commits and release build](#com 2. Git tag `vX.Y.Z` and publish the GitHub Release only on explicit request (see [§ Release image](#release-image)). -**Planned version cuts** (from pin `1.9.4`; adjust if semver changes mid-track): +**Planned version cuts** (from pin `1.9.5`; adjust if semver changes mid-track): | Stage | ID | Cut | |---|---|---| @@ -122,8 +122,9 @@ than the default «meaningful step» rule in [§ Commits and release build](#com | 9 | 2026-08-19 code review pass | `1.9.2` (PATCH) | | 10 | panel-redesign prep (docs only, owner-requested cut) | `1.9.3` (PATCH) | | 11 | panel-redesign plan: component kit, stages, reorder (docs only, owner-requested cut) | `1.9.4` (PATCH) | -| 12 | panel-redesign | `2.0.0` (planned; MAJOR, no compatibility) | -| 13 | inbound-antispam-panel | `2.1.0` (planned; was `1.10.0`, reordered 2026-10-09) | +| 12 | GHCR release fix: keep per-arch images, sweep orphaned versions (CI only) | `1.9.5` (PATCH) | +| 13 | panel-redesign | `2.0.0` (planned; MAJOR, no compatibility) | +| 14 | inbound-antispam-panel | `2.1.0` (planned; was `1.10.0`, reordered 2026-10-09) | Docs-only prep (checklists in plans, no product code) uses the same per-step commit rule but **no** version cut until the next product stage ships. @@ -409,13 +410,17 @@ prepare (version from release tag or workflow_dispatch input; checkout vX.Y.Z) → push ghcr.io/...:X.Y.Z-amd64 | X.Y.Z-arm64 → merge → docker buildx imagetools create → unified manifest X.Y.Z - → GitHub Packages API → drop X.Y.Z-amd64 and X.Y.Z-arm64 from GHCR + → GitHub Packages API → drop untagged versions no tagged manifest references ``` Native per-arch matrix (no QEMU): running the full Postfix/OpenDKIM stack under emulation for e2e is impractical. E2e first, then push — the registry receives -the bytes that passed the gate. Only `ghcr.io/mixeme/selfpost:X.Y.Z` remains -tagged in GHCR; per-arch names exist briefly during the merge job. +the bytes that passed the gate. Each release leaves three tags in GHCR: +`X.Y.Z` (the multi-arch manifest operators pin) and `X.Y.Z-amd64` / +`X.Y.Z-arm64` (the images it points at). The per-arch tags must stay — GHCR +cannot drop a tag without deleting the image, and deleting it breaks pulls of +`X.Y.Z`. The last merge step removes only orphans: untagged versions left +behind when a version is published again. A failed e2e **blocks** image publication. diff --git a/docs/guide.md b/docs/guide.md index b2770ed..334414c 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -173,7 +173,7 @@ cat ./data/setup-token #### Fixed image tag `deploy/docker-compose.yml` pins an explicit version (`ghcr.io/mixeme/selfpost:X.Y.Z`), -deliberately never `:latest`. The current pin is `1.9.4`. Intermediate +deliberately never `:latest`. The current pin is `1.9.5`. Intermediate CHANGELOG sections (`0.2.0`…`0.6.0`) record development cuts from before that image was published. Pinning matters because of the backup version check (see [Full backup and restore](#full-backup-and-restore)): the panel binary's