From e2ace6aa6bafea9c8f11ac258a024f84044b79db Mon Sep 17 00:00:00 2001 From: Nico Ritschel Date: Mon, 5 Oct 2026 06:16:44 -0700 Subject: [PATCH] Verify release tags without replacing checkout refs --- .../workflows/duckdb-extension-release.yml | 6 +- .../test/test_release_workflow.py | 73 +++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/.github/workflows/duckdb-extension-release.yml b/.github/workflows/duckdb-extension-release.yml index 6a6242d7..593fd8c0 100644 --- a/.github/workflows/duckdb-extension-release.yml +++ b/.github/workflows/duckdb-extension-release.yml @@ -183,8 +183,10 @@ jobs: RELEASE_COMMIT: ${{ github.sha }} run: | set -euo pipefail - git fetch --no-tags origin "refs/tags/$TAG:refs/tags/$TAG" - if [ "$(git rev-parse "$TAG^{commit}")" != "$RELEASE_COMMIT" ]; then + # Checkout can create a lightweight local ref for an annotated tag. + # Read the remote tag through FETCH_HEAD without replacing that ref. + git fetch --no-tags origin "refs/tags/$TAG" + if [ "$(git rev-parse 'FETCH_HEAD^{commit}')" != "$RELEASE_COMMIT" ]; then echo "Release tag does not identify the built commit" >&2 exit 1 fi diff --git a/sidemantic-duckdb/test/test_release_workflow.py b/sidemantic-duckdb/test/test_release_workflow.py index f817b58c..10120fd4 100644 --- a/sidemantic-duckdb/test/test_release_workflow.py +++ b/sidemantic-duckdb/test/test_release_workflow.py @@ -135,3 +135,76 @@ def test_publication_checks_provenance_and_all_payloads_before_creating_release( assert "--verify-tag" in arguments assert "--clobber" not in arguments assert sum(arg.endswith(".duckdb_extension") for arg in arguments) == len(PLATFORMS) + + +@pytest.mark.parametrize("matching_commit", [True, False]) +def test_publication_verifies_remote_annotated_tag_without_replacing_checkout_ref(tmp_path, matching_commit): + def git(directory, *arguments): + return subprocess.run( + [ + "git", + "-c", + "user.name=Release test", + "-c", + "user.email=release@example.invalid", + "-c", + "commit.gpgsign=false", + "-c", + "tag.gpgSign=false", + *arguments, + ], + cwd=directory, + check=True, + capture_output=True, + text=True, + ).stdout.strip() + + source = tmp_path / "source" + source.mkdir() + git(source, "init", "-q") + (source / "LICENSE").write_text("Initial license\n") + git(source, "add", "LICENSE") + git(source, "commit", "-qm", "Initial source") + earlier_commit = git(source, "rev-parse", "HEAD") + (source / "LICENSE").write_text("Release license\n") + git(source, "commit", "-qam", "Release source") + tagged_commit = git(source, "rev-parse", "HEAD") + tag = "sidemantic-duckdb-v0.1.0" + git(source, "tag", "-a", tag, "-m", "Release") + checkout = tmp_path / "checkout" + git(tmp_path, "clone", "-q", "--no-tags", str(source), str(checkout)) + built_commit = tagged_commit if matching_commit else earlier_commit + git(checkout, "checkout", "-q", "--detach", built_commit) + # actions/checkout can synthesize a lightweight ref at the built commit. + git(checkout, "tag", tag, built_commit) + dist = checkout / "dist" + dist.mkdir() + for platform in PLATFORMS: + asset = dist / f"sidemantic-duckdb-1.5.6-{platform}.duckdb_extension" + asset.write_bytes(platform.encode()) + asset.with_suffix(".duckdb_extension.sha256").write_text( + f"{hashlib.sha256(asset.read_bytes()).hexdigest()} {asset.name}\n" + ) + commands = tmp_path / "bin" + commands.mkdir() + gh = commands / "gh" + gh.write_text('#!/bin/sh\nprintf "%s\\n" "$@" > "$GH_ARGS"\n') + gh.chmod(0o755) + args = tmp_path / "gh-args" + step = workflow_step("github-release", "Verify and publish immutable artifacts") + result = subprocess.run( + ["bash", "-e", "-c", step["run"]], + cwd=checkout, + env={ + **os.environ, + "PATH": f"{commands}{os.pathsep}{os.environ['PATH']}", + "TAG": tag, + "RELEASE_COMMIT": built_commit, + "GH_ARGS": str(args), + }, + capture_output=True, + text=True, + ) + assert (result.returncode == 0) == matching_commit, result.stderr + assert args.exists() == matching_commit + assert git(checkout, "rev-parse", f"refs/tags/{tag}") == built_commit