diff --git a/scripts/release.py b/scripts/release.py index cd82224098..8cd5231623 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -2526,9 +2526,11 @@ def _resume_canary(tag: str, remote: str, repository: str, *, notes_file: Path | raise ValueError("Canary receipt differs from its exact remote tag object") view = subprocess.run( - ["gh", "release", "view", tag, "--repo", repository, "--json", "tagName,isDraft,isPrerelease"], + ["gh", "release", "view", tag, "--repo", repository, "--json", "tagName,isDraft,isPrerelease,url"], cwd=REPO_ROOT, capture_output=True, text=True, encoding="utf-8", ) + # A draft is served at an untagged-* URL, never releases/tag/; gh's + # answer is the only working link to it. if view.returncode != 0: create = [ "gh", "release", "create", tag, "--repo", repository, @@ -2541,11 +2543,13 @@ def _resume_canary(tag: str, remote: str, repository: str, *, notes_file: Path | ) if created.returncode != 0: raise ValueError(created.stderr.strip() or "Canary draft could not be recovered") + draft_url = created.stdout.strip() else: release = json.loads(view.stdout) if (release.get("tagName") != tag or not isinstance(release.get("isDraft"), bool) or release.get("isPrerelease") is not True): raise ValueError("Canary release state differs from its receipt") + draft_url = release.get("url") or "" from scripts.releases.versioning import published_channel_identity published = published_channel_identity(repository, "canary") @@ -2557,7 +2561,7 @@ def _resume_canary(tag: str, remote: str, repository: str, *, notes_file: Path | print(f"Resumed canary publication for {tag}.") print(f"Workflow: https://github.com/{repository}/actions/workflows/desktop-bundled-release.yml") print("Wait for that workflow to finish. It builds this canary and publishes the draft when the build is green.") - print(f"The draft is at https://github.com/{repository}/releases/tag/{tag}.") + print(f"The draft is at {draft_url}.") def cmd_canary(args) -> None: diff --git a/scripts/releases/entrypoint.py b/scripts/releases/entrypoint.py index c2b8e29742..bbcc9ec539 100644 --- a/scripts/releases/entrypoint.py +++ b/scripts/releases/entrypoint.py @@ -198,7 +198,6 @@ def release(commit: str, *, bump: str, repo: Path, remote: str, repository: str, # different version passes the same check. Re-read before anything starts. _refresh_claims(repo, remote) _outstanding_attempt(repo, remote) - url = f"https://github.com/{repository}/releases/tag/{tag}" try: notes = json.loads(execute([ "gh", "api", f"repos/{repository}/releases/generate-notes", @@ -213,11 +212,14 @@ def release(commit: str, *, bump: str, repo: Path, remote: str, repository: str, finally: file.close() try: - execute([ + # gh prints the new release's page. A draft is not reachable + # through releases/tag/ (GitHub serves it at an untagged-* + # URL), so this printed page is the only link to it. + url = (execute([ "gh", "release", "create", tag, "--repo", repository, "--verify-tag", "--draft", "--notes-file", file.name, "--title", f"Hermes Agent v{version}", - ]) + ]) or "").strip() finally: os.unlink(file.name) execute([ @@ -351,15 +353,17 @@ def next_steps(result: dict) -> str: lines = [ f"Claimed {result['tag']} for v{version}. The release workflow started on {result['tag']}.", f"Workflow: {result['run_url']}" if result.get("run_url") else "Workflow: the run is not listed yet. Open the Actions tab for this claim.", + f"Draft release: {result['url']}", + "The draft exists now. Edit its notes while the workflow runs; the edits carry through to publication.", "Wait for that workflow to finish. It builds and tests this commit.", - f"When it is green, the release notes are at {result['final_url']}.", ] if result["autopublish"]: lines.append("Autopublish is on. A green workflow publishes the release. You do not run publish.") else: - lines.append("Autopublish is off. The release stays a draft.") - lines.append("Edit the notes at that page, then publish the release to push this build live:") + lines.append("Autopublish is off. The release stays a draft after the workflow is green.") + lines.append("When it is green, publish the release to push this build live:") lines.append(f" python scripts/release.py publish --version {version} --remote origin") + lines.append(f"After publication the release is at {result['final_url']}.") return "\n".join(lines) @@ -386,7 +390,6 @@ def cmd_release(args) -> None: execute=execute, autopublish=args.autopublish, published=published_stable_identity(repository), ) - print(result["url"]) print(next_steps(result)) diff --git a/tests/scripts/test_release_entrypoint.py b/tests/scripts/test_release_entrypoint.py index 43224264dd..24c55ddea5 100644 --- a/tests/scripts/test_release_entrypoint.py +++ b/tests/scripts/test_release_entrypoint.py @@ -85,6 +85,8 @@ def test_release_claims_the_first_attempt_creates_a_draft_and_dispatches(source) if command[:3] == ["gh", "run", "list"]: return json.dumps([{"databaseId": 7, "url": "https://github.com/example/hermes-agent/actions/runs/7", "headBranch": "rc.1-v0.21.5", "status": "queued"}]) + if command[:3] == ["gh", "release", "create"]: + return "https://github.com/example/hermes-agent/releases/tag/untagged-0123abcd\n" return "" result = _release(source, commit, execute=execute, autopublish=True) @@ -92,7 +94,8 @@ def test_release_claims_the_first_attempt_creates_a_draft_and_dispatches(source) assert result["version"] == "0.21.5" assert result["tag"] == "rc.1-v0.21.5" assert result["commit"] == commit - assert result["url"] == "https://github.com/example/hermes-agent/releases/tag/rc.1-v0.21.5" + # A draft lives at the page gh reports, never at releases/tag/. + assert result["url"] == "https://github.com/example/hermes-agent/releases/tag/untagged-0123abcd" assert result["final_url"] == "https://github.com/example/hermes-agent/releases/tag/v0.21.5" assert git(source, "rev-parse", "rc.1-v0.21.5^{commit}") == commit claim = json.loads(git(source, "tag", "-l", "rc.1-v0.21.5", "--format=%(contents)")) @@ -132,11 +135,13 @@ def test_release_output_names_the_wait_and_the_publish_step(): result = {"version": "0.21.5", "tag": "rc.1-v0.21.5", "autopublish": False, "run_url": "https://github.com/example/hermes-agent/actions/runs/7", + "url": "https://github.com/example/hermes-agent/releases/tag/untagged-0123abcd", "final_url": "https://github.com/example/hermes-agent/releases/tag/v0.21.5"} text = next_steps(result) assert "Workflow: " + result["run_url"] in text assert "The release workflow started on rc.1-v0.21.5." in text - assert "Wait for that workflow to finish." in text + # The draft is reachable before the workflow is green, not only after it. + assert text.index(result["url"]) < text.index("Wait for that workflow to finish.") assert result["final_url"] in text assert "python scripts/release.py publish --version 0.21.5 --remote origin" in text