release: stamp the image with the release's version (audit H1)
test / test (push) Canceled after 0s

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 <bin> 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 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-14 13:18:20 +03:00
co-authored by Claude Opus 5.5
parent 86665f08e6
commit 70b4eb5585
8 changed files with 76 additions and 6 deletions
+19
View File
@@ -89,8 +89,27 @@ jobs:
cache-dependency-path: test/e2e/go.sum cache-dependency-path: test/e2e/go.sum
- name: e2e (gates publishing — see docs/development.md) - 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 ./... 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 - name: Log in to ghcr.io
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
+13
View File
@@ -5,6 +5,19 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
## [Unreleased] ## [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 ## [2.4.1] - 2026-10-12
Documentation only; no product code changes. Documentation only; no product code changes.
+1 -1
View File
@@ -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` ### 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 the environment (`VERSION: ${SELFPOST_E2E_VERSION:-e2e}` in
`compose.override.yml`, set by `release.yml`), or have the e2e reuse the `compose.override.yml`, set by `release.yml`), or have the e2e reuse the
image already built instead of rebuilding. Done when the e2e asserts image already built instead of rebuilding. Done when the e2e asserts
+10 -3
View File
@@ -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 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. 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 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):** **Steps (on explicit request):**
@@ -395,7 +400,8 @@ make e2e
[test/e2e/compose.override.yml](../test/e2e/compose.override.yml) — same [test/e2e/compose.override.yml](../test/e2e/compose.override.yml) — same
`cap_drop`/`cap_add`/`no-new-privileges` as production. Override: high ports `cap_drop`/`cap_add`/`no-new-privileges` as production. Override: high ports
(`20465`/`20587`/`20080`), test hostname, self-signed TLS, (`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 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 the panel and published into the zone — the test verifies the records an
operator would actually use. operator would actually use.
@@ -440,7 +446,8 @@ release: published
prepare (version from release tag or workflow_dispatch input; checkout vX.Y.Z) prepare (version from release tag or workflow_dispatch input; checkout vX.Y.Z)
→ build [matrix: ubuntu-latest / ubuntu-24.04-arm] → build [matrix: ubuntu-latest / ubuntu-24.04-arm]
→ docker build --load (VERSION from prepare) → 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 → 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
+1 -1
View File
@@ -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) | | 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) | | 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) | | 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 **Recommended order** (not binding; owner, 2026-10-09: feature order follows
development convenience): **panel-redesign** shipped as `2.0.0` (2026-10-11) and development convenience): **panel-redesign** shipped as `2.0.0` (2026-10-11) and
+5 -1
View File
@@ -28,7 +28,11 @@ services:
context: ../../.. context: ../../..
dockerfile: build/Dockerfile dockerfile: build/Dockerfile
args: 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 image: selfpost:e2e
environment: environment:
SELFPOST_HOSTNAME: mail.e2e.test SELFPOST_HOSTNAME: mail.e2e.test
+17
View File
@@ -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) { run("setup_and_login", func(t *testing.T) {
token, err := readSetupToken(h) token, err := readSetupToken(h)
if err != nil { if err != nil {
+10
View File
@@ -89,6 +89,16 @@ func (s *stack) execIn(service string, args ...string) (string, error) {
return string(out), err 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 { func (s *stack) build() error {
_, err := s.compose("build") _, err := s.compose("build")
return err return err