From 70b4eb5585065c751933709ef072f8fa4fb36dc7 Mon Sep 17 00:00:00 2001 From: Mix Date: Wed, 14 Oct 2026 13:18:20 +0300 Subject: [PATCH] release: stamp the image with the release's version (audit H1) The e2e gate rebuilt selfpost:e2e through compose.override.yml with its own VERSION=e2e, and that rebuild is the image release.yml pushed: every published image printed "e2e", and the backup/restore version check compared a backup's version with "e2e". - compose.override.yml: VERSION: ${SELFPOST_E2E_VERSION:-e2e}. - release.yml: the e2e step gets SELFPOST_E2E_VERSION = the release's X.Y.Z; a new step before the push checks panel -version and selfpost-backup -version of selfpost:e2e against it. - e2e: image_stamped_with_version checks both binaries against expectedVersion() (the variable, or "e2e" when unset). - development.md, CHANGELOG [Unreleased], audit plan line ticked, roadmap 1/61. Evidence: make e2e on the test server for this tree with SELFPOST_E2E_VERSION=2.4.2-stamp - exit 0, TestE2E/image_stamped_with_version PASS, and `docker run --rm --entrypoint selfpost:e2e -version` prints 2.4.2-stamp for panel and selfpost-backup. GOOS=linux go vet in test/e2e clean. Published images are left as they are. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 19 +++++++++++++++++++ CHANGELOG.md | 13 +++++++++++++ docs/audit-2026-10-12.md | 2 +- docs/development.md | 13 ++++++++++--- docs/roadmap.md | 2 +- test/e2e/compose.override.yml | 6 +++++- test/e2e/main_test.go | 17 +++++++++++++++++ test/e2e/stack.go | 10 ++++++++++ 8 files changed, 76 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0136cd5..78667a7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -89,8 +89,27 @@ jobs: cache-dependency-path: test/e2e/go.sum - name: e2e (gates publishing — see docs/development.md) + # The e2e builds selfpost:e2e again through compose.override.yml, and + # that rebuild is the image pushed below. Without the version here it + # was stamped "e2e" (audit 2026-10-12, H1); the suite now checks the + # stamp against this variable. + env: + SELFPOST_E2E_VERSION: ${{ needs.prepare.outputs.version }} run: cd test/e2e && go test -v -timeout 20m ./... + - name: Check the version stamp of the image about to be pushed + env: + WANT: ${{ needs.prepare.outputs.version }} + run: | + set -euo pipefail + for bin in panel selfpost-backup; do + got=$(docker run --rm --entrypoint "$bin" selfpost:e2e -version) + if [ "$got" != "$WANT" ]; then + echo "::error::selfpost:e2e $bin -version = '$got', want '$WANT'; refusing to push" + exit 1 + fi + done + - name: Log in to ghcr.io uses: docker/login-action@v3 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index b98adfb..28ce810 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] +### Fixed + +- **A release image says which release it is.** The e2e gate in + `release.yml` rebuilt the image it was about to push with its own + `VERSION=e2e`, so every published image printed `e2e` for `-version` and in + the panel's footer, and the backup/restore version check compared a + backup's version with `e2e` - any backup restored into any release (audit + 2026-10-12, H1). The gate now builds with the release's version + (`SELFPOST_E2E_VERSION`), the e2e checks `panel -version` and + `selfpost-backup -version` against it, and `release.yml` checks the stamp + again on the image right before pushing it. Images already published are + left as they are. + ## [2.4.1] - 2026-10-12 Documentation only; no product code changes. diff --git a/docs/audit-2026-10-12.md b/docs/audit-2026-10-12.md index 0392674..a9e6e8f 100644 --- a/docs/audit-2026-10-12.md +++ b/docs/audit-2026-10-12.md @@ -545,7 +545,7 @@ has already run is never edited — the next file is added; the published ### Stage 0 — release hygiene (no product code) → `2.4.2` -- [ ] **Stamp the right version** (H1). Make the e2e build take the version from +- [x] **Stamp the right version** (H1). Make the e2e build take the version from the environment (`VERSION: ${SELFPOST_E2E_VERSION:-e2e}` in `compose.override.yml`, set by `release.yml`), or have the e2e reuse the image already built instead of rebuilding. Done when the e2e asserts diff --git a/docs/development.md b/docs/development.md index 75cc80c..8f892e8 100644 --- a/docs/development.md +++ b/docs/development.md @@ -269,7 +269,12 @@ The release image is published **only** for a SemVer version `X.Y.Z`: a that supplies that version. Pushing a git tag alone does not publish. Ordinary commits, and a dispatch from `main` without a version input, do not publish. The version is the single source that drives the image tag and `-ldflags` in -the binaries so they cannot drift apart. +the binaries so they cannot drift apart. The e2e gate rebuilds the image it +then pushes, so `release.yml` hands it the version as `SELFPOST_E2E_VERSION`; +the suite's `image_stamped_with_version` and a check right before the push +both compare `panel -version` and `selfpost-backup -version` with it. (Until +`2.4.2` the rebuild stamped every published image `e2e`; those images stay as +they are.) **Steps (on explicit request):** @@ -395,7 +400,8 @@ make e2e [test/e2e/compose.override.yml](../test/e2e/compose.override.yml) — same `cap_drop`/`cap_add`/`no-new-privileges` as production. Override: high ports (`20465`/`20587`/`20080`), test hostname, self-signed TLS, -`PANEL_COOKIE_SECURE=false`, isolated compose project. Mail is hermetic: +`PANEL_COOKIE_SECURE=false`, isolated compose project, and the image's +`VERSION` from `SELFPOST_E2E_VERSION` (unset locally: `e2e`). Mail is hermetic: CoreDNS (fake zone) + Postfix `smtp-sink` as sink-MX; DKIM TXT is scraped from the panel and published into the zone — the test verifies the records an operator would actually use. @@ -440,7 +446,8 @@ release: published prepare (version from release tag or workflow_dispatch input; checkout vX.Y.Z) → build [matrix: ubuntu-latest / ubuntu-24.04-arm] → docker build --load (VERSION from prepare) - → e2e (test/e2e) + → e2e (test/e2e; rebuilds selfpost:e2e with SELFPOST_E2E_VERSION=X.Y.Z) + → check panel / selfpost-backup -version = X.Y.Z → push ghcr.io/...:X.Y.Z-amd64 | X.Y.Z-arm64 → merge → docker buildx imagetools create → unified manifest X.Y.Z diff --git a/docs/roadmap.md b/docs/roadmap.md index df1f60b..cd2aeac 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -42,7 +42,7 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md). | from-header-binding | Hold a message's `From:` header against the application that sends it | shipped `2.4.0` | 9/9 | [plans/from-header-binding.md](plans/from-header-binding.md) | | panel-redesign | Panel redesign (accepted mockups, design contract) | shipped `2.0.0` | 27/27 | [plans/panel-redesign.md](plans/panel-redesign.md) | | schema-squash | Squash SQLite migrations into a 2.x baseline | built in panel-redesign (stage 1) | — | [plans/panel-redesign.md](plans/panel-redesign.md) | -| audit-2026-10-12 | Follow-ups of the 2026-10-12 full-tree code audit, seven stages | agreed (2026-10-12: stage 0 first, then 1+2; the rest after) | 0/61 | [audit-2026-10-12.md](audit-2026-10-12.md) | +| audit-2026-10-12 | Follow-ups of the 2026-10-12 full-tree code audit, seven stages | agreed (2026-10-12: stage 0 first, then 1+2; the rest after) | 1/61 | [audit-2026-10-12.md](audit-2026-10-12.md) | **Recommended order** (not binding; owner, 2026-10-09: feature order follows development convenience): **panel-redesign** shipped as `2.0.0` (2026-10-11) and diff --git a/test/e2e/compose.override.yml b/test/e2e/compose.override.yml index 583ffa5..ce7256e 100644 --- a/test/e2e/compose.override.yml +++ b/test/e2e/compose.override.yml @@ -28,7 +28,11 @@ services: context: ../../.. dockerfile: build/Dockerfile args: - VERSION: e2e + # release.yml sets SELFPOST_E2E_VERSION to the release's X.Y.Z, so the + # image this build tags selfpost:e2e - the one release.yml then pushes - + # carries the release's version, not "e2e". A local `make e2e` leaves it + # unset. The suite checks the stamp (image_stamped_with_version). + VERSION: ${SELFPOST_E2E_VERSION:-e2e} image: selfpost:e2e environment: SELFPOST_HOSTNAME: mail.e2e.test diff --git a/test/e2e/main_test.go b/test/e2e/main_test.go index ed4e346..0def398 100644 --- a/test/e2e/main_test.go +++ b/test/e2e/main_test.go @@ -130,6 +130,23 @@ func TestE2E(t *testing.T) { } }) + // The image is pushed as the release, so it must say it is the release: + // the footer, -version and the backup/restore version check all read this + // stamp. Release images were once stamped "e2e" because this stand rebuilt + // them with its own VERSION (audit 2026-10-12, H1). + run("image_stamped_with_version", func(t *testing.T) { + want := expectedVersion() + for _, bin := range []string{"panel", "selfpost-backup"} { + out, err := h.execIn("selfpost", bin, "-version") + if err != nil { + t.Fatalf("%s -version: %v", bin, err) + } + if got := strings.TrimSpace(out); got != want { + t.Fatalf("%s -version = %q, want %q", bin, got, want) + } + } + }) + run("setup_and_login", func(t *testing.T) { token, err := readSetupToken(h) if err != nil { diff --git a/test/e2e/stack.go b/test/e2e/stack.go index 1bc98e8..2ac2c3b 100644 --- a/test/e2e/stack.go +++ b/test/e2e/stack.go @@ -89,6 +89,16 @@ func (s *stack) execIn(service string, args ...string) (string, error) { return string(out), err } +// expectedVersion is the version the stand's image is built with: the +// release's X.Y.Z when release.yml runs the suite, "e2e" for a local run. Its +// default must stay the default of VERSION in compose.override.yml. +func expectedVersion() string { + if v := os.Getenv("SELFPOST_E2E_VERSION"); v != "" { + return v + } + return "e2e" +} + func (s *stack) build() error { _, err := s.compose("build") return err