Merge pull request #128348 from NousResearch/fix/release-claim-copy
Release cut output shows its CI run link and leads with the next action
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