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:
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user