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:
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
+10
-3
@@ -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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user