From 284ba96c879964c26c5d6202782971002d53f862 Mon Sep 17 00:00:00 2001 From: probonopd Date: Sat, 3 May 2025 13:49:00 +0200 Subject: [PATCH] Accept PR number, #NNN, or PR URL [ci skip] --- updater.py | 211 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 170 insertions(+), 41 deletions(-) diff --git a/updater.py b/updater.py index bc33058..69d53c6 100644 --- a/updater.py +++ b/updater.py @@ -87,11 +87,61 @@ def extract_zip(zip_path): zip_ref.extractall(extract_path) return extract_path +# Function to download the latest release asset using GitHub API +def download_latest_release_github_api(release_type): + """ + release_type: 'latest' or 'continuous' + Returns: path to downloaded zip or None + """ + import json + headers = {'Accept': 'application/vnd.github.v3+json'} + repo = 'probonopd/MiniDexed' + if release_type == 'latest': + api_url = f'https://api.github.com/repos/{repo}/releases/latest' + resp = requests.get(api_url, headers=headers) + if resp.status_code != 200: + print(f"GitHub API error: {resp.status_code}") + return None + release = resp.json() + assets = release.get('assets', []) + elif release_type == 'continuous': + api_url = f'https://api.github.com/repos/{repo}/releases' + resp = requests.get(api_url, headers=headers) + if resp.status_code != 200: + print(f"GitHub API error: {resp.status_code}") + return None + releases = resp.json() + release = next((r for r in releases if 'continuous' in (r.get('tag_name','')+r.get('name','')).lower()), None) + if not release: + print("No continuous release found.") + return None + assets = release.get('assets', []) + else: + print(f"Unknown release type: {release_type}") + return None + asset = next((a for a in assets if a['name'].startswith('MiniDexed') and a['name'].endswith('.zip')), None) + if not asset: + print("No MiniDexed*.zip asset found in release.") + return None + url = asset['browser_download_url'] + print(f"Downloading asset: {asset['name']} from {url}") + resp = requests.get(url, stream=True) + if resp.status_code == 200: + zip_path = os.path.join(TEMP_DIR, asset['name']) + with open(zip_path, 'wb') as f: + for chunk in resp.iter_content(chunk_size=8192): + f.write(chunk) + return zip_path + print(f"Failed to download asset: {resp.status_code}") + return None + if __name__ == "__main__": parser = argparse.ArgumentParser(description="MiniDexed Updater") parser.add_argument("-v", action="store_true", help="Enable verbose FTP debug output") parser.add_argument("--ip", type=str, help="IP address of the device to upload to (skip mDNS discovery)") parser.add_argument("--version", type=int, choices=[1,2,3], help="Version to upload: 1=Latest official, 2=Continuous, 3=Local build (skip prompt)") + parser.add_argument("--pr", type=str, help="Pull request number or URL to fetch build artifacts from PR comment") + parser.add_argument("--github-token", type=str, help="GitHub personal access token for downloading PR artifacts (optional, can also use GITHUB_TOKEN env var)") args = parser.parse_args() import time @@ -100,6 +150,9 @@ if __name__ == "__main__": local_kernel_imgs = [f for f in os.listdir(local_kernel_dir) if f.startswith('kernel') and f.endswith('.img')] has_local_build = len(local_kernel_imgs) > 0 + # Get GitHub token from argument or environment + github_token = args.github_token or os.environ.get("GITHUB_TOKEN") + # Ask user which release to download (numbered choices) release_options = [ ("Latest official release", "https://github.com/probonopd/MiniDexed/releases/expanded_assets/latest"), @@ -118,61 +171,137 @@ if __name__ == "__main__": print("Which release do you want to update?") for idx, (desc, _) in enumerate(release_options): print(f" [{idx+1}] {desc}") + print(" [PR] Pull request build (enter PR number or URL)") while True: - choice = input(f"Enter the number of your choice (1-{len(release_options)}): ").strip() + choice = input(f"Enter the number of your choice (1-{len(release_options)}) or PR number: ").strip() if choice.isdigit() and 1 <= int(choice) <= len(release_options): selected_idx = int(choice)-1 github_url = release_options[selected_idx][1] + args.pr = None + break + # Accept PR number, #NNN, or PR URL + pr_match = re.match(r'^(#?\d+|https?://github.com/[^/]+/[^/]+/pull/\d+)$', choice) + if pr_match: + args.pr = choice + selected_idx = None + github_url = None break - print("Invalid selection. Please enter a valid number.") + print("Invalid selection. Please enter a valid number or PR number/URL.") # If local build is selected, skip all GitHub/zip logic and do not register cleanup use_local_build = has_local_build and selected_idx == len(release_options)-1 - if use_local_build: + if args.pr: + # Extract PR number from input (accepts URL, #899, or 899) + import re + pr_input = args.pr.strip() + m = re.search(r'(\d+)$', pr_input) + if not m: + print(f"Could not parse PR number from: {pr_input}") + sys.exit(1) + pr_number = m.group(1) + # Fetch PR page HTML + pr_url = f"https://github.com/probonopd/MiniDexed/pull/{pr_number}" + print(f"Fetching PR page: {pr_url}") + resp = requests.get(pr_url) + if resp.status_code != 200: + print(f"Failed to fetch PR page: {resp.status_code}") + sys.exit(1) + html = resp.text + # Find all 'Build for testing' artifact blocks (look for

