fix: release cut output waits for its CI run and leads with the next action
The cut printed before GitHub listed the dispatched run, so it almost always said "the run is not listed yet". It now polls up to 30s for the run, and when none lists it links the stable-release workflow page. The copy is shorter: Release CI, the skipped-tests warning (bold on a tty), the draft link, and the publish command on its own line.
This commit is contained in:
@@ -180,12 +180,15 @@ def _changelog(repo: Path, repository: str, *, commit: str, tag: str, version: s
|
||||
def release(commit: str, *, bump: str, repo: Path, remote: str, repository: str,
|
||||
execute, autopublish: bool = False, no_changelog: bool = False,
|
||||
skip_bundles: bool = False, skip_tests: bool = False,
|
||||
published: tuple[str, str | None] = (SEED, None)) -> dict:
|
||||
published: tuple[str, str | None] = (SEED, None),
|
||||
run_wait: float = 0, sleep=time.sleep) -> dict:
|
||||
"""Claim the next attempt of the derived version, cut its draft, and start the gate.
|
||||
|
||||
``published`` is the stable channel's ``(version, commit)``; the commit is
|
||||
None before the first publication. ``skip_bundles`` and ``skip_tests`` are
|
||||
written into the claim, which is the one record every later job reads.
|
||||
``run_wait`` is how many seconds to keep looking for the dispatched run;
|
||||
GitHub lists it a moment after the dispatch returns.
|
||||
"""
|
||||
_refresh_claims(repo, remote)
|
||||
_require_remote_main(repo, commit)
|
||||
@@ -267,19 +270,33 @@ def release(commit: str, *, bump: str, repo: Path, remote: str, repository: str,
|
||||
"gh", "workflow", "run", WORKFLOW, "--ref", tag, "--repo", repository,
|
||||
"--raw-field", f"tag={tag}",
|
||||
])
|
||||
found = execute([
|
||||
"gh", "run", "list", "--repo", repository, "--workflow", WORKFLOW,
|
||||
"--branch", tag, "--json", "databaseId,url,headBranch,status",
|
||||
])
|
||||
run_url = _await_run(execute, repository, tag, wait=run_wait, sleep=sleep)
|
||||
except Exception as exc:
|
||||
raise ReleaseRefused(f"release {tag} never started: {exc}") from exc
|
||||
run_url = _dispatched_run(found, tag)
|
||||
return {"version": version, "tag": tag, "commit": commit, "url": url,
|
||||
return {"version": version, "tag": tag, "commit": commit, "url": url, "repository": repository,
|
||||
"final_url": f"https://github.com/{repository}/releases/tag/v{version}",
|
||||
"run_url": run_url, "autopublish": autopublish,
|
||||
"skip_bundles": skip_bundles, "skip_tests": skip_tests}
|
||||
|
||||
|
||||
RUN_POLL_SECONDS = 2
|
||||
RUN_WAIT_SECONDS = 30
|
||||
|
||||
|
||||
def _await_run(execute, repository: str, tag: str, *, wait: float, sleep) -> str:
|
||||
"""The dispatched run's URL, polling up to ``wait`` seconds. Empty if it never lists."""
|
||||
polls = int(wait // RUN_POLL_SECONDS)
|
||||
for poll in range(polls + 1):
|
||||
run_url = _dispatched_run(execute([
|
||||
"gh", "run", "list", "--repo", repository, "--workflow", WORKFLOW,
|
||||
"--branch", tag, "--json", "databaseId,url,headBranch,status",
|
||||
]), tag)
|
||||
if run_url or poll == polls:
|
||||
return run_url
|
||||
sleep(RUN_POLL_SECONDS)
|
||||
return ""
|
||||
|
||||
|
||||
def _dispatched_run(raw: str, tag: str) -> str:
|
||||
"""The run URL for the claim ref. Empty when the list has no such run yet."""
|
||||
try:
|
||||
@@ -389,29 +406,33 @@ def abandon(version: str, *, repo: Path, remote: str, repository: str, delete, i
|
||||
return {"version": version, "tag": tag, "marker": marker, "repository": repository}
|
||||
|
||||
|
||||
def next_steps(result: dict) -> str:
|
||||
"""Say what started, what the operator waits for, and the next action."""
|
||||
version = result["version"]
|
||||
def next_steps(result: dict, *, bold: bool = False) -> str:
|
||||
"""Say what was attempted, where its CI runs, and the one command that publishes it.
|
||||
|
||||
``bold`` wraps the skipped-tests warning in ANSI bold; the caller decides
|
||||
from the terminal, so piped output stays plain.
|
||||
"""
|
||||
version, tag = result["version"], result["tag"]
|
||||
actions_url = f"https://github.com/{result['repository']}/actions/workflows/{WORKFLOW}"
|
||||
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"Attempting release {tag} for v{version}.",
|
||||
"Release CI: " + (result.get("run_url")
|
||||
or f"the run is not listed yet. Find {tag} at {actions_url}"),
|
||||
]
|
||||
if result["skip_bundles"]:
|
||||
lines.append("Bundles are skipped. Only the tag, the GitHub release and the Docker image "
|
||||
"ship; the desktop, Termux and Store channels stay on the previous release.")
|
||||
if result["skip_tests"]:
|
||||
lines.append("Tests are skipped. No CI, E2E, native smoke or upgrade acceptance job runs. "
|
||||
"The build is published untested.")
|
||||
warning = "Tests are skipped, since you passed --skip-tests"
|
||||
lines.append(f"\033[1m{warning}\033[0m" if bold else warning)
|
||||
lines.append(f"Draft release: {result['url']}")
|
||||
if result["autopublish"]:
|
||||
lines.append("Autopublish is on. A green workflow publishes the release. You do not run publish.")
|
||||
lines.append("Edit the notes in the draft release. Autopublish is on: "
|
||||
f"when Release CI is green, v{version} publishes itself.")
|
||||
else:
|
||||
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("Edit the notes in the draft release. "
|
||||
f"When Release CI is green, to publish v{version}, run:")
|
||||
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)
|
||||
|
||||
|
||||
@@ -437,9 +458,9 @@ def cmd_release(args) -> None:
|
||||
commit, bump=args.bump, repo=repo, remote=remote, repository=repository,
|
||||
execute=execute, autopublish=args.autopublish, no_changelog=args.no_changelog,
|
||||
skip_bundles=args.skip_bundles, skip_tests=args.skip_tests,
|
||||
published=published_stable_identity(repository),
|
||||
published=published_stable_identity(repository), run_wait=RUN_WAIT_SECONDS,
|
||||
)
|
||||
print(next_steps(result))
|
||||
print(next_steps(result, bold=sys.stdout.isatty() and "NO_COLOR" not in os.environ))
|
||||
|
||||
|
||||
def _command_repository(args) -> tuple[Path, str, str]:
|
||||
|
||||
@@ -133,28 +133,63 @@ def test_release_claims_the_first_attempt_creates_a_draft_and_dispatches(source)
|
||||
]
|
||||
|
||||
|
||||
def test_release_output_names_the_wait_and_the_publish_step():
|
||||
def test_release_output_names_the_ci_run_and_the_publish_step():
|
||||
from scripts.releases.entrypoint import next_steps
|
||||
|
||||
result = {"version": "0.21.5", "tag": "rc.1-v0.21.5", "autopublish": False,
|
||||
"skip_bundles": False, "skip_tests": False,
|
||||
"skip_bundles": False, "skip_tests": False, "repository": "example/hermes-agent",
|
||||
"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"}
|
||||
"url": "https://github.com/example/hermes-agent/releases/tag/untagged-0123abcd"}
|
||||
text = next_steps(result)
|
||||
assert "Workflow: " + result["run_url"] in text
|
||||
assert "The release workflow started on rc.1-v0.21.5." 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
|
||||
assert "Attempting release rc.1-v0.21.5 for v0.21.5." in text
|
||||
assert "Release CI: " + result["run_url"] in text
|
||||
assert "Draft release: " + result["url"] in text
|
||||
# The publish command sits on its own line so it can be copied whole.
|
||||
assert "\n python scripts/release.py publish --version 0.21.5 --remote origin" in text
|
||||
assert "skipped" not in text
|
||||
|
||||
automatic = next_steps({**result, "autopublish": True})
|
||||
assert "Autopublish is on." in automatic
|
||||
assert "Autopublish is on" in automatic
|
||||
assert "publish --version" not in automatic
|
||||
assert "skipped" not in text
|
||||
|
||||
skipped = next_steps({**result, "skip_bundles": True, "skip_tests": True})
|
||||
assert "Bundles are skipped." in skipped and "Tests are skipped." in skipped
|
||||
assert "Bundles are skipped." in skipped
|
||||
assert "Tests are skipped, since you passed --skip-tests" in skipped
|
||||
assert "\033[" not in skipped
|
||||
assert "\033[1mTests are skipped" in next_steps({**result, "skip_tests": True}, bold=True)
|
||||
|
||||
|
||||
def test_an_unlisted_run_points_at_the_workflow_page():
|
||||
from scripts.releases.entrypoint import next_steps
|
||||
|
||||
text = next_steps({"version": "0.21.5", "tag": "rc.1-v0.21.5", "autopublish": False,
|
||||
"skip_bundles": False, "skip_tests": False,
|
||||
"repository": "example/hermes-agent", "run_url": "",
|
||||
"url": "https://github.com/example/hermes-agent/releases/tag/untagged-0123abcd"})
|
||||
assert "the run is not listed yet" in text
|
||||
assert "https://github.com/example/hermes-agent/actions/workflows/stable-release.yml" in text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("lists_on", "wait", "run_url", "naps"), [
|
||||
(3, 30, "https://github.com/example/hermes-agent/actions/runs/7", 2),
|
||||
(None, 4, "", 2),
|
||||
])
|
||||
def test_release_looks_for_the_dispatched_run_only_within_its_wait(source, lists_on, wait, run_url, naps):
|
||||
listed = json.dumps([{"databaseId": 7, "url": "https://github.com/example/hermes-agent/actions/runs/7",
|
||||
"headBranch": "rc.1-v0.21.5", "status": "queued"}])
|
||||
lists, slept = [], []
|
||||
|
||||
def execute(command):
|
||||
if command[:3] == ["gh", "run", "list"]:
|
||||
lists.append(command)
|
||||
return listed if len(lists) == lists_on else "[]"
|
||||
return ""
|
||||
|
||||
result = _release(source, git(source, "rev-parse", "HEAD"), execute=execute,
|
||||
run_wait=wait, sleep=slept.append)
|
||||
|
||||
assert result["run_url"] == run_url
|
||||
assert len(slept) == naps and len(lists) == naps + 1
|
||||
|
||||
|
||||
def test_a_final_tag_for_the_next_version_refuses_the_cut(source):
|
||||
|
||||
Reference in New Issue
Block a user