release: 1.9.5: GHCR release fix: keep per-arch images, sweep orphaned versions (CI only)
test / test (push) Canceled after 0s

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 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-09 16:25:34 +03:00
co-authored by Claude Opus 5.5
parent 1e92c5b2a2
commit 57c6fddfc3
6 changed files with 53 additions and 28 deletions
+26 -19
View File
@@ -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"
+13
View File
@@ -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
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -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}"
+11 -6
View File
@@ -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.
+1 -1
View File
@@ -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