fix(release): link the draft at cut time, not a page that 404s until green

The stable cut already creates the draft on the claim ref before it
dispatches the gate, but the output pointed at releases/tag/<claim> and
then at releases/tag/v<version> "when it is green". GitHub serves a draft
only at an untagged-* URL, so neither link showed it. Operators read that
as "the draft appears after the build".

Take the URL gh release create prints (the release's html_url) and print
it up front, with a note that notes edited during the build survive:
edit_draft_release keeps the body and strips only the warning fence. The
v<version> URL stays, labelled as where the release lives once published.

The canary resume path had the same broken releases/tag/<tag> link; it
now uses the create output or the url field of gh release view.
This commit is contained in:
ethernet
2026-09-23 19:20:27 -04:00
parent 0384a24edc
commit c58744e59e
3 changed files with 23 additions and 11 deletions

View File

@@ -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/<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:

View File

@@ -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/<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))

View File

@@ -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/<claim>.
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