diff --git a/.github/workflows/release-on-publish.yml b/.github/workflows/release-on-publish.yml deleted file mode 100644 index 483ede6..0000000 --- a/.github/workflows/release-on-publish.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: release-on-publish - -# Listens for a published GitHub Release and starts release.yml via -# workflow_dispatch. GitHub documents that workflow_dispatch always creates a -# new run (even when triggered by GITHUB_TOKEN), unlike some release:published -# deliveries that never started release.yml in this repo (see repo events API -# 2026-08-14T16:44:21Z vs Actions run list). - -on: - release: - types: [published] - -permissions: - actions: write - contents: read - -jobs: - dispatch: - runs-on: ubuntu-latest - steps: - - name: Derive SemVer from release tag - id: version - env: - RELEASE_TAG: ${{ github.event.release.tag_name }} - run: | - set -euo pipefail - raw="${RELEASE_TAG#v}" - if ! [[ "$raw" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "::error::release tag is not SemVer vX.Y.Z (${RELEASE_TAG})" - exit 1 - fi - #region agent log - echo "::group::agent-log release-on-publish" - echo "release_tag=${RELEASE_TAG} version=${raw} release_id=${{ github.event.release.id }} draft=${{ github.event.release.draft }}" - echo "::endgroup::" - #endregion - echo "version=${raw}" >> "$GITHUB_OUTPUT" - - - name: Start release workflow - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh workflow run release.yml \ - --repo "${{ github.repository }}" \ - --ref main \ - -f "version=${{ steps.version.outputs.version }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index edb5832..dbe013c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,9 +2,8 @@ name: release # Publishes an immutable, version-tagged image on ghcr.io (spec 10.1). # Ordinary commits and bare git tag pushes do not publish anything. A published -# GitHub Release starts release-on-publish.yml, which dispatches this workflow -# with the release version. You can also run it manually via workflow_dispatch -# with an explicit SemVer X.Y.Z version. +# GitHub Release runs this workflow directly (same pattern as gosentry / imap-scrub). +# You can also run it manually via workflow_dispatch with an explicit SemVer X.Y.Z. # # Native per-architecture builds (see docs/development.md), not qemu: # running the full Postfix/OpenDKIM stack under emulation for the e2e gate @@ -12,10 +11,12 @@ name: release # tag on its 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, at the cost of per-arch tags lingering -# in the registry as a side effect (harmless — the version tag's immutability, -# spec 10.1, is about that tag, not these). +# 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 (spec 10.1). on: + release: + types: [published] workflow_dispatch: inputs: version: @@ -37,20 +38,20 @@ jobs: id: version env: INPUT_VERSION: ${{ github.event.inputs.version }} + RELEASE_TAG: ${{ github.event.release.tag_name }} EVENT_NAME: ${{ github.event_name }} run: | set -euo pipefail - raw="${INPUT_VERSION:-}" + if [ "$EVENT_NAME" = "release" ]; then + raw="${RELEASE_TAG:-}" + else + raw="${INPUT_VERSION:-}" + fi raw="${raw#v}" if ! [[ "$raw" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "::error::release version is not SemVer X.Y.Z (event=${EVENT_NAME}; pass inputs.version on workflow_dispatch). Refusing to publish ghcr.io/${{ github.repository }}:${raw:-?}" + echo "::error::release version is not SemVer X.Y.Z (event=${EVENT_NAME}; tag=${RELEASE_TAG:-} input=${INPUT_VERSION:-}). Refusing to publish ghcr.io/${{ github.repository }}:${raw:-?}" exit 1 fi - #region agent log - echo "::group::agent-log prepare version" - echo "event=${EVENT_NAME} version=${raw} checkout_ref=v${raw}" - echo "::endgroup::" - #endregion echo "version=${raw}" >> "$GITHUB_OUTPUT" build: @@ -136,24 +137,6 @@ jobs: with: ref: v${{ needs.prepare.outputs.version }} - - name: Debug — snapshot GitHub Release state (agent) - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ needs.prepare.outputs.version }} - run: | - #region agent log - set -euo pipefail - tag="v${VERSION}" - echo "::group::agent-log release snapshot for ${tag}" - if out=$(gh release view "$tag" --repo "${{ github.repository }}" --json isDraft,tagName,publishedAt,url 2>&1); then - echo "release-exists: ${out}" - else - echo "release-missing: ${out}" - fi - echo "event=${{ github.event_name }} ref=${GITHUB_REF_NAME}" - echo "::endgroup::" - #endregion - - uses: docker/setup-buildx-action@v3 - name: Log in to ghcr.io @@ -181,12 +164,6 @@ jobs: set -e if [ "$rc" -eq 0 ]; then printf '%s\n' "$out" - #region agent log - echo "::group::agent-log ghcr tags published" - echo "version=${{ needs.prepare.outputs.version }} event=${{ github.event_name }} ref=${GITHUB_REF_NAME}" - echo "tags_pushed=${{ needs.prepare.outputs.version }},${{ needs.prepare.outputs.version }}-amd64,${{ needs.prepare.outputs.version }}-arm64" - echo "::endgroup::" - #endregion exit 0 fi printf '%s\n' "$out" >&2 @@ -199,3 +176,23 @@ jobs: attempt=$((attempt + 1)) 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). + run: | + set -euo pipefail + repo="ghcr.io/${{ github.repository }}" + version="${{ needs.prepare.outputs.version }}" + for suffix in amd64 arm64; do + tag="${repo}:${version}-${suffix}" + set +e + out=$(docker buildx imagetools rm "$tag" 2>&1) + rc=$? + set -e + if [ "$rc" -eq 0 ]; then + printf '%s\n' "$out" + else + echo "::warning::could not remove ${tag}: ${out}" >&2 + fi + done diff --git a/docs/development.md b/docs/development.md index 9121540..3296ceb 100644 --- a/docs/development.md +++ b/docs/development.md @@ -205,9 +205,8 @@ the binaries so they cannot drift apart. local-trial image references) in the **same** release commit. 2. Create and push git tag `vX.Y.Z` on that commit. 3. Publish the GitHub Release for `vX.Y.Z` (not a draft). -4. Workflow [release-on-publish.yml](../.github/workflows/release-on-publish.yml) - starts [release.yml](../.github/workflows/release.yml) with that version; - the build checks out tag `vX.Y.Z` (not `main` HEAD). +4. Workflow [release.yml](../.github/workflows/release.yml) builds, e2e-gates, + and publishes `ghcr.io/mixeme/selfpost:X.Y.Z` (checks out tag `vX.Y.Z`). **GitHub Release vs GHCR.** The public [Releases](https://github.com/mixeme/selfpost/releases) page lists only **published** releases. A draft is visible to maintainers only — @@ -307,29 +306,31 @@ Workflows in [.github/workflows/](../.github/workflows/). What each job runs — ### `release.yml` — published GitHub Release, or `workflow_dispatch` with SemVer -Publishing a GitHub Release runs [release-on-publish.yml](../.github/workflows/release-on-publish.yml), -which dispatches `release.yml` with the version parsed from the release tag. -You can also run `release.yml` manually via `workflow_dispatch` and an -explicit `X.Y.Z` input. A bare git tag push does not run either workflow. -The build always checks out `vX.Y.Z`, not `main` HEAD. +Publishing a GitHub Release runs `release.yml` directly (`release: published`, +same pattern as gosentry / imap-scrub). You can also run it manually via +`workflow_dispatch` with an explicit `X.Y.Z` input. A bare git tag push does not +run the workflow. The build always checks out `vX.Y.Z`, not `main` HEAD. -`prepare` takes the version from the `workflow_dispatch` `version` input. A -dispatch whose input is missing or not `X.Y.Z` fails in `prepare`. +`prepare` takes the version from `github.event.release.tag_name` on a release +event, or from the `workflow_dispatch` `version` input. A dispatch whose input +is missing or not `X.Y.Z` fails in `prepare`. ``` -release-on-publish (release: published → workflow_dispatch) -prepare (version from workflow_dispatch input; checkout vX.Y.Z) +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) → push ghcr.io/...:X.Y.Z-amd64 | X.Y.Z-arm64 → merge → docker buildx imagetools create → unified manifest X.Y.Z + → imagetools rm → drop X.Y.Z-amd64 and X.Y.Z-arm64 from GHCR ``` 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. +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. A failed e2e **blocks** image publication.