|
| 1 | +""" |
| 2 | +Resolve release_type from PR labels with safe defaults. |
| 3 | +
|
| 4 | +Modes |
| 5 | +----- |
| 6 | +1) Manual override (MANUAL_RELEASE_TYPE): emit that and exit. |
| 7 | +2) Single-PR mode (default): inspect PR labels for THIS_SHA; default "rc". |
| 8 | +3) Aggregate mode (AGGREGATE=true): consider *all PRs merged since latest final tag* |
| 9 | + up to LATEST_TEST_SHA (BOUNDARY), and pick highest of {major > minor > patch > rc}. |
| 10 | +
|
| 11 | +Env inputs |
| 12 | +---------- |
| 13 | +GH_TOKEN / GITHUB_TOKEN : required |
| 14 | +THIS_SHA : SHA being promoted (required unless manual override) |
| 15 | +LATEST_TEST_SHA : required when AGGREGATE=true |
| 16 | +MANUAL_RELEASE_TYPE : optional (rc|patch|minor|major) |
| 17 | +AGGREGATE : "true"|"false" (default "false") |
| 18 | +BRANCH : branch (default "main") |
| 19 | +
|
| 20 | +Outputs (GITHUB_OUTPUT) |
| 21 | +----------------------- |
| 22 | +release_type : rc|patch|minor|major |
| 23 | +basis : manual|single-pr|aggregate |
| 24 | +pr_numbers : comma-separated PR numbers considered |
| 25 | +""" |
| 26 | + |
| 27 | +import os, subprocess, sys |
| 28 | +from typing import List, Set |
| 29 | + |
| 30 | +BRANCH = os.getenv("BRANCH", "main") |
| 31 | + |
| 32 | +def run(cmd: List[str], check=True, capture=True) -> subprocess.CompletedProcess: |
| 33 | + return subprocess.run(cmd, check=check, capture_output=capture, text=True) |
| 34 | + |
| 35 | +def fail(msg: str) -> int: |
| 36 | + print(f"::error::{msg}", file=sys.stderr) |
| 37 | + return 1 |
| 38 | + |
| 39 | +def ensure_graph(): |
| 40 | + run(["git","fetch","origin", BRANCH, "--quiet"], check=True) |
| 41 | + run(["git","fetch","--tags","--force","--quiet"], check=True) |
| 42 | + |
| 43 | +def gh_api(path: str, jq: str | None = None) -> List[str]: |
| 44 | + args = ["gh","api", path] |
| 45 | + if jq: |
| 46 | + args += ["--jq", jq] |
| 47 | + cp = run(args, check=True) |
| 48 | + return [x for x in cp.stdout.splitlines() if x] |
| 49 | + |
| 50 | +def latest_final_tag() -> str | None: |
| 51 | + cp = run(["git","tag","--list","v[0-9]*.[0-9]*.[0-9]*","--sort=-v:refname"], check=True) |
| 52 | + tags = cp.stdout.splitlines() |
| 53 | + return tags[0] if tags else None |
| 54 | + |
| 55 | +def first_commit() -> str: |
| 56 | + return run(["git","rev-list","--max-parents=0","HEAD"], check=True).stdout.strip() |
| 57 | + |
| 58 | +def first_parent_merges(base: str, head: str) -> List[str]: |
| 59 | + rng = f"{base}..{head}" |
| 60 | + cp = run(["git","rev-list","--merges","--first-parent", rng], check=False) |
| 61 | + return [x for x in cp.stdout.splitlines() if x] |
| 62 | + |
| 63 | +def prs_for_commit(sha: str) -> List[int]: |
| 64 | + nums = gh_api(f"/repos/{os.getenv('GITHUB_REPOSITORY')}/commits/{sha}/pulls", jq=".[].number") |
| 65 | + return [int(n) for n in nums] |
| 66 | + |
| 67 | +def labels_for_pr(pr: int) -> List[str]: |
| 68 | + return gh_api(f"/repos/{os.getenv('GITHUB_REPOSITORY')}/issues/{pr}/labels", jq=".[].name") |
| 69 | + |
| 70 | +def pick_highest(labels: List[str]) -> str | None: |
| 71 | + has_major = any(l == "release:major" for l in labels) |
| 72 | + has_minor = any(l == "release:minor" for l in labels) |
| 73 | + has_patch = any(l == "release:patch" for l in labels) |
| 74 | + has_rc = any(l == "release:rc" for l in labels) |
| 75 | + if has_major: return "major" |
| 76 | + if has_minor: return "minor" |
| 77 | + if has_patch: return "patch" |
| 78 | + if has_rc: return "rc" |
| 79 | + return None |
| 80 | + |
| 81 | +def main() -> int: |
| 82 | + if not (os.getenv("GH_TOKEN") or os.getenv("GITHUB_TOKEN")): |
| 83 | + return fail("GH_TOKEN/GITHUB_TOKEN is required") |
| 84 | + |
| 85 | + manual = (os.getenv("MANUAL_RELEASE_TYPE") or "").strip() |
| 86 | + if manual: |
| 87 | + if manual not in {"rc","patch","minor","major"}: |
| 88 | + return fail(f"Invalid MANUAL_RELEASE_TYPE: {manual}") |
| 89 | + out = os.getenv("GITHUB_OUTPUT") |
| 90 | + if out: |
| 91 | + with open(out, "a") as f: |
| 92 | + f.write(f"release_type={manual}\n") |
| 93 | + f.write("basis=manual\n") |
| 94 | + f.write("pr_numbers=\n") |
| 95 | + print(f"Release type (manual) → {manual}") |
| 96 | + return 0 |
| 97 | + |
| 98 | + this_sha = (os.getenv("THIS_SHA") or "").strip() |
| 99 | + if not this_sha: |
| 100 | + return fail("THIS_SHA is required when no MANUAL_RELEASE_TYPE is provided") |
| 101 | + |
| 102 | + ensure_graph() |
| 103 | + |
| 104 | + aggregate = (os.getenv("AGGREGATE","false").lower() == "true") |
| 105 | + pr_nums: Set[int] = set() |
| 106 | + basis = "single-pr" |
| 107 | + release_type = "rc" |
| 108 | + |
| 109 | + if aggregate: |
| 110 | + latest_test_sha = (os.getenv("LATEST_TEST_SHA") or "").strip() |
| 111 | + if not latest_test_sha: |
| 112 | + return fail("LATEST_TEST_SHA is required when AGGREGATE=true") |
| 113 | + base = latest_final_tag() or first_commit() |
| 114 | + merges = first_parent_merges(base, latest_test_sha) |
| 115 | + for m in merges: |
| 116 | + for n in prs_for_commit(m): |
| 117 | + pr_nums.add(n) |
| 118 | + all_labels: List[str] = [] |
| 119 | + for pr in pr_nums: |
| 120 | + all_labels.extend(labels_for_pr(pr)) |
| 121 | + release_type = pick_highest(all_labels) or "rc" |
| 122 | + basis = "aggregate" |
| 123 | + else: |
| 124 | + pnums = prs_for_commit(this_sha) |
| 125 | + if pnums: |
| 126 | + pr = pnums[0] |
| 127 | + pr_nums.add(pr) |
| 128 | + release_type = pick_highest(labels_for_pr(pr)) or "rc" |
| 129 | + else: |
| 130 | + release_type = "rc" |
| 131 | + basis = "single-pr" |
| 132 | + |
| 133 | + out = os.getenv("GITHUB_OUTPUT") |
| 134 | + if out: |
| 135 | + with open(out, "a") as f: |
| 136 | + f.write(f"release_type={release_type}\n") |
| 137 | + f.write(f"basis={basis}\n") |
| 138 | + f.write(f"pr_numbers={','.join(str(x) for x in sorted(pr_nums))}\n") |
| 139 | + |
| 140 | + print(f"Release type ({basis}) → {release_type}") |
| 141 | + if pr_nums: |
| 142 | + print(f"Considered PRs: {', '.join(str(x) for x in sorted(pr_nums))}") |
| 143 | + return 0 |
| 144 | + |
| 145 | +if __name__ == "__main__": |
| 146 | + sys.exit(main()) |
0 commit comments