ci: build each release once from its tag and publish it as a draft (#2754)

Co-authored-by: Claude <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jan-Otto Kröpke
2026-10-16 12:16:30 +02:00
committed by GitHub
co-authored by Claude
parent 1e48663824
commit 4af7012048
3 changed files with 133 additions and 20 deletions
+60 -20
View File
@@ -1,8 +1,15 @@
name: CI
# Pull requests run the Windows tests only when Go sources, modules, test data,
# tools/, the configuration examples or this workflow change. Pushes, tags and
# releases always run them.
# tools/, the configuration examples or this workflow change. Pushes always run
# them.
#
# A release is built by exactly one run: the push of its tag, or the dispatch
# on its tag from the Create tag workflow. That run builds and signs the
# binaries once, pushes the container images and creates a draft release with
# the same executables, so the images and the release assets match. The draft
# is published by hand; with immutable releases, its assets and tag are locked
# from then on.
on:
push:
branches:
@@ -15,9 +22,6 @@ on:
- "v*"
pull_request:
workflow_dispatch:
release:
types:
- published
permissions: {}
@@ -43,7 +47,7 @@ jobs:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
# Pushes to main and the release branches, tags, releases and manual
# Pushes to main, the release branches and tags, and manual
# runs always test, so a gap in the filter below cannot let a
# regression reach a branch that ships.
if [[ "$EVENT_NAME" != "pull_request" || -z "$BASE_SHA" ]]; then
@@ -85,6 +89,18 @@ jobs:
persist-credentials: 'false'
fetch-depth: '0'
# A published release is immutable. Rebuilding its tag would push images
# whose executable differs from the release assets, so stop before that.
- name: Check that the release is not published
if: ${{ startsWith(github.ref, 'refs/tags/') }}
env:
GH_TOKEN: ${{ github.token }}
run: |
if is_draft="$(gh release view "$GITHUB_REF_NAME" --json isDraft --jq .isDraft 2>/dev/null)" && [[ "$is_draft" == "false" ]]; then
echo "::error::Release $GITHUB_REF_NAME is already published"
exit 1
fi
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
# renovate: golang=go
@@ -142,6 +158,10 @@ jobs:
# renovate: github=goreleaser/goreleaser
version: v2.18.3
args: build --clean
env:
# Several tags can point at one commit, such as an rc on the last
# beta. Build the version of the tag this run is for.
GORELEASER_CURRENT_TAG: ${{ github.ref_name }}
- name: Verify Go overlay
run: go run ./tools/go-overlay verify dist/windows_exporter-amd64.exe dist/windows_exporter-arm64.exe
@@ -150,7 +170,11 @@ jobs:
run: |
mkdir -p output
version="$(git describe --tags --always)"
if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then
version="$GITHUB_REF_NAME"
else
version="$(git describe --tags --always)"
fi
version="${version#v}"
version="${version//+/_}"
printf '%s\n' "$version" | tee output/VERSION
@@ -449,15 +473,14 @@ jobs:
annotations: ${{ steps.meta.outputs.labels }}
publish:
# Publishing a release fires both a tag push and a release:published run for
# the same tag. Upload only from the release run: it is the only one where
# the release is guaranteed to exist, and uploading from both runs would
# race on the same asset names.
if: github.repository == 'prometheus-community/windows_exporter' && github.event_name == 'release'
# Draft the release only once the images are pushed, so a published release
# always has them.
if: github.repository == 'prometheus-community/windows_exporter' && startsWith(github.ref, 'refs/tags/') && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
runs-on: ubuntu-26.04
needs:
- package
- windows-test
- docker
permissions:
contents: write
steps:
@@ -466,16 +489,33 @@ jobs:
with:
name: windows_exporter_binaries
- name: Publish Artifacts
- name: Create draft release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# The job runs without a checkout, so gh cannot infer the repository.
GH_REPO: ${{ github.repository }}
run: |
gh release upload "$GITHUB_REF_NAME" \
windows_exporter-*.exe \
windows_exporter-*.msi \
sha256sums.txt
assets=(windows_exporter-*.exe windows_exporter-*.msi sha256sums.txt)
# A re-run replaces the assets of its own draft. A published release
# is immutable and must not be touched.
if is_draft="$(gh release view "$GITHUB_REF_NAME" --json isDraft --jq .isDraft 2>&1)"; then
if [[ "$is_draft" != "true" ]]; then
echo "::error::Release $GITHUB_REF_NAME is already published"
exit 1
fi
gh release upload "$GITHUB_REF_NAME" --clobber "${assets[@]}"
exit 0
elif [[ "$is_draft" != *"release not found"* ]]; then
echo "::error::Cannot look up release $GITHUB_REF_NAME: $is_draft"
exit 1
fi
args=(--draft --verify-tag --generate-notes --title "$GITHUB_REF_NAME")
if [[ "$GITHUB_REF_NAME" == *-* ]]; then
args+=(--prerelease)
fi
gh release create "$GITHUB_REF_NAME" "${args[@]}" "${assets[@]}"
windows-test:
name: Windows tests (${{ matrix.os }})
@@ -1831,9 +1871,8 @@ jobs:
super-linter:
name: Super Linter
# Super Linter cannot derive the commit range VALIDATE_ALL_CODEBASE=false
# needs from release events; the tag push run already lints the same commit.
if: github.event_name != 'release'
# The push run for the branch a tag is cut from already linted the commit.
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
runs-on: ubuntu-26.04
permissions:
contents: read
@@ -1884,6 +1923,7 @@ jobs:
- build
- package
- docker
- publish
- windows-test
- lint
- super-linter
+64
View File
@@ -0,0 +1,64 @@
name: Create tag
run-name: ${{ inputs.tag }}
# Creates a release tag on the commit the workflow runs on and starts CI for it.
# A tag created with GITHUB_TOKEN does not trigger the tag push run, but a
# workflow_dispatch does, so CI runs exactly once for the tag. That run builds
# and signs the binaries, pushes the container images and creates the draft
# release. Pushing a tag from a local clone triggers the same run directly.
on:
workflow_dispatch:
inputs:
tag:
description: Tag to create, such as v1.2.3 or v1.2.3-beta.1
required: true
type: string
permissions: {}
jobs:
create:
name: Create tag
if: github.repository == 'prometheus-community/windows_exporter'
runs-on: ubuntu-26.04
permissions:
actions: write
contents: write
steps:
- name: Create tag and start CI
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
TAG: ${{ inputs.tag }}
run: |
# The CI build derives the Windows file version from the tag and
# accepts only these forms: alpha.N and beta.N up to 99, rc.N up to
# 899.
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta|rc)\.([0-9]+))?$ ]]; then
echo "::error::Tag $TAG must look like v1.2.3, v1.2.3-beta.1 or v1.2.3-rc.1"
exit 1
fi
number="${BASH_REMATCH[3]}"
case "${BASH_REMATCH[2]}" in
alpha|beta) limit=99 ;;
rc) limit=899 ;;
esac
if [[ -n "$number" ]] && (( 10#$number > limit )); then
echo "::error::Pre-release number of tag $TAG does not fit the file version scheme"
exit 1
fi
# A retry after a failed dispatch finds the tag already on this
# commit and only starts CI again.
if existing="$(gh api "repos/$GH_REPO/git/ref/tags/$TAG" --jq .object.sha 2>/dev/null)"; then
if [[ "$existing" != "$GITHUB_SHA" ]]; then
echo "::error::Tag $TAG already exists on $existing"
exit 1
fi
else
gh api --method POST "repos/$GH_REPO/git/refs" \
-f "ref=refs/tags/$TAG" \
-f "sha=$GITHUB_SHA"
fi
gh workflow run ci.yml --ref "$TAG"
+9
View File
@@ -42,6 +42,15 @@ their runtime behavior.
for `alpha.N`/`beta.N`, 100+N for `rc.N` and 1000 for the release, so MDM
tools that compare file versions update from one beta to the next and to the
final release. Windows Installer itself ignores the fourth field.
- A release is built once, by the CI run for its tag: a tag push, or the
dispatch that the Create tag workflow
([`tag.yml`](.github/workflows/tag.yml)) starts after creating the tag with
`GITHUB_TOKEN`, which cannot trigger the push run itself. That run pushes the
container images and creates a draft release with the same signed
executables; a maintainer publishes it. Releases are immutable once
published, so never upload assets after publishing, and never let a second
run build the same tag. Start releases only with that workflow or a tag push,
never by creating a release with a new tag in the GitHub UI.
New collectors need discussion with maintainers first. Prefer the configurable
`performancecounter` collector where it can provide the requested metrics, as