1+ #!/usr/bin/env python3
12"""
23Resolve release_type from PR labels with safe defaults.
34
45Modes
5-
66- Manual override (MANUAL_RELEASE_TYPE): emit that and exit.
77- Single-PR mode (default): inspect PR labels for THIS_SHA; default "rc".
8- - Aggregate mode (AGGREGATE=true): consider TEST deployed PRs merged since latest final tag*
9- up to LATEST_TEST_SHA (BOUNDARY), and pick highest of major > minor > patch > rc
8+ - Aggregate mode (AGGREGATE=true): consider TEST- deployed PRs merged since latest final tag
9+ up to LATEST_TEST_SHA (BOUNDARY), and pick highest of major > minor > patch > rc.
1010
1111Env 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: (rc|patch|minor|major)
17- AGGREGATE: "true"|"false" (default "false")
18- BRANCH: branch (default "main")
12+ - GH_TOKEN / GITHUB_TOKEN: required
13+ - THIS_SHA: SHA being promoted (required unless manual override)
14+ - LATEST_TEST_SHA: required when AGGREGATE=true
15+ - MANUAL_RELEASE_TYPE: (rc|patch|minor|major)
16+ - AGGREGATE: "true"|"false" (default "false")
17+ - BRANCH: branch (default "main")
1918
2019Outputs
21-
22- release_type: rc|patch|minor|major
23- basis: manual|single-pr|aggregate
24- pr_numbers: comma-separated PR numbers considered
20+ - release_type: rc|patch|minor|major
21+ - basis: manual|single-pr|aggregate
22+ - pr_numbers: comma-separated PR numbers considered
2523"""
2624
27- import os , subprocess , sys
25+ from __future__ import annotations
26+ import os
27+ import sys
2828from typing import List , Set
2929
30- BRANCH = os .getenv ("BRANCH" , "main" )
31-
32- def run (cmd : List [str ], check = True , capture = True ) -> subprocess .CompletedProcess :
33- # cp = completed process (will use this to refer)
34- return subprocess .run (cmd , check = check , capture_output = capture , text = True )
35-
36- def fail (msg : str ) -> int :
37- print (f"::error::{ msg } " , file = sys .stderr )
38- return 1
39-
40- def fetch_latest_from_remote ():
41- run (["git" ,"fetch" ,"origin" , BRANCH , "--quiet" ], check = True )
42- run (["git" ,"fetch" ,"--tags" ,"--force" ,"--quiet" ], check = True )
43-
44- def gh_api (path : str , jq : str | None = None ) -> List [str ]:
45- """
46- A simple python wrapper around the GitHub API
47- to make it a callable function.
48- """
49- args = ["gh" ,"api" , path ]
50- if jq :
51- args += ["--jq" , jq ]
52- cp = run (args , check = True )
53- return [x for x in cp .stdout .splitlines () if x ]
54-
55- def latest_final_tag () -> str | None :
56- """
57- Grabs the version tags and sorts semantically desc
58- """
59- cp = run (["git" ,"tag" ,"--list" ,"v[0-9]*.[0-9]*.[0-9]*" ,"--sort=-v:refname" ], check = True )
60- tags = cp .stdout .splitlines ()
61- return tags [0 ] if tags else None
62-
63- def first_commit () -> str :
64- """
65- Returns the first commit of the current branch.
66-
67- We will never use this for our project since we
68- already have a release but can be used as a
69- fallback for new projects.
70- """
71- return run (["git" ,"rev-list" ,"--max-parents=0" ,"HEAD" ], check = True ).stdout .strip ()
72-
73- def list_merged_pr_commits (base : str , head : str ) -> List [str ]:
74- rng = f"{ base } ..{ head } "
75- cp = run (["git" ,"rev-list" ,"--merges" ,"--first-parent" , rng ], check = False )
76- return [x for x in cp .stdout .splitlines () if x ]
77-
78- def prs_for_commit (sha : str ) -> List [int ]:
79- nums = gh_api (f"/repos/{ os .getenv ('GITHUB_REPOSITORY' )} /commits/{ sha } /pulls" , jq = ".[].number" )
80- return [int (n ) for n in nums ]
81-
82- def labels_for_pr (pr : int ) -> List [str ]:
83- return gh_api (f"/repos/{ os .getenv ('GITHUB_REPOSITORY' )} /issues/{ pr } /labels" , jq = ".[].name" )
30+ from ci_utils import (
31+ ensure_token ,
32+ fetch_latest_from_remote ,
33+ latest_final_tag ,
34+ first_commit ,
35+ list_merged_pr_commits ,
36+ prs_for_commit ,
37+ labels_for_pr ,
38+ fail ,
39+ )
8440
8541def pick_highest (labels : List [str ]) -> str | None :
8642 has_major = any (l == "release:major" for l in labels )
@@ -94,13 +50,12 @@ def pick_highest(labels: List[str]) -> str | None:
9450 return None
9551
9652def main () -> int :
97- if not (os .getenv ("GH_TOKEN" ) or os .getenv ("GITHUB_TOKEN" )):
98- return fail ("GH_TOKEN/GITHUB_TOKEN is required" )
53+ ensure_token ()
9954
10055 manual = (os .getenv ("MANUAL_RELEASE_TYPE" ) or "" ).strip ()
10156 if manual :
10257 if manual not in {"rc" ,"patch" ,"minor" ,"major" }:
103- return fail (f"Invalid MANUAL_RELEASE_TYPE: { manual } " )
58+ fail (f"Invalid MANUAL_RELEASE_TYPE: { manual } " )
10459 out = os .getenv ("GITHUB_OUTPUT" )
10560 if out :
10661 with open (out , "a" ) as f :
@@ -112,7 +67,7 @@ def main() -> int:
11267
11368 this_sha = (os .getenv ("THIS_SHA" ) or "" ).strip ()
11469 if not this_sha :
115- return fail ("Cannot determine sha" )
70+ fail ("Cannot determine sha" )
11671
11772 fetch_latest_from_remote ()
11873
@@ -124,7 +79,7 @@ def main() -> int:
12479 if aggregate :
12580 latest_test_sha = (os .getenv ("LATEST_TEST_SHA" ) or "" ).strip ()
12681 if not latest_test_sha :
127- return fail ("LATEST_TEST_SHA is required when AGGREGATE=true" )
82+ fail ("LATEST_TEST_SHA is required when AGGREGATE=true" )
12883 base = latest_final_tag () or first_commit ()
12984 merges = list_merged_pr_commits (base , latest_test_sha )
13085 for m in merges :
0 commit comments