From bcf16424e37611ea3dfddffde2340dfec875499f Mon Sep 17 00:00:00 2001 From: ZelinWang Date: Thu, 2 Jan 2025 14:02:04 +0800 Subject: [PATCH 1/5] create release tag --- .github/workflows/CreateReleaseTag.yml | 35 ++++++ scripts/automation/create_release_tag.py | 131 +++++++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 .github/workflows/CreateReleaseTag.yml create mode 100644 scripts/automation/create_release_tag.py diff --git a/.github/workflows/CreateReleaseTag.yml b/.github/workflows/CreateReleaseTag.yml new file mode 100644 index 00000000000..636e28b48ae --- /dev/null +++ b/.github/workflows/CreateReleaseTag.yml @@ -0,0 +1,35 @@ +name: Create Release Tag + +on: + push: + branches: + - main + paths: + - 'src/index.json' + +jobs: + create-release: + runs-on: windows-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install requests + + - name: Create Release Tag + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + python scripts/automation/create_release_tag.py \ No newline at end of file diff --git a/scripts/automation/create_release_tag.py b/scripts/automation/create_release_tag.py new file mode 100644 index 00000000000..db6b4ac5bda --- /dev/null +++ b/scripts/automation/create_release_tag.py @@ -0,0 +1,131 @@ +import os +import re +import requests +import subprocess +from typing import List, Dict, Tuple, Optional +import sys + +TARGET_FILE = "src/index.json" + +def get_api_urls() -> Tuple[str, str]: + """Generate GitHub API URLs based on GITHUB_REPOSITORY environment variable.""" + repo = os.environ.get("GITHUB_REPOSITORY") + if not repo: + print("Error: GITHUB_REPOSITORY environment variable is not set") + print("This script is designed to run in GitHub Actions environment") + sys.exit(1) + + base_url = f"https://api.github.com/repos/{repo}" + return f"{base_url}/releases", f"{base_url}/git/tags" + +def get_file_changes() -> List[str]: + diff_output = subprocess.check_output( + ["git", "diff", "HEAD^", "HEAD", "--", TARGET_FILE], + text=True + ) + + added_lines = [ + line[1:].strip() + for line in diff_output.splitlines() + if line.startswith("+") and not line.startswith("+++") + ] + + return added_lines + +def parse_filename(added_lines: List[str]) -> Optional[str]: + for line in added_lines: + if '"filename":' in line: + try: + filename = line.split(":")[1].strip().strip('",') + return filename + except IndexError: + print(f"Error parsing line: {line}") + return None + +def generate_tag_and_title(filename: str) -> Tuple[str, str]: + match = re.match(r"^(.*?)[-_](\d+\.\d+\.\d+[a-z0-9]*)", filename) + if not match: + raise ValueError(f"Invalid filename format: {filename}") + + name = match.group(1).replace("_", "-") + version = match.group(2) + + tag_name = f"{name}-{version}" + release_title = f"Release {tag_name}" + return tag_name, release_title + +def check_tag_exists(url: str, tag_name: str, headers: Dict[str, str]) -> bool: + response = requests.get( + f"{url}/{tag_name}", + headers=headers + ) + return response.status_code == 200 + +def create_release(url: str, release_data: Dict[str, str], headers: Dict[str, str]) -> None: + try: + response = requests.post( + url, + json=release_data, + headers=headers + ) + response.raise_for_status() + print(f"Successfully created release for {release_data['tag_name']}") + except requests.exceptions.RequestException as e: + print(f"Failed to create release for {release_data['tag_name']}: {e}") + +def main(): + github_token = os.environ.get("GITHUB_TOKEN") + if not github_token: + print("Error: GITHUB_TOKEN environment variable is required") + sys.exit(1) + + # Get API URLs from environment + release_url, tag_url = get_api_urls() + + headers = { + "Authorization": f"token {github_token}", + "Accept": "application/vnd.github.v3+json" + } + + try: + added_lines = get_file_changes() + if not added_lines: + print("No changes found in index.json") + return + + filename = parse_filename(added_lines) + if not filename: + print("No filename found in changes") + return + + try: + tag_name, release_title = generate_tag_and_title(filename) + + commit_sha = subprocess.check_output( + ["git", "rev-parse", "HEAD"], + text=True + ).strip() + + if check_tag_exists(tag_url, commit_sha, headers): + print(f"Commit {commit_sha} already has a tag, skipping...") + return + + release_data = { + "tag_name": tag_name, + "target_commitish": commit_sha, + "name": release_title, + "body": release_title + } + + create_release(release_url, release_data, headers) + + except ValueError as e: + print(f"Error generating tag for filename {filename}: {e}") + return + + except Exception as e: + print(f"Unexpected error: {e}") + raise + +if __name__ == "__main__": + main() From cb7241ff68ecceb2b3884e3b30ed978b489c8e52 Mon Sep 17 00:00:00 2001 From: ZelinWang Date: Thu, 2 Jan 2025 14:06:12 +0800 Subject: [PATCH 2/5] update --- .github/workflows/CreateReleaseTag.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CreateReleaseTag.yml b/.github/workflows/CreateReleaseTag.yml index 636e28b48ae..8fdd807fc05 100644 --- a/.github/workflows/CreateReleaseTag.yml +++ b/.github/workflows/CreateReleaseTag.yml @@ -32,4 +32,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - python scripts/automation/create_release_tag.py \ No newline at end of file + python scripts/automation/create_release_tag.py From 5e5634c30c381b5f117606f1fbe7b00d06fb9cba Mon Sep 17 00:00:00 2001 From: ZelinWang Date: Thu, 2 Jan 2025 14:30:37 +0800 Subject: [PATCH 3/5] update --- scripts/automation/create_release_tag.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/automation/create_release_tag.py b/scripts/automation/create_release_tag.py index db6b4ac5bda..26a658f5bb2 100644 --- a/scripts/automation/create_release_tag.py +++ b/scripts/automation/create_release_tag.py @@ -1,3 +1,11 @@ +#!/usr/bin/env python + +#------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +#-------------------------------------------------------------------------- + import os import re import requests From 6b8d396b1276d3060d5df42b91eea0983057f583 Mon Sep 17 00:00:00 2001 From: ZelinWang Date: Thu, 9 Jan 2025 11:50:48 +0800 Subject: [PATCH 4/5] update --- scripts/automation/create_release_tag.py | 429 +++++++++++++++++++---- 1 file changed, 368 insertions(+), 61 deletions(-) diff --git a/scripts/automation/create_release_tag.py b/scripts/automation/create_release_tag.py index 26a658f5bb2..705485526b9 100644 --- a/scripts/automation/create_release_tag.py +++ b/scripts/automation/create_release_tag.py @@ -1,32 +1,36 @@ #!/usr/bin/env python -#------------------------------------------------------------------------- +# ------------------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See License.txt in the project root for # license information. -#-------------------------------------------------------------------------- +# -------------------------------------------------------------------------- import os import re import requests import subprocess +import tempfile +import zipfile from typing import List, Dict, Tuple, Optional import sys +import json +repo = os.environ.get("GITHUB_REPOSITORY") TARGET_FILE = "src/index.json" +base_url = f"https://api.github.com/repos/{repo}" +github_token = os.environ.get("GITHUB_TOKEN") +if not github_token: + print("Error: GITHUB_TOKEN environment variable is required") + sys.exit(1) +headers = { + "Authorization": f"token {github_token}", + "Accept": "application/vnd.github.v3+json" +} -def get_api_urls() -> Tuple[str, str]: - """Generate GitHub API URLs based on GITHUB_REPOSITORY environment variable.""" - repo = os.environ.get("GITHUB_REPOSITORY") - if not repo: - print("Error: GITHUB_REPOSITORY environment variable is not set") - print("This script is designed to run in GitHub Actions environment") - sys.exit(1) - base_url = f"https://api.github.com/repos/{repo}" - return f"{base_url}/releases", f"{base_url}/git/tags" - -def get_file_changes() -> List[str]: +def get_file_diff() -> List[str]: + """Get added lines from git diff""" diff_output = subprocess.check_output( ["git", "diff", "HEAD^", "HEAD", "--", TARGET_FILE], text=True @@ -40,17 +44,185 @@ def get_file_changes() -> List[str]: return added_lines -def parse_filename(added_lines: List[str]) -> Optional[str]: + +def parse_filenames(added_lines: List[str]) -> List[str]: + """Parse filenames from added lines""" + filenames = [] for line in added_lines: if '"filename":' in line: try: filename = line.split(":")[1].strip().strip('",') - return filename + filenames.append(filename) except IndexError: print(f"Error parsing line: {line}") - return None + return filenames + + +def parse_sha256_digest(added_lines: List[str]) -> str: + shas = [] + for line in added_lines: + if "sha256Digest" in line: + try: + sha_match = re.search(r'"sha256Digest"\s*:\s*"([a-fA-F0-9]{64})"', line) + if sha_match: + sha = sha_match.group(1) + print(f"get sha256Digest change: {sha}") + shas.append(sha) + except IndexError: + print(f"Error parsing line: {line}") + return shas + + +def get_file_info_by_sha(sha: str) -> Tuple[str, str]: + try: + with open(TARGET_FILE, 'r', encoding='utf-8') as f: + data = json.load(f) + + for ext_name, versions in data["extensions"].items(): + for version in versions: + if version["sha256Digest"] == sha: + return version["filename"], version["downloadUrl"] + + return None, None -def generate_tag_and_title(filename: str) -> Tuple[str, str]: + except FileNotFoundError: + print(f"Error: {TARGET_FILE} not found") + raise + except json.JSONDecodeError: + print(f"Error: Invalid JSON format in {TARGET_FILE}") + raise + + +def get_release_info(tag_name: str) -> Tuple[Optional[int], Optional[int]]: + try: + url = f"{base_url}/releases/tags/{tag_name}" + + response = requests.get(url, headers=headers) + response.raise_for_status() + + data = response.json() + + release_id = data.get("id") + release_body = data.get("body") + assets = data.get("assets", []) + asset_id = assets[0].get("id") if assets else None + + return release_id, release_body, asset_id + except requests.RequestException as e: + print(f"API request failed: {e}") + return None, None, None + except (KeyError, IndexError) as e: + print(f"Failed to parse response data: {e}") + return None, None, None + + +def update_release_body(release_id: int, commit_sha: str, old_body: str, sha: str, tag_name: str) -> bool: + try: + url = f"{base_url}/releases/{release_id}" + ref_url = f"{base_url}/git/refs/tags/{tag_name}" + data = { + "sha": commit_sha, + "force": True + } + print(f"Updating tag {tag_name} to point to commit {commit_sha}") + response = requests.patch(ref_url, headers=headers, json=data) + response.raise_for_status() + print(f"Successfully updated tag {tag_name}") + + sha_pattern = r'[a-fA-F0-9]{64}' + new_body = old_body + + found_shas = re.finditer(sha_pattern, old_body) + for match in found_shas: + old_sha = match.group() + + if old_sha != commit_sha: + new_body = new_body.replace(old_sha, sha) + break + + payload = { + "body": new_body + } + + response = requests.patch(url, json=payload, headers=headers) + response.raise_for_status() + + return True + except requests.RequestException as e: + print(f"Failed to update release: {e}") + if hasattr(e.response, 'text'): + print(f"Response: {e.response.text}") + return False + except Exception as e: + print(f"Unexpected error: {e}") + return False + + +def update_release_asset(wheel_url: str, asset_id: int, release_id: int) -> bool: + try: + print(f"Downloading wheel from {wheel_url}") + wheel_response = requests.get(wheel_url) + wheel_response.raise_for_status() + + if asset_id is not None: + delete_url = f"{base_url}/releases/assets/{asset_id}" + delete_response = requests.delete(delete_url, headers=headers) + delete_response.raise_for_status() + print("Successfully deleted old asset") + + release_url = f"{base_url}/releases/{release_id}" + response = requests.get(release_url, headers=headers) + response.raise_for_status() + release_info = response.json() + upload_url = release_info["upload_url"].replace("{?name,label}", "") + + upload_headers = headers.copy() + upload_headers["Content-Type"] = "application/octet-stream" + params = {"name": os.path.basename(wheel_url)} + + print(f"Uploading new wheel to {upload_url}") + upload_response = requests.post( + upload_url, + headers=upload_headers, + params=params, + data=wheel_response.content + ) + upload_response.raise_for_status() + print("Successfully updated wheel file") + return True + + except requests.RequestException as e: + print(f"Failed to update release asset: {e}") + if hasattr(e, 'response') and e.response is not None: + print(f"Response: {e.response.text}") + return False + except Exception as e: + print(f"Unexpected error: {e}") + return False + + +def get_extension_info(filename: str) -> Optional[Dict]: + """Get extension information from index.json""" + try: + with open(TARGET_FILE, 'r') as f: + index_data = json.load(f) + + # Search for the extension entry with matching filename + for ext_name, versions in index_data.get("extensions", {}).items(): + for version in versions: + if version.get("filename") == filename: + return version + + print(f"Extension {filename} not found in index.json") + return None + + except Exception as e: + print(f"Error reading index.json: {e}") + return None + + +def generate_tag_and_title(filename: str) -> Tuple[str, str, str]: + """Generate tag name and release title from filename""" match = re.match(r"^(.*?)[-_](\d+\.\d+\.\d+[a-z0-9]*)", filename) if not match: raise ValueError(f"Invalid filename format: {filename}") @@ -59,18 +231,23 @@ def generate_tag_and_title(filename: str) -> Tuple[str, str]: version = match.group(2) tag_name = f"{name}-{version}" - release_title = f"Release {tag_name}" - return tag_name, release_title + release_title = f"{name} {version}" + return tag_name, release_title, version + -def check_tag_exists(url: str, tag_name: str, headers: Dict[str, str]) -> bool: +def check_tag_exists(tag_name: str) -> bool: + url = f"{base_url}/tags/{tag_name}" response = requests.get( - f"{url}/{tag_name}", + url, headers=headers ) return response.status_code == 200 -def create_release(url: str, release_data: Dict[str, str], headers: Dict[str, str]) -> None: + +def create_release(release_data: Dict[str, str], wheel_url: str = None) -> None: try: + url = f"{base_url}/releases" + # Create release response = requests.post( url, json=release_data, @@ -78,62 +255,192 @@ def create_release(url: str, release_data: Dict[str, str], headers: Dict[str, st ) response.raise_for_status() print(f"Successfully created release for {release_data['tag_name']}") + release_info = response.json() + + # Upload wheel file if URL is provided + if wheel_url: + print(f"Downloading wheel from {wheel_url}") + wheel_response = requests.get(wheel_url) + wheel_response.raise_for_status() + + upload_url = release_info["upload_url"].replace("{?name,label}", "") + params = {"name": os.path.basename(wheel_url)} + + print(f"Uploading wheel to {upload_url}") + upload_headers = headers.copy() + upload_headers["Content-Type"] = "application/octet-stream" + upload_response = requests.post( + upload_url, + headers=upload_headers, + params=params, + data=wheel_response.content + ) + upload_response.raise_for_status() + print("Successfully uploaded wheel file") + except requests.exceptions.RequestException as e: - print(f"Failed to create release for {release_data['tag_name']}: {e}") + print(f"\nError creating release for {release_data['tag_name']}") + print(f"Error type: {type(e).__name__}") + print(f"Error message: {str(e)}") + if hasattr(e, 'response') and e.response is not None: + print(f"Response status code: {e.response.status_code}") + print(f"Response body: {e.response.text}") + raise -def main(): - github_token = os.environ.get("GITHUB_TOKEN") - if not github_token: - print("Error: GITHUB_TOKEN environment variable is required") - sys.exit(1) - - # Get API URLs from environment - release_url, tag_url = get_api_urls() - - headers = { - "Authorization": f"token {github_token}", - "Accept": "application/vnd.github.v3+json" - } +def generate_release_body(history_note: str, sha256_digest: str, filename: str) -> str: + """Generate release body with history notes and wheel information""" + return f"{history_note}\n\nSHA256 hashes of the release artifacts:\n```\n{sha256_digest} {filename}\n```\n" + + +def get_history_note(wheel_url: str, version: str) -> str: + """Download wheel package and extract HISTORY.rst to find version notes""" + try: + # Download wheel file + response = requests.get(wheel_url) + response.raise_for_status() + + with tempfile.TemporaryFile() as temp_file: + temp_file.write(response.content) + temp_file.seek(0) + + # Open wheel as zip + with zipfile.ZipFile(temp_file, 'r') as wheel: + # Find DESCRIPTION.rst file + history_files = [f for f in wheel.namelist() if f.endswith('DESCRIPTION.rst')] + if not history_files: + return "No history notes found" + + history_content = wheel.read(history_files[0]).decode('utf-8') + + # Match any line starting with the version number + version_pattern = rf"^{re.escape(version)}.*?\n(?:[-=+~]+\n)?(.*?)(?=^[\d.]+[a-z0-9].*?(?:\n[-=+~]+)?|\Z)" + match = re.search(version_pattern, history_content, re.DOTALL | re.MULTILINE) + + if match: + return match.group(1).strip() + + return "No history notes found for this version" + + except Exception as e: + print(f"Error getting history notes: {e}") + return "No history notes found for this version" + + +def get_history_note_from_source(version: str, extension_name: str) -> str: + """Get history notes from source code HISTORY.rst""" + try: + history_path = f"src/{extension_name}/HISTORY.rst" + if not os.path.exists(history_path): + return "No history notes found" + + with open(history_path, 'r', encoding='utf-8') as f: + history_content = f.read() + + # Match any line starting with the version number + version_pattern = rf"^{re.escape(version)}.*?\n(?:[-=+~]+\n)?(.*?)(?=^[\d.]+[a-z0-9].*?(?:\n[-=+~]+)?|\Z)" + match = re.search(version_pattern, history_content, re.DOTALL | re.MULTILINE) + + if match: + return match.group(1).strip() + + return "No history notes found in source code" + + except Exception as e: + print(f"Error reading history from source: {e}") + return "No history notes found for this version" + + +def main(): try: - added_lines = get_file_changes() + # Get added lines from git diff + added_lines = get_file_diff() if not added_lines: print("No changes found in index.json") return - filename = parse_filename(added_lines) - if not filename: - print("No filename found in changes") - return + # Parse filenames from added lines + filenames = parse_filenames(added_lines) + commit_sha = subprocess.check_output( + ["git", "rev-parse", "HEAD"], + text=True + ).strip() + if not filenames: + print("No filenames found in changes") + shas = parse_sha256_digest(added_lines) + if not shas: + print("No sha256Digest found in changes") + return + else: + for sha in shas: + filename, wheel_url = get_file_info_by_sha(sha) + tag_name, release_title, version = generate_tag_and_title(filename) + release_id, release_body, asset_id = get_release_info(tag_name) + update_release_body(release_id, commit_sha, release_body, sha, tag_name) if release_id else None + update_release_asset(wheel_url, asset_id, release_id) if release_id else None - try: - tag_name, release_title = generate_tag_and_title(filename) - - commit_sha = subprocess.check_output( - ["git", "rev-parse", "HEAD"], - text=True - ).strip() + print(f"Found {len(filenames)} files to process") + # Process each filename + for filename in filenames: + print(f"\nProcessing {filename}...") - if check_tag_exists(tag_url, commit_sha, headers): - print(f"Commit {commit_sha} already has a tag, skipping...") - return + # Get extension info from index.json + extension_info = get_extension_info(filename) + if not extension_info: + print(f"Could not get extension information for {filename}, skipping...") + continue - release_data = { - "tag_name": tag_name, - "target_commitish": commit_sha, - "name": release_title, - "body": release_title - } + try: + tag_name, release_title, version = generate_tag_and_title(filename) + # Check if tag already exists + if check_tag_exists(tag_name): + print(f"Tag {tag_name} already exists, skipping...") + continue - create_release(release_url, release_data, headers) + # Try to get history notes from source code first + print(f"Getting history notes from source code...") + extension_name = re.match(r"^(.*?)[-_]\d+\.\d+\.\d+", filename).group(1) + history_note = get_history_note_from_source(version, extension_name) - except ValueError as e: - print(f"Error generating tag for filename {filename}: {e}") - return + # If no notes found in source code, try wheel package + if "No history notes found" in history_note: + print(f"No history notes found in source code, trying wheel package...") + history_note = get_history_note(extension_info["downloadUrl"], version) + + # If still no notes found, use default release note + if "No history notes found" in history_note: + print(f"No history notes found in wheel package, using default release note...") + history_note = f"Release {extension_name} {version}" + + # Generate release body + release_body = generate_release_body(history_note, extension_info["sha256Digest"], filename) + + release_data = { + "tag_name": tag_name, + "target_commitish": commit_sha, + "name": release_title, + "body": release_body + } + + print(f"\nCreating release with data:") + print(f"Tag name: {tag_name}") + print(f"Release title: {release_title}") + print(f"Target commit: {commit_sha}") + print(f"Body preview: {release_body[:200]}...") + + create_release(release_data, extension_info["downloadUrl"]) + + except ValueError as e: + print(f"Error generating tag for filename {filename}: {e}") + continue + except Exception as e: + print(f"Unexpected error processing {filename}: {e}") + continue except Exception as e: print(f"Unexpected error: {e}") raise + if __name__ == "__main__": main() From 0234226c74a2455a576173c72d756b19789b220e Mon Sep 17 00:00:00 2001 From: ZelinWang Date: Fri, 10 Jan 2025 14:56:00 +0800 Subject: [PATCH 5/5] update --- scripts/automation/create_release_tag.py | 184 ++++++++++------------- 1 file changed, 83 insertions(+), 101 deletions(-) diff --git a/scripts/automation/create_release_tag.py b/scripts/automation/create_release_tag.py index 705485526b9..1bf1e2f7897 100644 --- a/scripts/automation/create_release_tag.py +++ b/scripts/automation/create_release_tag.py @@ -158,12 +158,37 @@ def update_release_body(release_id: int, commit_sha: str, old_body: str, sha: st return False +def upload_wheel_file(wheel_url: str, upload_url: str) -> None: + """ + Download and upload a wheel file to GitHub release. + Args: + wheel_url: URL to download the wheel file from + upload_url: GitHub API upload URL + Raises: + requests.RequestException: If download or upload fails + """ + print(f"Downloading wheel from {wheel_url}") + wheel_response = requests.get(wheel_url) + wheel_response.raise_for_status() + + upload_url = upload_url.replace("{?name,label}", "") + params = {"name": os.path.basename(wheel_url)} + + print(f"Uploading wheel to {upload_url}") + upload_headers = headers.copy() + upload_headers["Content-Type"] = "application/octet-stream" + upload_response = requests.post( + upload_url, + headers=upload_headers, + params=params, + data=wheel_response.content + ) + upload_response.raise_for_status() + print("Successfully uploaded wheel file") + + def update_release_asset(wheel_url: str, asset_id: int, release_id: int) -> bool: try: - print(f"Downloading wheel from {wheel_url}") - wheel_response = requests.get(wheel_url) - wheel_response.raise_for_status() - if asset_id is not None: delete_url = f"{base_url}/releases/assets/{asset_id}" delete_response = requests.delete(delete_url, headers=headers) @@ -174,21 +199,8 @@ def update_release_asset(wheel_url: str, asset_id: int, release_id: int) -> bool response = requests.get(release_url, headers=headers) response.raise_for_status() release_info = response.json() - upload_url = release_info["upload_url"].replace("{?name,label}", "") - - upload_headers = headers.copy() - upload_headers["Content-Type"] = "application/octet-stream" - params = {"name": os.path.basename(wheel_url)} - - print(f"Uploading new wheel to {upload_url}") - upload_response = requests.post( - upload_url, - headers=upload_headers, - params=params, - data=wheel_response.content - ) - upload_response.raise_for_status() - print("Successfully updated wheel file") + + upload_wheel_file(wheel_url, release_info["upload_url"]) return True except requests.RequestException as e: @@ -247,7 +259,6 @@ def check_tag_exists(tag_name: str) -> bool: def create_release(release_data: Dict[str, str], wheel_url: str = None) -> None: try: url = f"{base_url}/releases" - # Create release response = requests.post( url, json=release_data, @@ -259,24 +270,7 @@ def create_release(release_data: Dict[str, str], wheel_url: str = None) -> None: # Upload wheel file if URL is provided if wheel_url: - print(f"Downloading wheel from {wheel_url}") - wheel_response = requests.get(wheel_url) - wheel_response.raise_for_status() - - upload_url = release_info["upload_url"].replace("{?name,label}", "") - params = {"name": os.path.basename(wheel_url)} - - print(f"Uploading wheel to {upload_url}") - upload_headers = headers.copy() - upload_headers["Content-Type"] = "application/octet-stream" - upload_response = requests.post( - upload_url, - headers=upload_headers, - params=params, - data=wheel_response.content - ) - upload_response.raise_for_status() - print("Successfully uploaded wheel file") + upload_wheel_file(wheel_url, release_info["upload_url"]) except requests.exceptions.RequestException as e: print(f"\nError creating release for {release_data['tag_name']}") @@ -293,62 +287,57 @@ def generate_release_body(history_note: str, sha256_digest: str, filename: str) return f"{history_note}\n\nSHA256 hashes of the release artifacts:\n```\n{sha256_digest} {filename}\n```\n" -def get_history_note(wheel_url: str, version: str) -> str: - """Download wheel package and extract HISTORY.rst to find version notes""" - try: - # Download wheel file - response = requests.get(wheel_url) - response.raise_for_status() - - with tempfile.TemporaryFile() as temp_file: - temp_file.write(response.content) - temp_file.seek(0) - - # Open wheel as zip - with zipfile.ZipFile(temp_file, 'r') as wheel: - # Find DESCRIPTION.rst file - history_files = [f for f in wheel.namelist() if f.endswith('DESCRIPTION.rst')] - if not history_files: - return "No history notes found" - - history_content = wheel.read(history_files[0]).decode('utf-8') +def get_history_note(version: str, extension_name: str, wheel_url: str = None) -> str: + """ + Get history notes for a version, first trying from source code then from wheel package. - # Match any line starting with the version number - version_pattern = rf"^{re.escape(version)}.*?\n(?:[-=+~]+\n)?(.*?)(?=^[\d.]+[a-z0-9].*?(?:\n[-=+~]+)?|\Z)" - match = re.search(version_pattern, history_content, re.DOTALL | re.MULTILINE) + Args: + version: Version string to search for + extension_name: Name of the extension + wheel_url: Optional URL to download wheel package if source check fails - if match: - return match.group(1).strip() - - return "No history notes found for this version" - - except Exception as e: - print(f"Error getting history notes: {e}") - return "No history notes found for this version" - - -def get_history_note_from_source(version: str, extension_name: str) -> str: - """Get history notes from source code HISTORY.rst""" + Returns: + str: History notes for the version or default message if none found + """ + version_pattern = rf"^{re.escape(version)}.*?\n(?:[-=+~]+\n)?(.*?)(?=^[\d.]+[a-z0-9].*?(?:\n[-=+~]+)?|\Z)" + # First try to get history from source code try: history_path = f"src/{extension_name}/HISTORY.rst" - if not os.path.exists(history_path): - return "No history notes found" + if os.path.exists(history_path): + with open(history_path, 'r', encoding='utf-8') as f: + history_content = f.read() + + # Match any line starting with the version number + match = re.search(version_pattern, history_content, re.DOTALL | re.MULTILINE) + if match: + return match.group(1).strip() + except Exception as e: + print(f"Error reading history from source: {e}") - with open(history_path, 'r', encoding='utf-8') as f: - history_content = f.read() + # If source check failed and wheel_url is provided, try wheel package + if wheel_url: + try: + response = requests.get(wheel_url) + response.raise_for_status() - # Match any line starting with the version number - version_pattern = rf"^{re.escape(version)}.*?\n(?:[-=+~]+\n)?(.*?)(?=^[\d.]+[a-z0-9].*?(?:\n[-=+~]+)?|\Z)" - match = re.search(version_pattern, history_content, re.DOTALL | re.MULTILINE) + with tempfile.TemporaryFile() as temp_file: + temp_file.write(response.content) + temp_file.seek(0) - if match: - return match.group(1).strip() + with zipfile.ZipFile(temp_file, 'r') as wheel: + history_files = [f for f in wheel.namelist() if f.endswith('DESCRIPTION.rst')] + if history_files: + history_content = wheel.read(history_files[0]).decode('utf-8') - return "No history notes found in source code" + # Match any line starting with the version number + match = re.search(version_pattern, history_content, re.DOTALL | re.MULTILINE) + if match: + return match.group(1).strip() + except Exception as e: + print(f"Error getting history notes from wheel: {e}") - except Exception as e: - print(f"Error reading history from source: {e}") - return "No history notes found for this version" + # Return default message if no history found + return f"Release {extension_name} {version}" def main(): @@ -362,9 +351,9 @@ def main(): # Parse filenames from added lines filenames = parse_filenames(added_lines) commit_sha = subprocess.check_output( - ["git", "rev-parse", "HEAD"], - text=True - ).strip() + ["git", "rev-parse", "HEAD"], + text=True + ).strip() if not filenames: print("No filenames found in changes") shas = parse_sha256_digest(added_lines) @@ -397,20 +386,13 @@ def main(): print(f"Tag {tag_name} already exists, skipping...") continue - # Try to get history notes from source code first - print(f"Getting history notes from source code...") extension_name = re.match(r"^(.*?)[-_]\d+\.\d+\.\d+", filename).group(1) - history_note = get_history_note_from_source(version, extension_name) - - # If no notes found in source code, try wheel package - if "No history notes found" in history_note: - print(f"No history notes found in source code, trying wheel package...") - history_note = get_history_note(extension_info["downloadUrl"], version) - - # If still no notes found, use default release note - if "No history notes found" in history_note: - print(f"No history notes found in wheel package, using default release note...") - history_note = f"Release {extension_name} {version}" + print(f"Getting history notes...") + history_note = get_history_note( + version, + extension_name, + extension_info["downloadUrl"] + ) # Generate release body release_body = generate_release_body(history_note, extension_info["sha256Digest"], filename)