From 5b63da0e49ede9d946b1db57392e04ca8cc21b61 Mon Sep 17 00:00:00 2001 From: Mikhail Yenuchenko Date: Fri, 14 Aug 2026 19:43:06 +0300 Subject: [PATCH] ci: trigger release workflow on published GitHub Release Bare git tag pushes no longer start the image build; publishing a GitHub Release does. workflow_dispatch still requires an explicit X.Y.Z version. Docs explain draft vs published releases and that deleting a tag on GitHub converts a published release back to draft. Co-Authored-By: Cursor --- .github/workflows/release.yml | 65 +++++++++++++++++++++++++++-------- docs/development.md | 38 +++++++++++++------- 2 files changed, 77 insertions(+), 26 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f03d60c..8ca2d64 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,13 +1,14 @@ name: release # Publishes an immutable, version-tagged image on ghcr.io (spec 10.1). -# Ordinary commits do not publish anything. A pushed tag matching vX.Y.Z does, -# and so does workflow_dispatch when it supplies a SemVer X.Y.Z version (or -# runs on such a tag). A dispatch from main without that input must fail in -# prepare — it must not publish ghcr.io/...:main. The version is the single -# source that goes into both the image tag and the panel binary's -ldflags -# version, so the two can never drift apart (the invariant restore's version -# check in spec 7.5.A depends on). +# Ordinary commits and bare git tag pushes do not publish anything. The +# workflow runs when a GitHub Release is published (not while it is still a +# draft), or on workflow_dispatch with an explicit SemVer X.Y.Z version. A +# dispatch from main without that input must fail in prepare — it must not +# publish ghcr.io/...:main. The version is the single source that goes into +# both the image tag and the panel binary's -ldflags version, so the two can +# never drift apart (the invariant restore's version check in spec 7.5.A +# depends on). # # Native per-architecture builds (see docs/development.md), not qemu: # running the full Postfix/OpenDKIM stack under emulation for the e2e gate @@ -19,13 +20,12 @@ name: release # in the registry as a side effect (harmless — the version tag's immutability, # spec 10.1, is about that tag, not these). on: - push: - tags: - - "v[0-9]+.[0-9]+.[0-9]+" + release: + types: [published] workflow_dispatch: inputs: version: - description: "Image version as X.Y.Z (no v prefix). Required unless this run is already on a vX.Y.Z tag." + description: "Image version as X.Y.Z (no v prefix). Required on workflow_dispatch." required: false type: string @@ -43,17 +43,24 @@ 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 [ -z "$raw" ] && [[ "${GITHUB_REF_NAME}" == v[0-9]*.[0-9]*.[0-9]* ]]; then - raw="${GITHUB_REF_NAME#v}" + if [ -z "$raw" ] && [ "${EVENT_NAME}" = "release" ] && [ -n "${RELEASE_TAG:-}" ]; then + raw="${RELEASE_TAG}" fi raw="${raw#v}" if ! [[ "$raw" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "::error::release version is not SemVer X.Y.Z (ref is ${GITHUB_REF_NAME}; pass inputs.version on workflow_dispatch). Refusing to publish ghcr.io/${{ github.repository }}:${GITHUB_REF_NAME}" + echo "::error::release version is not SemVer X.Y.Z (event=${EVENT_NAME}; release tag=${RELEASE_TAG:-none}; pass inputs.version on workflow_dispatch). Refusing to publish ghcr.io/${{ github.repository }}:${raw:-?}" exit 1 fi + #region agent log + echo "::group::agent-log prepare version" + echo "event=${EVENT_NAME} release_tag=${RELEASE_TAG:-none} version=${raw}" + echo "::endgroup::" + #endregion echo "version=${raw}" >> "$GITHUB_OUTPUT" build: @@ -69,6 +76,8 @@ jobs: runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name || github.ref }} - uses: docker/setup-buildx-action@v3 @@ -133,6 +142,28 @@ jobs: needs: [prepare, build] runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.release.tag_name || github.ref }} + + - 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 @@ -160,6 +191,12 @@ 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 diff --git a/docs/development.md b/docs/development.md index cb8b2da..b2aeac7 100644 --- a/docs/development.md +++ b/docs/development.md @@ -191,11 +191,12 @@ tag / push only on explicit request (see `release.yml`). ### Release image -The release image is published **only** for a SemVer version `X.Y.Z`: a pushed -tag `vX.Y.Z`, or a `workflow_dispatch` that supplies that version (or runs on -such a tag). 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 release image is published **only** for a SemVer version `X.Y.Z`: a +**published** GitHub Release whose tag is `vX.Y.Z`, or a `workflow_dispatch` +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. **Steps (on explicit request):** @@ -203,9 +204,22 @@ tag and `-ldflags` in the binaries so they cannot drift apart. tag in [deploy/docker-compose.yml](../deploy/docker-compose.yml) (and any local-trial image references) in the **same** release commit. 2. Create and push git tag `vX.Y.Z` on that commit. -3. Workflow [release.yml](../.github/workflows/release.yml) builds, e2e-gates, +3. Publish the GitHub Release for `vX.Y.Z` (not a draft). +4. Workflow [release.yml](../.github/workflows/release.yml) builds, e2e-gates, and publishes `ghcr.io/mixeme/selfpost:X.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 — +it looks like “no releases” to everyone else. CI does not create or publish the +GitHub Release; you do that in the UI. Deleting a release’s git tag on GitHub +(or re-pushing tags while cleaning the registry) converts a published release +back into a **draft** — that matches “I published three times and it keeps +disappearing”. After publish, leave the tag on GitHub; clean up only unwanted +GHCR package versions, not the git tag. + +Push workflow and source changes to **github.com/mixeme/selfpost** before +publishing — Actions reads that repo, not Gitea. + Ordinary commits **do not** publish an image. The compose pin and the git tag must match (`1.0.0` / `v1.0.0` for the first published release). Intermediate CHANGELOG sections (`0.2.0`…`0.6.0`) record development history before that cut. @@ -290,15 +304,15 @@ Workflows in [.github/workflows/](../.github/workflows/). What each job runs — `gofmt -l` → `go vet ./...` → `go test ./...` (main module, no e2e). -### `release.yml` — push of tag `vX.Y.Z`, or `workflow_dispatch` with SemVer +### `release.yml` — published GitHub Release, or `workflow_dispatch` with SemVer -`prepare` takes the version from the tag (`v1.2.5` → `1.2.5`) or from the -`workflow_dispatch` `version` input. A dispatch whose ref is not a `vX.Y.Z` -tag and whose input is missing or not `X.Y.Z` fails in `prepare` — it must -not publish `ghcr.io/...:main`. +`prepare` takes the version from the published release tag (`v1.2.5` → `1.2.5`) +or from the `workflow_dispatch` `version` input. A bare git tag push does not +run this workflow. A dispatch whose input is missing or not `X.Y.Z` fails in +`prepare` — it must not publish `ghcr.io/...:main`. ``` -prepare (version from tag or workflow_dispatch input) +prepare (version from published release tag or workflow_dispatch input) → build [matrix: ubuntu-latest / ubuntu-24.04-arm] → docker build --load (VERSION from prepare) → e2e (test/e2e)