Merge pull request #138592 from NousResearch/ethie/release-branches

ci(release): admit stable releases from release/* branches, not just main
This commit is contained in:
Teknium authored and GitHub committed 2026-10-14 13:18:58 -07:00
commit d04056d609
8 files changed
+90 -28

No files matched your search

@@ -2136,6 +2136,7 @@ jobs:
from pathlib import Path
from scripts.releases.handoff import fetch
from scripts.releases.semver import is_valid_version
from scripts.releases.stable import release_line_contains
tag = os.environ['PREVIOUS_TAG']
if not tag.startswith('v') or not is_valid_version(tag[1:]):
raise ValueError('Invalid previous release tag')
@@ -2145,7 +2146,8 @@ jobs:
if release['tagName'] != tag or release['isDraft']:
raise ValueError('Previous package must come from a published release')
commit = subprocess.check_output(['git', 'rev-parse', '--verify', f'refs/tags/{tag}^{{commit}}'], text=True).strip()
subprocess.run(['git', 'merge-base', '--is-ancestor', commit, 'origin/main'], check=True)
if not subprocess.check_output(release_line_contains(commit, 'origin'), text=True).strip():
raise ValueError('Previous package must come from main or a release/* branch')
fetch(tag, commit, ['termux'], Path('termux-build/previous'), ['deb/*.deb'])
PY
+1 -1
View File
@@ -647,7 +647,7 @@ def main():
release_cmd = subcommands.add_parser(
"release", help="Claim a version, cut a draft, and dispatch the gate")
release_cmd.add_argument("--commit", required=True, metavar="SHA",
help="The main commit to release")
help="The commit to release; it must be on main or a release/* branch")
release_cmd.add_argument("--bump", choices=["major", "minor", "patch"], default="patch")
release_cmd.add_argument("--autopublish", action="store_true",
help="Publish on green instead of leaving a draft")
+11 -14
View File
@@ -26,6 +26,8 @@ from scripts.releases.versioning import (
WORKFLOW = "stable-release.yml"
# A fetch refspec may hold one ``*``; the parsers filter what the globs over-match.
_ATTEMPT_GLOBS = ("rc.*", "abandoned-rc.*")
# Local namespace for the remote's release-line branches, kept apart from the user's own remotes.
_RELEASE_LINE_PREFIX = "hermes-release"
class ReleaseRefused(RuntimeError):
@@ -60,21 +62,16 @@ def _claim_commit(repo: Path, tag: str) -> str:
def _refresh_claims(repo: Path, remote: str) -> None:
_git(
repo, "fetch", remote,
"+refs/heads/main:refs/remotes/hermes-release/main",
*(f"+refs/tags/{glob}:refs/tags/{glob}" for glob in _ATTEMPT_GLOBS),
)
# Pruned, so a deleted release/* branch stops admitting commits. Tags are
# fetched separately: pruning there would delete local-only claim tags.
_git(repo, "fetch", "--prune", remote, *stable.release_line_refspecs(_RELEASE_LINE_PREFIX))
_git(repo, "fetch", remote, *(f"+refs/tags/{glob}:refs/tags/{glob}" for glob in _ATTEMPT_GLOBS))
def _require_remote_main(repo: Path, commit: str) -> None:
result = subprocess.run(
["git", "merge-base", "--is-ancestor", commit, "refs/remotes/hermes-release/main"],
cwd=repo, capture_output=True,
check=False,
)
if result.returncode != 0:
raise ReleaseRefused(f"{commit} is not on origin/main")
def _require_release_line(repo: Path, commit: str) -> None:
"""The commit must be on the remote's ``main`` or a ``release/*`` branch."""
if not _git(repo, *stable.release_line_contains(commit, _RELEASE_LINE_PREFIX)[1:]):
raise ReleaseRefused(f"{commit} is not on the remote's main or a release/* branch")
def _claim_collision(repo: Path, remote: str, tag: str, error: Exception) -> ReleaseRefused:
@@ -243,7 +240,7 @@ def release(commit: str, *, bump: str, repo: Path, remote: str, repository: str,
and the cut goes on; no confirmer, or a no, is the refusal.
"""
_refresh_claims(repo, remote)
_require_remote_main(repo, commit)
_require_release_line(repo, commit)
outstanding = _outstanding_attempt(repo, remote)
refusal = None
if outstanding is not None:
+28 -10
View File
@@ -34,12 +34,33 @@ SMOKE_TARGETS = {"smoke-darwin-arm64": "macos/arm64", "smoke-darwin-x64": "macos
"smoke-win32-arm64": "windows/arm64", "smoke-win32-x64": "windows/x64"}
# Stable releases are cut from ``main`` or from a ``release/*`` maintenance branch.
RELEASE_LINE_BRANCHES = ("main", "release/*")
def release_line_refspecs(remote_prefix: str) -> list[str]:
"""Fetch refspecs that mirror every release-line branch under ``refs/remotes/<remote_prefix>/``."""
return [f"+refs/heads/{branch}:refs/remotes/{remote_prefix}/{branch}"
for branch in RELEASE_LINE_BRANCHES]
def release_line_contains(commit: str, remote_prefix: str) -> list[str]:
"""The git command that lists the fetched release-line branches holding ``commit``.
Empty output means the commit is on no release line. Git matches the
``release/*`` pattern across the slash, so ``release/0.22.1`` qualifies.
"""
return ["git", "for-each-ref", "--contains", commit, "--format=%(refname)",
*(f"refs/remotes/{remote_prefix}/{branch}" for branch in RELEASE_LINE_BRANCHES)]
def admit_claim(tag: str, commit: str, *, on_main) -> dict:
"""Admit a release from its attempt ref. The checkout version is not read.
``main`` carries ``0.0.0`` on purpose, so the version and the attempt come
from ``rc.<N>-vX.Y.Z`` and the only question about the commit is whether
it is on ``main``.
it is on a release line: ``main`` or a ``release/*`` branch. ``on_main`` is
that predicate; the name predates release branches.
"""
from scripts.releases.versioning import parse_attempt_ref
@@ -48,7 +69,7 @@ def admit_claim(tag: str, commit: str, *, on_main) -> dict:
raise ValueError(f"{tag} is not a claim tag")
version, attempt = parsed
if not on_main(commit):
raise ValueError(f"{commit} is not on main")
raise ValueError(f"{commit} is not on main or a release/* branch")
return {"claim_tag": tag, "tag": f"v{version}", "version": version, "attempt": attempt,
"commit": commit}
@@ -542,14 +563,10 @@ def check_claim(env: dict, run=output) -> dict:
if (remote_commit != commit or remote_object != local_object
or (expected_object and remote_object != expected_object)):
raise ValueError("Stable claim tag moved")
run(["git", "fetch", "origin", "+refs/heads/main:refs/remotes/origin/main"])
run(["git", "fetch", "origin", *release_line_refspecs("origin")])
def on_main(sha: str) -> bool:
try:
run(["git", "merge-base", "--is-ancestor", sha, "origin/main"])
except subprocess.CalledProcessError:
return False
return True
return bool(run(release_line_contains(sha, "origin")))
admitted = admit_claim(claim_tag, commit, on_main=on_main)
raw_metadata = run(["git", "tag", "-l", claim_tag, "--format=%(contents)"])
@@ -595,7 +612,7 @@ def final_context(env: dict, run=output) -> tuple[str, str, dict]:
or not refs.get(f"refs/tags/{tag}")):
raise ValueError("Final release tag custody changed")
run(["git", "fetch", "origin", "+refs/heads/main:refs/remotes/origin/main",
run(["git", "fetch", "origin", *release_line_refspecs("origin"),
f"+refs/tags/{claim_tag}:refs/tags/{claim_tag}",
f"+refs/tags/{tag}:refs/tags/{tag}"])
for receipt, expected_object in ((claim_tag, claim_object),
@@ -603,7 +620,8 @@ def final_context(env: dict, run=output) -> tuple[str, str, dict]:
local_object = run(["git", "rev-parse", f"refs/tags/{receipt}"])
if local_object != expected_object or run(["git", "cat-file", "-t", local_object]) != "tag":
raise ValueError("Final release local tag differs from the remote")
run(["git", "merge-base", "--is-ancestor", commit, "origin/main"])
if not run(release_line_contains(commit, "origin")):
raise ValueError(f"{commit} is not on main or a release/* branch")
claim = _claim_metadata(
run(["git", "tag", "-l", claim_tag, "--format=%(contents)"]),
version=admitted["version"], attempt=admitted["attempt"], commit=commit,
+20 -1
View File
@@ -227,6 +227,25 @@ def test_claim_on_origin_main_is_admitted_and_exports_the_full_sha(
assert f"sha={expected}" in gh_output
def test_claim_on_a_release_branch_is_admitted(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
origin, clone = _seed_repo(tmp_path)
# A hotfix lands on origin's release/0.1.2 only; main never sees it.
_git("checkout", "-b", "release/0.1.2", cwd=origin)
(origin / "fix.txt").write_text("hotfix\n", encoding="utf-8")
_git("add", "-A", cwd=origin)
_git("commit", "-m", "hotfix", cwd=origin)
_git("checkout", "main", cwd=origin)
_git("fetch", "origin", cwd=clone)
_git("checkout", "--detach", "origin/release/0.1.2", cwd=clone)
monkeypatch.setenv("GIT_COMMITTER_DATE", "@1790000000 +0000")
_git("tag", "-a", "rc.1-v0.1.2", "-m", _claim_message(clone), cwd=clone)
_git("push", "origin", "refs/tags/rc.1-v0.1.2", cwd=clone)
proc = _run_admission(clone, "v0.1.2", "rc.1-v0.1.2")
assert proc.returncode == 0, proc.stdout + proc.stderr
assert f"sha={_git('rev-parse', 'HEAD', cwd=clone)}" in (clone / "github_output.txt").read_text(encoding="utf-8")
def test_claim_not_on_origin_main_is_refused(tmp_path: Path):
_origin, clone = _seed_repo(tmp_path)
# A commit that exists ONLY in the clone — never pushed, never reviewed.
@@ -238,7 +257,7 @@ def test_claim_not_on_origin_main_is_refused(tmp_path: Path):
proc = _run_admission(clone, "v0.1.2", "rc.1-v0.1.2")
assert proc.returncode != 0, "a tag off origin/main must not be admitted"
assert "is not on main" in proc.stdout + proc.stderr
assert "is not on main or a release/* branch" in proc.stdout + proc.stderr
# And nothing was exported for the signing jobs to consume.
assert "sha=" not in (clone / "github_output.txt").read_text(encoding="utf-8")
+24
View File
@@ -144,6 +144,30 @@ def test_release_claims_the_first_attempt_creates_a_draft_and_dispatches(source)
]
def test_a_commit_only_on_a_release_branch_can_be_cut(source):
git(source, "checkout", "--quiet", "-b", "release/0.21.5")
git(source, "commit", "--allow-empty", "--quiet", "-m", "hotfix")
git(source, "push", "--quiet", "origin", "release/0.21.5")
commit = git(source, "rev-parse", "HEAD")
git(source, "checkout", "--quiet", "main")
result = _release(source, commit)
assert result["tag"] == "rc.1-v0.21.5"
assert git(source, "rev-parse", "rc.1-v0.21.5^{commit}") == commit
def test_a_commit_on_neither_main_nor_a_release_branch_is_refused(source):
from scripts.releases.entrypoint import ReleaseRefused
git(source, "checkout", "--quiet", "-b", "feature/x")
git(source, "commit", "--allow-empty", "--quiet", "-m", "wip")
git(source, "push", "--quiet", "origin", "feature/x")
with pytest.raises(ReleaseRefused, match="not on the remote's main or a release/"):
_release(source, git(source, "rev-parse", "HEAD"), execute=_must_not_execute)
def test_release_output_names_the_ci_run_and_the_publish_step():
from scripts.releases.entrypoint import next_steps
+1 -1
View File
@@ -34,7 +34,7 @@ def test_admission_reads_the_version_and_attempt_from_the_attempt_ref():
def test_admission_refuses_a_claim_for_a_commit_off_main():
from scripts.releases.stable import admit_claim
with pytest.raises(ValueError, match="not on main"):
with pytest.raises(ValueError, match="not on main or a release/"):
admit_claim("rc.1-v0.21.5", "b" * 40, on_main=lambda _sha: False)
+2
View File
@@ -435,6 +435,8 @@ def test_claim_object_movement_and_lightweight_tags_fail_closed(tmp_path, monkey
return claim_object if argv[-1] == ref else commit
if argv[1] == "tag":
return json.dumps(message)
if argv[1] == "for-each-ref":
return "refs/remotes/origin/release/1.2.3"
return ""
assert check_claim(env, git) == {