From 5ce1f05f9b5047d2a7356d127e371a01a19b56ac Mon Sep 17 00:00:00 2001 From: David Miller Date: Sat, 17 Oct 2026 13:49:57 +0200 Subject: [PATCH] fix: make release creation safe to rerun --- .github/workflows/ci.yml | 3 ++ .github/workflows/release.yml | 15 ++++-- tests/release/release-create-rerun.test.sh | 58 ++++++++++++++++++++++ 3 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 tests/release/release-create-rerun.test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da3eb67b..4bdaabf5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,6 +67,9 @@ jobs: - name: Check the release waits for crates.io to index each crate run: bash tests/release/crates-index-poll.test.sh + - name: Check GitHub release creation is safe to re-run + run: bash tests/release/release-create-rerun.test.sh + - name: Test internal dependency pin coverage run: bash tests/release/release-version-pins.test.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6d6e2546..f828e938 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -262,10 +262,17 @@ jobs: VERSION: ${{ github.ref_name }} REPO: ${{ github.repository }} run: | - gh release create "$VERSION" \ - --repo "$REPO" \ - --title "$VERSION" \ - --generate-notes + if release_output="$(gh release view "$VERSION" --repo "$REPO" 2>&1)"; then + echo "Release $VERSION already exists; skipping creation" + elif [[ "$release_output" == *'release not found'* ]]; then + gh release create "$VERSION" \ + --repo "$REPO" \ + --title "$VERSION" \ + --generate-notes + else + printf '::error::Failed to check release %s: %s\n' "$VERSION" "$release_output" >&2 + exit 1 + fi - name: Upload release assets # `dist/sysknife-*-linux-x86_64` matches BOTH `sysknife-v…-x86_64` diff --git a/tests/release/release-create-rerun.test.sh b/tests/release/release-create-rerun.test.sh new file mode 100644 index 00000000..c4be8b68 --- /dev/null +++ b/tests/release/release-create-rerun.test.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$repo_root" + +python3 - <<'PY' +import re +import sys +from pathlib import Path + +import yaml + +workflow = yaml.safe_load(Path(".github/workflows/release.yml").read_text()) +steps = workflow["jobs"]["release"]["steps"] +scripts = [step.get("run", "") for step in steps] +creates = [script for script in scripts if 'gh release create "$VERSION"' in script] +if len(creates) != 1: + sys.exit("FAIL: expected exactly one release creation script") + +# Match the control flow, not merely the presence of view and create somewhere +# in the workflow. Creation must be confined to the explicit not-found branch. +guard = re.fullmatch( + r'if release_output="\$\(gh release view "\$VERSION" --repo "\$REPO" 2>&1\)"; then\n' + r'(?P.*?)' + r'^elif \[\[ "\$release_output" == \*\'release not found\'\* \]\]; then\n' + r'(?P.*?)' + r'^else\n(?P.*?)^fi\s*', + creates[0], + re.MULTILINE | re.DOTALL, +) +if guard is None: + sys.exit("FAIL: gh release create is not guarded by the release-existence check") + +create = re.fullmatch( + r'\s*gh release create "\$VERSION" \\\n' + r'\s*--repo "\$REPO" \\\n' + r'\s*--title "\$VERSION" \\\n' + r'\s*--generate-notes\s*', + guard["missing"], +) +if create is None or 'gh release create' in guard["exists"] + guard["error"]: + sys.exit("FAIL: release creation must occur only for a missing release") +if not re.fullmatch(r'\s*echo [^\n]+\n', guard["exists"]): + sys.exit("FAIL: an existing release must continue without creation or exit") +if not re.fullmatch( + r"\s*printf '::error::[^\n]+\"\$release_output\" >&2\n\s*exit 1\n", + guard["error"], +): + sys.exit("FAIL: unexpected lookup errors must be reported and stop the release") + +create_index = scripts.index(creates[0]) +if not any('gh release upload "$VERSION"' in script and '--clobber' in script + for script in scripts[create_index + 1:]): + sys.exit("FAIL: guarded creation must be followed by clobber-enabled upload") + +print("release creation re-run contract passed.") +PY