Create the GitHub Release object automatically on a tag push

Nothing in the repository published the Release object. All five
packaging workflows run a "Wait for tag release" step that polls
`gh release view` twenty times at fifteen-second intervals and then
fails the job, so a tag push failed every packaging leg unless someone
ran `gh release create` by hand inside that five-minute budget.

The previous creator lived in package-openwrt.yml and generated the
release body from the commit list, which raced the hand-created object
and could strand the written release notes. It was removed for v0.4.2
and nothing replaced it. This adds a dedicated workflow instead, so
there is one creator, it runs as early as a tag push allows, and it
publishes `docs/releases/release-notes-<tag>.md` rather than a
generated commit list.

It is idempotent in both directions: an existing Release is left
untouched, and a Release created concurrently between the check and the
create is treated as success. A pre-release tag is marked as such and
gets the RC placeholder body, since the written notes are finalized
only for the stable tag.

(cherry picked from commit 3429fd6064a8b8ea1f87023cfccabd499696a37f)
This commit is contained in:
Johnathan Corgan
2026-08-25 20:46:53 +01:00
parent e9b6bfd3ad
commit dc3e7945ce
+77
View File
@@ -0,0 +1,77 @@
name: Release Object
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Tag to create the Release object for"
required: true
# Every packaging workflow's upload job runs a `Wait for tag release` step that
# polls `gh release view` 20 times at 15s intervals and then fails the job. Five
# waiters and no creator means a tag push fails all five unless a human runs
# `gh release create` inside that five-minute budget. This job is the creator.
#
# It deliberately does not live in a packaging workflow. The previous creator did
# (package-openwrt.yml, via an action with generate_release_notes), and it raced
# the operator's hand-created object for the right to decide the Release body;
# whichever lost left the written notes stranded. Here there is one creator, it
# publishes the notes file the release wrote, and it is idempotent, so running it
# alongside a hand-created object is a no-op rather than a race.
permissions:
contents: read
concurrency:
group: release-object-${{ github.ref }}
jobs:
create-release:
name: Create the Release object
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
ref: ${{ inputs.tag || github.ref }}
- name: Create the Release object if it does not exist
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.tag || github.ref_name }}
run: |
set -euo pipefail
if gh release view "${TAG}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
echo "Release ${TAG} already exists; leaving it untouched."
exit 0
fi
args=(--repo "${GITHUB_REPOSITORY}" --title "FIPS ${TAG}")
# A tag carrying a pre-release suffix (v0.5.0-rc1) is an RC: it is
# marked as a pre-release and has no written notes of its own, since
# the notes are finalized for the stable tag.
notes="docs/releases/release-notes-${TAG}.md"
if [[ "${TAG}" == *-* ]]; then
args+=(--prerelease --notes "Release candidate - packaging validation only.")
elif [[ -f "${notes}" ]]; then
args+=(--notes-file "${notes}")
else
# Never --generate-notes: an auto-generated commit list published
# under a stable tag is exactly what stranded the written notes
# before. An empty body is visibly wrong and trivially editable.
echo "::warning::${notes} is missing; publishing ${TAG} with an empty body."
args+=(--notes "")
fi
if ! gh release create "${TAG}" "${args[@]}"; then
# A concurrent creator - the operator by hand, or a re-run - may have
# won between the check above and here. An object that now exists is
# the outcome this job wanted.
gh release view "${TAG}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1
echo "Release ${TAG} was created concurrently; nothing to do."
fi