Build for testing: ...

) + import html as ihtml + import re + pattern = re.compile(r'

Build for testing:(.*?)Use at your own risk\.', re.DOTALL) + matches = pattern.findall(html) + if not matches: + print("No build artifact links found in PR comment.") + sys.exit(1) + last_block = matches[-1] + # Find all artifact links in the last block + link_pattern = re.compile(r'([^<]+)') + links = link_pattern.findall(last_block) + if not links: + print("No artifact links found in PR comment block.") + sys.exit(1) + # Download both 32bit and 64bit artifacts if present + artifact_paths = [] + for url, name in links: + print(f"Downloading artifact: {name} from {url}") + # Try to extract the artifact ID from the URL + m = re.search(r'/artifacts/(\d+)', url) + if m and github_token: + artifact_id = m.group(1) + api_url = f"https://api.github.com/repos/probonopd/MiniDexed/actions/artifacts/{artifact_id}/zip" + headers = { + "Authorization": f"Bearer {github_token}", + "Accept": "application/vnd.github.v3+json", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" + } + resp = requests.get(api_url, stream=True, headers=headers) + if resp.status_code == 200: + local_path = os.path.join(TEMP_DIR, name + ".zip") + with open(local_path, 'wb') as f: + for chunk in resp.iter_content(chunk_size=8192): + f.write(chunk) + artifact_paths.append(local_path) + else: + print(f"Failed to download artifact {name} via GitHub API: {resp.status_code}") + print(f"Request headers: {resp.request.headers}") + print(f"Response headers: {resp.headers}") + print(f"Response URL: {resp.url}") + print(f"Response text (first 500 chars): {resp.text[:500]}") + else: + # Fallback to direct link if no artifact ID or no token + headers = {} + if github_token: + headers["Authorization"] = f"Bearer {github_token}" + headers["Accept"] = "application/octet-stream" + headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" + resp = requests.get(url, stream=True, headers=headers) + if resp.status_code == 200: + local_path = os.path.join(TEMP_DIR, name + ".zip") + with open(local_path, 'wb') as f: + for chunk in resp.iter_content(chunk_size=8192): + f.write(chunk) + artifact_paths.append(local_path) + else: + print(f"Failed to download artifact {name}: {resp.status_code}") + print(f"Request headers: {resp.request.headers}") + print(f"Response headers: {resp.headers}") + print(f"Response URL: {resp.url}") + print(f"Response text (first 500 chars): {resp.text[:500]}") + if not github_token: + print("You may need to provide a GitHub personal access token using --github-token or the GITHUB_TOKEN environment variable.") + if not artifact_paths: + print("No artifacts downloaded.") + sys.exit(1) + # Extract all downloaded zips + extract_paths = [] + for path in artifact_paths: + ep = extract_zip(path) + print(f"Extracted {path} to {ep}") + extract_paths.append(ep) + # Use the first extracted path for further logic (or merge as needed) + extract_path = extract_paths[0] if extract_paths else None + use_local_build = False + elif use_local_build: # Remove cleanup function if registered atexit.unregister(cleanup_temp_files) print("Using local build: src/kernel*.img will be uploaded.") extract_path = None else: - # Use the selected GitHub URL for release - def get_release_url(github_url): - print(f"Fetching release page: {github_url}") - response = requests.get(github_url) - print(f"HTTP status code: {response.status_code}") - if response.status_code == 200: - print("Successfully fetched release page. Scanning for MiniDexed*.zip links...") - # Find all tags with a MiniDexed*.zip - pattern = re.compile(r']+href=["\']([^"\']+\.zip)["\'][^>]*>\s*]*class=["\']Truncate-text text-bold["\'][^>]*>(MiniDexed[^<]*?\.zip)', re.IGNORECASE) - matches = pattern.findall(response.text) - print(f"Found {len(matches)} candidate .zip links.") - for href, filename in matches: - print(f"Examining link: href={href}, filename={filename}") - if filename.startswith("MiniDexed") and filename.endswith(".zip"): - if href.startswith('http'): - print(f"Selected direct link: {href}") - return href - else: - full_url = f"https://github.com{href}" - print(f"Selected relative link, full URL: {full_url}") - return full_url - print("No valid MiniDexed*.zip link found.") - else: - print(f"Failed to fetch release page. Status code: {response.status_code}") - return None - - latest_release_url = get_release_url(github_url) - if latest_release_url: - print(f"Release URL: {latest_release_url}") - zip_path = download_latest_release(latest_release_url) - if zip_path: - print(f"Downloaded to: {zip_path}") - extract_path = extract_zip(zip_path) - print(f"Extracted to: {extract_path}") - else: - print("Failed to download the release.") - sys.exit(1) + # Use GitHub API instead of HTML parsing + if selected_idx == 0: + zip_path = download_latest_release_github_api('latest') + elif selected_idx == 1: + zip_path = download_latest_release_github_api('continuous') + else: + zip_path = None + if zip_path: + print(f"Downloaded to: {zip_path}") + extract_path = extract_zip(zip_path) + print(f"Extracted to: {extract_path}") else: - print("Failed to get the release URL.") + print("Failed to download the release.") sys.exit(1) # Ask user if they want to update Performances (default no)