1515BRANCH = os .getenv ("BRANCH" , "main" )
1616REPO_FALLBACK = "NHSDigital/eligibility-signposting-api"
1717
18+
19+ def _repo () -> str :
20+ return os .environ .get ("GITHUB_REPOSITORY" ) or REPO_FALLBACK
21+
1822def fail (msg : str ) -> NoReturn :
1923 print (f"::error::{ msg } " , file = sys .stderr )
2024 sys .exit (1 )
@@ -41,34 +45,6 @@ def git_ok(args: List[str]) -> bool:
4145def is_ancestor (older : str , newer : str ) -> bool :
4246 return subprocess .run (["git" , "merge-base" , "--is-ancestor" , older , newer ]).returncode == 0
4347
44- def gh_json (args : List [str ]) -> Any :
45- # Map GITHUB_TOKEN -> GH_TOKEN (gh prefers GH_TOKEN)
46- if "GH_TOKEN" not in os .environ and os .environ .get ("GITHUB_TOKEN" ):
47- os .environ ["GH_TOKEN" ] = os .environ ["GITHUB_TOKEN" ]
48-
49- # Ensure repo is explicit (act containers often need this)
50- repo = os .environ .get ("GITHUB_REPOSITORY" ) or REPO_FALLBACK
51- base = ["gh" , * args ]
52- if "--repo" not in args and "-R" not in args :
53- base .extend (["--repo" , repo ])
54-
55- cp = run (base , check = True )
56- try :
57- return json .loads (cp .stdout or "null" )
58- except json .JSONDecodeError as e :
59- raise RuntimeError (f"Failed to parse gh JSON: { e } \n STDOUT:\n { cp .stdout } \n STDERR:\n { cp .stderr } " )
60-
61- def gh_api (path : str , jq : Optional [str ] = None ) -> List [str ]:
62- """
63- A simple python wrapper around the GitHub API
64- to make it a callable function.
65- """
66- args = ["gh" , "api" , path ]
67- if jq :
68- args += ["--jq" , jq ]
69- cp = run (args , check = True )
70- return [x for x in cp .stdout .splitlines () if x ]
71-
7248def dev_tag_for_sha (sha : str ) -> Optional [str ]:
7349 cp = run (["git" , "tag" , "--points-at" , sha ], check = False )
7450 for t in cp .stdout .splitlines ():
@@ -80,7 +56,7 @@ def sha_for_tag(tag: str) -> Optional[str]:
8056 cp = run (["git" , "rev-list" , "-n1" , tag ], check = False )
8157 return cp .stdout .strip () or None
8258
83- def latest_final_tag () -> Optional [str ]:
59+ def latest_release_tag () -> Optional [str ]:
8460 cp = run (["git" , "tag" , "--list" , "v[0-9]*.[0-9]*.[0-9]*" , "--sort=-v:refname" ], check = True )
8561 tags = cp .stdout .splitlines ()
8662 return tags [0 ] if tags else None
@@ -91,24 +67,100 @@ def first_commit() -> str:
9167
9268 We will never use this for our project since we
9369 already have a release but can be used as a
94- fallback for new projects .
70+ fallback.
9571 """
9672 return run (["git" , "rev-list" , "--max-parents=0" , "HEAD" ], check = True ).stdout .strip ()
9773
98- def list_merged_pr_commits (base : str , head : str ) -> List [str ]:
74+ def labels_for_pr (pr : int ) -> List [str ]:
75+ """Return all labels on a PR (or issue)."""
76+ args = [
77+ "gh" , "api" ,
78+ f"/repos/{ _repo ()} /issues/{ pr } /labels" ,
79+ "-H" , "X-GitHub-Api-Version: 2022-11-28" ,
80+ "--jq" , ".[].name" ,
81+ ]
82+ cp = run (args , check = False )
83+ if cp .returncode not in (0 , 1 ):
84+ print (f"Warning: gh api exit { cp .returncode } for PR #{ pr } " , file = sys .stderr )
85+ if cp .stdout .strip ():
86+ return [x .strip () for x in cp .stdout .splitlines () if x .strip ()]
87+ return []
88+
89+
90+ def commit_subject (sha : str ) -> str :
91+ """Return the one-line subject for a commit SHA."""
92+ cp = run (["git" , "log" , "-1" , "--pretty=%s" , sha ], check = False )
93+ return (cp .stdout or "" ).strip ()
94+
95+ def parse_merge_subject_for_pr_numbers (subject : str ) -> List [int ]:
96+ """
97+ Extract PR numbers from common merge subjects, e.g.:
98+ - 'Merge pull request #123 from ...'
99+ - 'Some feature (#456)'
100+ """
101+ import re
102+ nums = set ()
103+ for m in re .finditer (r"(?:#|\bPR\s*#)(\d+)" , subject , flags = re .IGNORECASE ):
104+ try :
105+ nums .add (int (m .group (1 )))
106+ except ValueError :
107+ pass
108+ # Also match explicit 'Merge pull request #123'
109+ m2 = re .search (r"Merge pull request #(\d+)" , subject , flags = re .IGNORECASE )
110+ if m2 :
111+ try :
112+ nums .add (int (m2 .group (1 )))
113+ except ValueError :
114+ pass
115+ return sorted (nums )
116+
117+ def list_merge_commits (base : str , head : str ) -> List [str ]:
118+ """
119+ Merge commits on the first-parent path from base..head.
120+ """
99121 rng = f"{ base } ..{ head } "
100122 cp = run (["git" , "rev-list" , "--merges" , "--first-parent" , rng ], check = False )
101123 return [x for x in cp .stdout .splitlines () if x ]
102124
103- def prs_for_commit (sha : str ) -> List [int ]:
104- nums = gh_api (
105- f"/repos/{ os .getenv ('GITHUB_REPOSITORY' )} /commits/{ sha } /pulls" ,
106- jq = ".[].number" ,
107- )
108- return [int (n ) for n in nums ]
125+ def list_all_commits (base : str , head : str ) -> List [str ]:
126+ """
127+ All commits on the first-parent path base..head (includes squash merges as single commits).
128+ """
129+ rng = f"{ base } ..{ head } "
130+ # We choose first-parent so we're scanning the mainline history only.
131+ cp = run (["git" , "rev-list" , "--first-parent" , rng ], check = False )
132+ return [x for x in cp .stdout .splitlines () if x ]
109133
110- def labels_for_pr (pr : int ) -> List [str ]:
111- return gh_api (
112- f"/repos/{ os .getenv ('GITHUB_REPOSITORY' )} /issues/{ pr } /labels" ,
113- jq = ".[].name" ,
114- )
134+ def prs_for_commit_via_api (sha : str ) -> List [int ]:
135+ """
136+ Use the official endpoint linking any commit to associated PRs.
137+ Works for merge commits and for commits that landed via rebase merges.
138+ """
139+ args = [
140+ "gh" , "api" ,
141+ f"/repos/{ _repo ()} /commits/{ sha } /pulls" ,
142+ "-H" , "Accept: application/vnd.github.groot-preview+json" ,
143+ "-H" , "X-GitHub-Api-Version: 2022-11-28" ,
144+ "--jq" , ".[].number"
145+ ]
146+ try :
147+ cp = run (args , check = True )
148+ return [int (x ) for x in cp .stdout .splitlines () if x ]
149+ except subprocess .CalledProcessError as e :
150+ print (f"Warning: commit→PR lookup failed for { sha [:7 ]} ({ e .returncode } )" , file = sys .stderr )
151+ return []
152+
153+ def title_for_pr (pr : int ) -> str :
154+ """
155+ Return the title of a pull request.
156+ """
157+ args = [
158+ "gh" , "pr" , "view" , str (pr ),
159+ "--repo" , _repo (),
160+ "--json" , "title" ,
161+ "--jq" , ".title" ,
162+ ]
163+ cp = run (args , check = False )
164+ if cp .returncode not in (0 , 1 ):
165+ print (f"Warning: gh pr view exit { cp .returncode } for PR #{ pr } " , file = sys .stderr )
166+ return cp .stdout .strip ()
0 commit comments