1414import hashlib
1515import requests
1616import logging
17+ import os
1718from jinja2 import Environment , FileSystemLoader
18- from typing import Dict , Any , List
19+ from typing import Dict , Any , List , Optional
1920from collections import namedtuple
2021
2122WIFI_FW_BIN_NAME : str = "nrf70.bin"
@@ -71,14 +72,56 @@ def get_wifi_blob_info(name: str) -> BlobInfo:
7172logging .basicConfig (level = logging .INFO )
7273
7374
75+ def get_pr_head_commit (pr_number : str ) -> str :
76+ """Get the head commit SHA for a PR from GitHub API"""
77+ github_token = os .environ .get ('GITHUB_TOKEN' )
78+ headers = {}
79+ if github_token :
80+ headers ['Authorization' ] = f'token { github_token } '
81+
82+ url = f"https://api.github.com/repos/nrfconnect/sdk-nrfxlib/pulls/{ pr_number } "
83+
84+ try :
85+ response = requests .get (url , headers = headers , timeout = 30 )
86+ response .raise_for_status ()
87+
88+ pr_data = response .json ()
89+
90+ # Check if PR exists and is not closed/merged
91+ if pr_data .get ('state' ) == 'closed' :
92+ logger .warning (f"PR #{ pr_number } is closed" )
93+
94+ if pr_data .get ('merged' ):
95+ logger .warning (f"PR #{ pr_number } is already merged" )
96+
97+ # Get head commit
98+ head_commit = pr_data ['head' ]['sha' ]
99+ if not head_commit :
100+ raise ValueError (f"PR #{ pr_number } has no head commit" )
101+
102+ logger .debug (f"PR #{ pr_number } head commit: { head_commit } " )
103+ return head_commit
104+
105+ except requests .exceptions .RequestException as e :
106+ if response .status_code == 404 :
107+ raise ValueError (f"PR #{ pr_number } not found in nrfconnect/sdk-nrfxlib repository" )
108+ elif response .status_code == 403 :
109+ raise ValueError (f"Access denied to PR #{ pr_number } . Check GITHUB_TOKEN permissions" )
110+ else :
111+ raise ValueError (f"Failed to fetch PR #{ pr_number } : { e } " )
112+ except KeyError as e :
113+ raise ValueError (f"Invalid response format for PR #{ pr_number } : missing { e } " )
114+ except Exception as e :
115+ raise ValueError (f"Unexpected error fetching PR #{ pr_number } : { e } " )
116+
74117def compute_sha256 (url : str ) -> str :
75118 response = requests .get (url )
76119 response .raise_for_status ()
77120 sha256_hash : str = hashlib .sha256 (response .content ).hexdigest ()
78121 return sha256_hash
79122
80123
81- def render_template (template_path : str , output_path : str , latest_sha : str ) -> None :
124+ def render_template (template_path : str , output_path : str , latest_sha : str , is_pr : bool = False , pr_number : Optional [ str ] = None ) -> None :
82125 # Load the Jinja2 template
83126 env : Environment = Environment (loader = FileSystemLoader ("." ))
84127 template = env .get_template (template_path )
@@ -97,21 +140,41 @@ def render_template(template_path: str, output_path: str, latest_sha: str) -> No
97140 blob_info ["doc_url" ] = f"{ blob .docpath } "
98141
99142 # Download the binary to compute SHA-256 and extract version
100- response = requests .get (blob_info ["url" ])
101- response .raise_for_status ()
102- binary_data = response .content
143+ try :
144+ response = requests .get (blob_info ["url" ], timeout = 60 )
145+ response .raise_for_status ()
146+ binary_data = response .content
103147
104- blob_info ["sha256" ] = hashlib .sha256 (binary_data ).hexdigest ()
105- blob_info ["description" ] = blob .description
148+ blob_info ["sha256" ] = hashlib .sha256 (binary_data ).hexdigest ()
149+ blob_info ["description" ] = blob .description
106150
107- # Parse version from the actual binary
108- blob_info ["version" ] = parse_version_from_binary (binary_data )
151+ # Parse version from the actual binary
152+ blob_info ["version" ] = parse_version_from_binary (binary_data )
153+
154+ except requests .exceptions .RequestException as e :
155+ logger .error (f"Failed to download blob from { blob_info ['url' ]} : { e } " )
156+ raise ValueError (f"Failed to download blob for { blob .name } : { e } " )
157+ except Exception as e :
158+ logger .error (f"Unexpected error processing blob { blob .name } : { e } " )
159+ raise ValueError (f"Error processing blob { blob .name } : { e } " )
109160
110161 blobs [blob .name ] = blob_info
111162
112163 logger .debug (blobs )
164+
165+ # Prepare metadata comment
166+ metadata_comment = None
167+ if is_pr and pr_number :
168+ metadata_comment = f"# Generated from PR #{ pr_number } (commit: { latest_sha } )"
169+ else :
170+ metadata_comment = f"# Generated from commit: { latest_sha } "
171+
113172 # Render the template with the provided context
114- rendered_content : str = template .render (blobs = blobs , latest_sha = latest_sha )
173+ rendered_content : str = template .render (
174+ blobs = blobs ,
175+ latest_sha = latest_sha ,
176+ metadata_comment = metadata_comment
177+ )
115178
116179 # Write the rendered content to the output file
117180 with open (output_path , "w" ) as output_file :
@@ -137,8 +200,12 @@ def main() -> None:
137200 parser .add_argument (
138201 "-c" ,
139202 "--commit" ,
140- required = True ,
141- help = "The latest commit SHA for the nrfxlib repository." ,
203+ help = "The commit SHA for the nrfxlib repository (for merged commits)." ,
204+ )
205+ parser .add_argument (
206+ "-p" ,
207+ "--pr" ,
208+ help = "The PR number for the nrfxlib repository (for unmerged PRs)." ,
142209 )
143210 parser .add_argument (
144211 "-d" , "--debug" , action = "store_true" , help = "Enable debug logging."
@@ -149,8 +216,47 @@ def main() -> None:
149216 if args .debug :
150217 logger .setLevel (logging .DEBUG )
151218
152- # Render the template
153- render_template (args .template , args .output , args .commit )
219+ # Validate arguments
220+ if not args .commit and not args .pr :
221+ parser .error ("Either --commit or --pr must be specified" )
222+ if args .commit and args .pr :
223+ parser .error ("Only one of --commit or --pr can be specified" )
224+
225+ # Validate commit format if provided
226+ if args .commit :
227+ import re
228+ if not re .match (r'^[a-fA-F0-9]{7,40}$' , args .commit ):
229+ parser .error (f"Invalid commit hash format: { args .commit } . Expected 7-40 hex characters." )
230+
231+ # Validate PR number if provided
232+ if args .pr :
233+ if not args .pr .isdigit () or int (args .pr ) <= 0 :
234+ parser .error (f"Invalid PR number: { args .pr } . Expected a positive integer." )
235+
236+ # Determine the reference to use
237+ try :
238+ if args .pr :
239+ # For PRs, get the head commit from GitHub API
240+ logger .debug (f"Processing PR #{ args .pr } " )
241+ reference = get_pr_head_commit (args .pr )
242+ is_pr = True
243+ pr_number = args .pr
244+ else :
245+ # For merged commits, use the commit hash directly
246+ logger .debug (f"Processing commit { args .commit } " )
247+ reference = args .commit
248+ is_pr = False
249+ pr_number = None
250+
251+ # Render the template
252+ render_template (args .template , args .output , reference , is_pr , pr_number )
253+
254+ except ValueError as e :
255+ logger .error (f"Error: { e } " )
256+ exit (1 )
257+ except Exception as e :
258+ logger .error (f"Unexpected error: { e } " )
259+ exit (1 )
154260
155261
156262if __name__ == "__main__" :
0 commit comments