release: 1.9.5: GHCR release fix: keep per-arch images, sweep orphaned versions (CI only)
test / test (push) Canceled after 0s
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:
@@ -11,9 +11,9 @@ name: release
|
|||||||
# own native runner; a merge job then combines them into the one manifest tag
|
# 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
|
# 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
|
# 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
|
# bytes that passed e2e. The per-arch tags stay in GHCR next to the version
|
||||||
# assemble the multi-arch manifest; merge removes them from GHCR so operators
|
# tag: they are the images the multi-arch manifest points at, and GHCR cannot
|
||||||
# see a single version tag.
|
# drop a tag without deleting the image behind it.
|
||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
@@ -177,28 +177,35 @@ jobs:
|
|||||||
backoff=$((backoff * 2))
|
backoff=$((backoff * 2))
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Remove per-arch tags from GHCR
|
- name: Remove orphaned untagged versions from GHCR
|
||||||
# Side-effect tags for imagetools assembly only — not part of the public
|
# Re-publishing a version moves its tags to new manifests and leaves the
|
||||||
# version surface (deploy/docker-compose.yml pins X.Y.Z, not X.Y.Z-amd64).
|
# old ones behind as untagged "sha256:..." package versions. Delete only
|
||||||
# imagetools has no "rm" subcommand; delete via the GitHub Packages API.
|
# 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:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
version="${{ needs.prepare.outputs.version }}"
|
image="ghcr.io/${{ github.repository }}"
|
||||||
owner="${{ github.repository_owner }}"
|
owner="${{ github.repository_owner }}"
|
||||||
pkg="${{ github.event.repository.name }}"
|
pkg="${{ github.event.repository.name }}"
|
||||||
api="/users/${owner}/packages/container/${pkg}/versions"
|
api="/users/${owner}/packages/container/${pkg}/versions"
|
||||||
for suffix in amd64 arm64; do
|
versions=$(gh api "${api}?per_page=100" --paginate \
|
||||||
tag="${version}-${suffix}"
|
--jq '.[] | [.id, .name, (.metadata.container.tags | length)] | @tsv')
|
||||||
mapfile -t ids < <(gh api "$api" --paginate \
|
keep=$(mktemp)
|
||||||
--jq ".[] | select([.metadata.container.tags[]] | index(\"${tag}\")) | .id")
|
while IFS=$'\t' read -r id digest ntags; do
|
||||||
if [ "${#ids[@]}" -eq 0 ]; then
|
[ "$ntags" -gt 0 ] || continue
|
||||||
echo "no GHCR package version for tag ${tag}"
|
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
|
continue
|
||||||
fi
|
fi
|
||||||
for id in "${ids[@]}"; do
|
echo "deleting orphaned GHCR package version ${id} (${digest})"
|
||||||
echo "deleting GHCR package version ${id} (tag ${tag})"
|
gh api -X DELETE "${api}/${id}"
|
||||||
gh api -X DELETE "${api}/${id}"
|
done <<<"$versions"
|
||||||
done
|
|
||||||
done
|
|
||||||
|
|||||||
@@ -5,6 +5,19 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [1.9.4] - 2026-10-09
|
||||||
|
|
||||||
Plan-only cut, owner-requested: the redesign goes first as `2.0.0` with a
|
Plan-only cut, owner-requested: the redesign goes first as `2.0.0` with a
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ docker run --rm -d --name selfpost-try \
|
|||||||
-e SELFPOST_HOSTNAME=mail.local.test \
|
-e SELFPOST_HOSTNAME=mail.local.test \
|
||||||
-e PANEL_COOKIE_SECURE=false \
|
-e PANEL_COOKIE_SECURE=false \
|
||||||
-v selfpost-try-data:/data \
|
-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):
|
**Get the setup URL** (pick one):
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
selfpost:
|
selfpost:
|
||||||
image: ghcr.io/mixeme/selfpost:1.9.4
|
image: ghcr.io/mixeme/selfpost:1.9.5
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
SELFPOST_HOSTNAME: "${SELFPOST_HOSTNAME:?set the mail/panel hostname, e.g. mail.example.com}"
|
SELFPOST_HOSTNAME: "${SELFPOST_HOSTNAME:?set the mail/panel hostname, e.g. mail.example.com}"
|
||||||
|
|||||||
+11
-6
@@ -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
|
2. Git tag `vX.Y.Z` and publish the GitHub Release only on explicit request (see
|
||||||
[§ Release image](#release-image)).
|
[§ 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 |
|
| 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) |
|
| 9 | 2026-08-19 code review pass | `1.9.2` (PATCH) |
|
||||||
| 10 | panel-redesign prep (docs only, owner-requested cut) | `1.9.3` (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) |
|
| 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) |
|
| 12 | GHCR release fix: keep per-arch images, sweep orphaned versions (CI only) | `1.9.5` (PATCH) |
|
||||||
| 13 | inbound-antispam-panel | `2.1.0` (planned; was `1.10.0`, reordered 2026-10-09) |
|
| 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
|
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.
|
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
|
→ push ghcr.io/...:X.Y.Z-amd64 | X.Y.Z-arm64
|
||||||
→ merge
|
→ merge
|
||||||
→ docker buildx imagetools create → unified manifest X.Y.Z
|
→ 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
|
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
|
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
|
the bytes that passed the gate. Each release leaves three tags in GHCR:
|
||||||
tagged in GHCR; per-arch names exist briefly during the merge job.
|
`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.
|
A failed e2e **blocks** image publication.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -173,7 +173,7 @@ cat ./data/setup-token
|
|||||||
#### Fixed image tag
|
#### Fixed image tag
|
||||||
|
|
||||||
`deploy/docker-compose.yml` pins an explicit version (`ghcr.io/mixeme/selfpost:X.Y.Z`),
|
`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
|
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
|
image was published. Pinning matters because of the backup version check (see
|
||||||
[Full backup and restore](#full-backup-and-restore)): the panel binary's
|
[Full backup and restore](#full-backup-and-restore)): the panel binary's
|
||||||
|
|||||||
Reference in New Issue
Block a user