diff --git a/.github/scripts/check_pr.py b/.github/scripts/check_pr.py index c68f94c..7e5b4ec 100755 --- a/.github/scripts/check_pr.py +++ b/.github/scripts/check_pr.py @@ -10,6 +10,10 @@ no-op that passes on rc-* so hotfix/regression PRs are not forced to carry the full template). +Release-promotion PRs — a 'chore(release):' title from an 'rc-*' head branch, +the promote step of the release train — are exempt from both checks (V2-1092): +they are mechanical version-bump PRs with no issue or template of their own. + PR fields are read from the environment (set by the workflow from the event payload): PR_TITLE, PR_BODY, PR_BRANCH, PR_BASE. @@ -38,6 +42,14 @@ r"linear\.app/[^/\s]+/issue/[A-Za-z][A-Za-z0-9]*-[0-9]+", re.IGNORECASE ) +# Release-promotion PRs (the promote step of the release train) are mechanical +# version-bump PRs opened from an rc-* branch. Both checks self-pass on them — +# they must pass rather than be skipped because linear-link/pr-template are +# required status checks. Matched on BOTH the conventional-commit title prefix +# and the rc-* head branch so an ordinary PR cannot opt out via its title alone. +RELEASE_TITLE = re.compile(r"^chore\(release\):") +RC_BRANCH = re.compile(r"^rc-") + # Canonical section headings, exactly as they appear in the template, keyed by # their lower-cased form. Used so failure messages name the real heading. CANONICAL_HEADINGS = { @@ -72,6 +84,12 @@ def strip_comments(text): return re.sub(r"", "", text, flags=re.DOTALL) +def release_promotion(): + return bool( + RELEASE_TITLE.match(env("PR_TITLE")) and RC_BRANCH.match(env("PR_BRANCH")) + ) + + def linear_ref(*parts): """Return the first Linear URL/key found in the given (comment-free) parts.""" haystack = "\n".join(parts) @@ -96,6 +114,12 @@ def sections(body): def check_linear(): + if release_promotion(): + ok( + f"✅ linear-link not enforced on release-promotion PRs " + f"(chore(release) from '{env('PR_BRANCH')}')." + ) + # Strip comments from the body so the template's own example does not count. ref = linear_ref(env("PR_TITLE"), strip_comments(env("PR_BODY")), env("PR_BRANCH")) if ref: @@ -110,6 +134,12 @@ def check_linear(): def check_template(): + if release_promotion(): + ok( + f"✅ pr-template not enforced on release-promotion PRs " + f"(chore(release) from '{env('PR_BRANCH')}')." + ) + base = env("PR_BASE") if base and base != "main": ok(f"✅ pr-template not enforced on base '{base}' (main only).") diff --git a/.github/scripts/test_check_pr.py b/.github/scripts/test_check_pr.py index c41f323..aad0b24 100644 --- a/.github/scripts/test_check_pr.py +++ b/.github/scripts/test_check_pr.py @@ -71,6 +71,17 @@ def body_without(section_swap): ("linear: issue URL in body", "linear", {"PR_BODY": "https://linear.app/autonominetwork/issue/V2-719/foo"}, 0), ("linear: key in branch (lowercased)", "linear", {"PR_BRANCH": "chrisoneil/v2-720-ci-check"}, 0), ("linear: key in title", "linear", {"PR_TITLE": "AUTO-42 do the thing", "PR_BRANCH": "x"}, 0), + # --- release-promotion exemption (title prefix AND rc-* head branch) --- + ("linear: release promotion exempt", "linear", + {"PR_TITLE": "chore(release): promote rc-2026.8.3", "PR_BRANCH": "rc-2026.8.3"}, 0), + ("template: release promotion exempt", "template", + {"PR_TITLE": "chore(release): promote rc-2026.8.3", "PR_BRANCH": "rc-2026.8.3", + "PR_BASE": "main", "PR_BODY": "Promotes rc-2026.8.3 to release versions."}, 0), + ("linear: chore(release) title without rc branch still enforced", "linear", + {"PR_TITLE": "chore(release): tidy", "PR_BRANCH": "chore/tidy"}, 1), + ("template: rc branch without chore(release) title still enforced", "template", + {"PR_TITLE": "fix something", "PR_BRANCH": "rc-2026.9.1", + "PR_BASE": "main", "PR_BODY": "freeform"}, 1), # --- pr-template: acceptances --- ("template: valid T0 body", "template", {"PR_BASE": "main", "PR_BODY": VALID_BODY}, 0), ("template: rc-* base is a no-op pass", "template", {"PR_BASE": "rc-2025.10", "PR_BODY": "anything"}, 0), diff --git a/CLAUDE.md b/CLAUDE.md index 3730c3a..97249b4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -157,3 +157,6 @@ description. CI enforces the Linear link and the template fields (`linear-link` and `pr-template` status checks); a PR that omits them fails its checks. +Release-promotion PRs — a `chore(release):` title from an `rc-*` head branch — +are exempt from both checks (they are mechanical version-bump PRs generated by +the release process).