From 14674b9dd92d221825795e92f44f0e13b8795e2c Mon Sep 17 00:00:00 2001 From: mervin008 Date: Wed, 23 Sep 2026 09:11:20 +0200 Subject: [PATCH] Create the release page for each tag Tagging a version published the module and nothing else. The release page was a step somebody had to remember, and v0.3.0 of the three gateways and their SDK went nine days without one: the module and the images were served, and every Releases page still said v0.2.0 (certpilot/certpilot#101). A new workflow, since this repository had none: the module needs only its tag, but the Releases page is where somebody evaluating it looks first. - An annotated tag's message becomes the notes. A lightweight tag gets GitHub's generated list of merged pull requests. - Latest goes only to the highest vX.Y.Z tag. A patch to an older line leaves the badge where it is, and a tag with a suffix is a prerelease. - A release that already exists is left alone, so re-running a tag's workflow, or writing the page by hand first, neither fails nor overwrites. - It runs only on a tag push. Verified by running the script against the real repositories, with `gh release create` replaced by a stub that prints the command: - an existing release was skipped - the agent's annotated v0.2.0 tag produced a notes file from its message - a lightweight tag produced --generate-notes - the ACME gateway's v0.2.0 got --latest=false, and v0.3.0 --latest=true - a stubbed v0.4.0-rc.1 got --prerelease actionlint is clean. The job itself has not run yet: the first tag pushed after this merges is its first real run. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 57 +++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a07d9a7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,57 @@ +name: Release + +# The release page for each tag. The module needs only the tag, which the Go +# module proxy serves from, but the Releases page is where somebody evaluating +# a dependency looks first. Nothing used to create it: the gateway SDK's v0.3.0 +# was tagged and served for nine days while its Releases page still said v0.2.0, +# because the page was a step somebody had to remember (certpilot/certpilot#101). +on: + push: + tags: ['v*'] + +permissions: + contents: write + +jobs: + release: + name: publish release + runs-on: ubuntu-latest + steps: + - name: Create the release for this tag + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + + # Re-running a tag's workflow, or a release written by hand before this + # ran, must neither fail nor overwrite what is there. + if gh release view "$TAG" >/dev/null 2>&1; then + echo "$TAG already has a release; leaving it as it is." + exit 0 + fi + + # An annotated tag's message is the release notes, written by whoever cut + # the tag. A lightweight tag carries none, so GitHub lists the pull + # requests merged since the previous release instead. + notes=(--generate-notes) + ref=$(gh api "repos/$GH_REPO/git/ref/tags/$TAG") + if [ "$(jq -r .object.type <<<"$ref")" = tag ]; then + gh api "repos/$GH_REPO/git/tags/$(jq -r .object.sha <<<"$ref")" --jq .message \ + | sed '/^-----BEGIN [A-Z ]*SIGNATURE-----$/,$d' > "$RUNNER_TEMP/notes.md" + notes=(--notes-file "$RUNNER_TEMP/notes.md") + fi + + # Latest only for the highest version. A fix to an older line must not + # take the badge from the newest one, and a prerelease never has it. + flags=(--latest=false) + if [[ "$TAG" == *-* ]]; then + flags+=(--prerelease) + else + highest=$(gh api "repos/$GH_REPO/tags" --paginate --jq '.[].name' \ + | { grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' || true; } | sort -V | tail -n 1) + [ "$highest" = "$TAG" ] && flags=(--latest=true) + fi + + gh release create "$TAG" --verify-tag --title "$TAG" "${notes[@]}" "${flags[@]}"