From 9642882babed0a854f2b972016d9327dc06b8ec1 Mon Sep 17 00:00:00 2001 From: ethernet Date: Mon, 21 Sep 2026 22:04:34 -0400 Subject: [PATCH] fix(release): create stable drafts before dispatch --- scripts/releases/entrypoint.py | 18 +++++++++++++----- tests/scripts/test_release_entrypoint.py | 22 ++++++++++++++-------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/scripts/releases/entrypoint.py b/scripts/releases/entrypoint.py index 2e852e77aa..33f4cc3046 100644 --- a/scripts/releases/entrypoint.py +++ b/scripts/releases/entrypoint.py @@ -62,7 +62,7 @@ def _require_ancestry(repo: Path, commit: str) -> None: def release(commit: str, *, bump: str, repo: Path, remote: str, repository: str, - dispatch, autopublish: bool = False) -> dict: + execute, autopublish: bool = False) -> dict: """Claim the derived version, cut its draft, and start the gate.""" _require_ancestry(repo, commit) version = derive_next_version(published=None, claims=_claims(repo), bump=bump) @@ -71,7 +71,15 @@ def release(commit: str, *, bump: str, repo: Path, remote: str, repository: str, _git(repo, "push", remote, f"refs/tags/{tag}") url = f"https://github.com/{repository}/releases/tag/{tag}" try: - dispatch(["gh", "workflow", "run", WORKFLOW, "--ref", tag, "--repo", repository]) + execute([ + "gh", "release", "create", tag, "--repo", repository, + "--verify-tag", "--draft", "--generate-notes", "--title", f"Hermes Agent v{version}", + ]) + execute([ + "gh", "workflow", "run", WORKFLOW, "--ref", tag, "--repo", repository, + "--raw-field", f"tag={tag}", + "--raw-field", f"autopublish={str(autopublish).lower()}", + ]) except Exception as exc: raise ReleaseRefused(f"release {tag} never started: {exc}") from exc return {"version": version, "tag": tag, "commit": commit, "url": url, @@ -104,11 +112,11 @@ def cmd_release(args) -> None: raise SystemExit(f"release: remote {remote!r} does not point at a GitHub repository") commit = _git(repo, "rev-parse", "--verify", f"{args.commit}^{{commit}}") - def dispatch(command: list[str]) -> None: + def execute(command: list[str]) -> None: completed = subprocess.run(command, cwd=repo, capture_output=True, text=True, encoding="utf-8") if completed.returncode != 0: - raise RuntimeError(completed.stderr.strip() or "workflow dispatch failed") + raise RuntimeError(completed.stderr.strip() or "release command failed") result = release(commit, bump=args.bump, repo=repo, remote=remote, repository=repository, - dispatch=dispatch, autopublish=args.autopublish) + execute=execute, autopublish=args.autopublish) print(result["url"]) diff --git a/tests/scripts/test_release_entrypoint.py b/tests/scripts/test_release_entrypoint.py index 8ef87b0a3b..8389cfd4f2 100644 --- a/tests/scripts/test_release_entrypoint.py +++ b/tests/scripts/test_release_entrypoint.py @@ -36,21 +36,26 @@ def _claim(repo, version, commit, actor="release-bot", when="2026-09-22T00:14:00 git(repo, "push", "--quiet", "origin", f"refs/tags/v{version}-rc") -def test_release_claims_the_derived_version_and_dispatches(source): +def test_release_claims_the_derived_version_creates_a_draft_and_dispatches(source): from scripts.releases.entrypoint import release commit = git(source, "rev-parse", "HEAD") calls = [] result = release(commit, bump="patch", repo=source, remote="origin", - repository="example/hermes-agent", dispatch=calls.append) + repository="example/hermes-agent", execute=calls.append, autopublish=True) assert result["version"] == "0.21.5" assert result["tag"] == "v0.21.5-rc" assert result["commit"] == commit assert result["url"] == "https://github.com/example/hermes-agent/releases/tag/v0.21.5-rc" assert git(source, "rev-parse", "v0.21.5-rc^{commit}") == commit - assert calls == [["gh", "workflow", "run", "stable-release.yml", - "--ref", "v0.21.5-rc", "--repo", "example/hermes-agent"]] + assert calls == [ + ["gh", "release", "create", "v0.21.5-rc", "--repo", "example/hermes-agent", + "--verify-tag", "--draft", "--generate-notes", "--title", "Hermes Agent v0.21.5"], + ["gh", "workflow", "run", "stable-release.yml", "--ref", "v0.21.5-rc", + "--repo", "example/hermes-agent", "--raw-field", "tag=v0.21.5-rc", + "--raw-field", "autopublish=true"], + ] # The claim push names the claim ref and nothing else. pushed = git(source, "ls-remote", "origin", "refs/tags/v0.21.5-rc") assert pushed.startswith(git(source, "rev-parse", "v0.21.5-rc")) @@ -66,19 +71,20 @@ def test_a_commit_behind_an_outstanding_claim_is_refused(source): with pytest.raises(ReleaseRefused, match="0\\.21\\.5 already claimed"): release(earlier, bump="patch", repo=source, remote="origin", - repository="example/hermes-agent", dispatch=lambda _cmd: pytest.fail("must not dispatch")) + repository="example/hermes-agent", execute=lambda _cmd: pytest.fail("must not execute")) assert "v0.21.6-rc" not in git(source, "tag", "--list") def test_a_dispatch_that_never_starts_is_an_error(source): from scripts.releases.entrypoint import ReleaseRefused, release - def refuse(_cmd): - raise RuntimeError("workflow dispatch rejected") + def refuse(command): + if command[1:3] == ["workflow", "run"]: + raise RuntimeError("workflow dispatch rejected") with pytest.raises(ReleaseRefused, match="never started"): release(git(source, "rev-parse", "HEAD"), bump="patch", repo=source, remote="origin", - repository="example/hermes-agent", dispatch=refuse) + repository="example/hermes-agent", execute=refuse) # The claim stands: a failed start burns the version rather than retrying it. assert "v0.21.5-rc" in git(source, "tag", "--list")