From 492bac093102f461b595675048be7699d6a61f2d Mon Sep 17 00:00:00 2001 From: Ventsislav Kostadinov Date: Wed, 8 Jul 2026 13:06:30 +0300 Subject: [PATCH] fix: release_prep.sh tag source, archive name, and MODULE.bazel version patching - Read tag from $1 (positional arg from release_ruleset.yaml), not env var - Archive name includes 'v' prefix (bazel-contrib convention) - Patch MODULE.bazel version inside module() block in the archive (BCR compat) - Add tag format validation (semver regex guard) - Add release-prep-test.yaml regression test with path filters - Reproducible archives: gzip -n + GNU tar deterministic flags when available - Temp dir cleanup via EXIT trap - Graceful error on missing/invalid arguments --- .github/workflows/release-prep-test.yaml | 84 ++++++++++++++++++++++++ .github/workflows/release_prep.sh | 67 ++++++++++++++----- 2 files changed, 136 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/release-prep-test.yaml diff --git a/.github/workflows/release-prep-test.yaml b/.github/workflows/release-prep-test.yaml new file mode 100644 index 0000000..5edff93 --- /dev/null +++ b/.github/workflows/release-prep-test.yaml @@ -0,0 +1,84 @@ +# Regression test for the release source archive preparation script. +# +# Only runs when release_prep.sh or MODULE.bazel changes (path filter). +# Not part of the main gate — doesn't block unrelated PRs. +# +# Validates: +# - Archive naming (includes 'v' prefix) +# - MODULE.bazel version patching (only module() block, not bazel_dep) +# - Release notes output +# - Graceful failures (no args, invalid tag format) + +name: Release prep test + +on: + pull_request: + paths: + - ".github/workflows/release_prep.sh" + - ".github/workflows/release-prep-test.yaml" + - "MODULE.bazel" + push: + branches: [main] + paths: + - ".github/workflows/release_prep.sh" + - ".github/workflows/release-prep-test.yaml" + - "MODULE.bazel" + +permissions: + contents: read + +jobs: + test: + name: Validate release_prep.sh + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + + - name: Test with synthetic tag + run: | + .github/workflows/release_prep.sh v99.0.0 > /tmp/notes.txt + + # Assert archive name + test -f rules_odin-v99.0.0.tar.gz + + # Assert MODULE.bazel version patched inside module() block + MODULE=$(tar -xzf rules_odin-v99.0.0.tar.gz -O rules_odin-99.0.0/MODULE.bazel) + echo "$MODULE" | sed -n '/^module(/,/^)/p' | grep -q 'version = "99.0.0"' + + # Assert bazel_dep versions NOT changed + echo "$MODULE" | grep -q 'version = "1.7.1"' + echo "$MODULE" | grep -q 'version = "0.0.10"' + + # Assert release notes mention correct version + grep -q 'version = "99.0.0"' /tmp/notes.txt + + rm -f rules_odin-v99.0.0.tar.gz + echo "Happy path: PASSED" + + - name: Test no-arg invocation fails + run: | + if .github/workflows/release_prep.sh 2>/dev/null; then + echo "::error::Expected failure with no args" + exit 1 + fi + echo "No-arg rejection: PASSED" + + - name: Test invalid tag format fails + run: | + if .github/workflows/release_prep.sh "not-a-tag" 2>/dev/null; then + echo "::error::Expected failure with invalid tag 'not-a-tag'" + exit 1 + fi + + if .github/workflows/release_prep.sh "main" 2>/dev/null; then + echo "::error::Expected failure with branch name 'main'" + exit 1 + fi + + if .github/workflows/release_prep.sh "0.2.0" 2>/dev/null; then + echo "::error::Expected failure with tag missing 'v' prefix" + exit 1 + fi + echo "Invalid tag rejection: PASSED" diff --git a/.github/workflows/release_prep.sh b/.github/workflows/release_prep.sh index c687b31..0480114 100755 --- a/.github/workflows/release_prep.sh +++ b/.github/workflows/release_prep.sh @@ -4,37 +4,74 @@ # Creates the release source archive and outputs release notes. # The archive name MUST match the glob in release.yaml: rules_odin-*.tar.gz # -# Environment variables available: -# TAG — the version tag being released (e.g., v0.2.0) -# GITHUB_WORKSPACE — repo checkout root +# Called as: .github/workflows/release_prep.sh ${{ inputs.tag_name || github.ref_name }} +# The tag is passed as the first positional argument (not as an env var). -set -euo pipefail +set -o errexit -o nounset -o pipefail -TAG="${TAG:-${GITHUB_REF_NAME:-}}" +# Argument provided by release_ruleset.yaml. +TAG="${1:-}" if [[ -z "$TAG" ]]; then echo "::error::TAG is not set. Cannot determine release version." exit 1 fi -# Strip the 'v' prefix for the archive name (e.g., v0.2.0 → 0.2.0) +# Validate tag format (semver with 'v' prefix). +if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then + echo "::error::TAG '${TAG}' does not match expected format (vMAJOR.MINOR.PATCH)." + exit 1 +fi + +# Strip the 'v' prefix for module version (v0.2.0 -> 0.2.0) VERSION="${TAG#v}" -ARCHIVE="rules_odin-${VERSION}.tar.gz" +# Archive name includes the 'v' prefix to match GitHub's source archive naming. +# This allows users to switch between released artifacts and source archives +# with minimal differences (strip_prefix stays the same). +PREFIX="rules_odin-${VERSION}" +ARCHIVE="rules_odin-${TAG}.tar.gz" echo "Creating release archive: ${ARCHIVE}" -# Create a reproducible source archive using git archive. -# --prefix ensures the extracted directory is named rules_odin-{VERSION}/ -git archive \ - --format=tar.gz \ - --prefix="rules_odin-${VERSION}/" \ - --output="${ARCHIVE}" \ - HEAD +# Create the archive via git archive, then patch MODULE.bazel in-place. +# smlx/ccv tags HEAD as-is, so MODULE.bazel still has the old version. +# The BCR validation requires the version to match the tag exactly. +# +# We extract the full archive, patch MODULE.bazel, then re-archive. +# This avoids GNU tar --delete/--append which is not available on macOS bsdtar. +TMPDIR=$(mktemp -d) +trap 'rm -rf "${TMPDIR}"' EXIT + +git archive --format=tar --prefix="${PREFIX}/" HEAD | tar -xf - -C "${TMPDIR}" + +# Patch the version in MODULE.bazel to match the tag. +# Only replace the version inside the module() call, not bazel_dep versions. +# The module() block range ensures we don't touch dependency versions. +sed -i.bak "/^module(/,/^)/s/version = \"[^\"]*\"/version = \"${VERSION}\"/" "${TMPDIR}/${PREFIX}/MODULE.bazel" +rm -f "${TMPDIR}/${PREFIX}/MODULE.bazel.bak" + +# Verify the patch was applied successfully. +# Check only within the module() block to avoid false-passing on +# bazel_dep versions that happen to match the tag version. +if ! sed -n '/^module(/,/^)/p' "${TMPDIR}/${PREFIX}/MODULE.bazel" | grep -q "version = \"${VERSION}\""; then + echo "::error::Failed to patch MODULE.bazel version to ${VERSION}." + echo "::error::Check that MODULE.bazel has a module() block with a version field." + exit 1 +fi + +# Re-create the archive from the patched tree with deterministic metadata. +# GNU tar (used in CI on ubuntu-latest) supports --sort, --mtime, --owner, --group. +# On macOS, bsdtar does not support these flags; fall back to basic tar. +TAR_REPRO_OPTS="" +if tar --help 2>&1 | grep -q -- '--sort='; then + TAR_REPRO_OPTS="--sort=name --mtime=@0 --owner=0 --group=0 --numeric-owner" +fi +# gzip -n strips the gzip timestamp for reproducible output (portable). +tar -cf - -C "${TMPDIR}" ${TAR_REPRO_OPTS} "${PREFIX}" | gzip -n > "${ARCHIVE}" echo "Archive created: $(ls -lh "${ARCHIVE}" | awk '{print $5}')" -# Compute integrity hash (SHA256 in SRI format for MODULE.bazel snippets) SHA256=$(shasum -a 256 "${ARCHIVE}" | cut -d' ' -f1) SRI="sha256-$(echo -n "${SHA256}" | xxd -r -p | base64)"