From 530983ddd5f1ae069485c80b0265302756a9e8ea Mon Sep 17 00:00:00 2001 From: mahmishr Date: Thu, 8 Jan 2026 15:32:04 +0530 Subject: [PATCH 01/13] code --- config.json | 8 ++ requirements.txt | 1 + update-sdk-versions.py | 299 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 308 insertions(+) create mode 100644 config.json create mode 100644 requirements.txt create mode 100644 update-sdk-versions.py diff --git a/config.json b/config.json new file mode 100644 index 0000000..9486389 --- /dev/null +++ b/config.json @@ -0,0 +1,8 @@ +{ + "github_token": "", + "json_file": "java-sdk-versions.json", + "add_to_list": false, + "pr_repo_owner": "CyberSource", + "pr_repo_name": "CAPIC-cybersource-rest-samples-java", + "pr_base_branch": "master" +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..37912b8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests>=2.31.0 \ No newline at end of file diff --git a/update-sdk-versions.py b/update-sdk-versions.py new file mode 100644 index 0000000..e8d7f95 --- /dev/null +++ b/update-sdk-versions.py @@ -0,0 +1,299 @@ +#!/usr/bin/env python3 +""" +CyberSource SDK Version Updater +Fetches latest releases from GitHub and updates java-sdk-versions.json +""" + +import json +import os +import sys +import argparse +from datetime import datetime +from typing import Dict, Optional +import requests +import subprocess + + +class SDKVersionUpdater: + def __init__(self, config_path: str = "config.json", github_token: Optional[str] = None): + """Initialize the updater with configuration""" + self.config = self.load_config(config_path) + # Priority: CLI argument > environment variable > config file + self.github_token = github_token or os.environ.get("GITHUB_TOKEN") or self.config.get("github_token", "") + self.json_file = self.config.get("json_file", "java-sdk-versions.json") + self.add_to_list = self.config.get("add_to_list", False) + + def load_config(self, config_path: str) -> Dict: + """Load configuration from JSON file""" + if os.path.exists(config_path): + with open(config_path, 'r') as f: + return json.load(f) + return {} + + def get_latest_release(self) -> Optional[Dict]: + """Fetch the latest release from GitHub API""" + owner = "CyberSource" + repo = "cybersource-rest-client-java" + url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest" + + headers = {} + if self.github_token: + headers["Authorization"] = f"token {self.github_token}" + + try: + # Disable SSL verification for corporate proxy + import urllib3 + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + response = requests.get(url, headers=headers, verify=False) + #disabled ssl verify above + response.raise_for_status() + return response.json() + except requests.RequestException as e: + print(f"Error fetching release from GitHub: {e}") + return None + + def parse_release_data(self, release: Dict) -> Dict: + """Parse GitHub release data into our format""" + tag_name = release.get("tag_name", "") + + # Extract version from tag (e.g., "cybersource-rest-client-java-0.0.84" -> "0.0.84") + version = tag_name.replace("cybersource-rest-client-java-", "") + + # Get release date + published_at = release.get("published_at", "") + release_date = published_at.split("T")[0] if published_at else datetime.now().strftime("%Y-%m-%d") + + # Construct download URL + download_url = f"https://github.com/CyberSource/cybersource-rest-client-java/archive/refs/tags/{tag_name}.zip" + + return { + "version": version, + "release_date": release_date, + "tag_name": tag_name, + "download_url": download_url + } + + def load_json_file(self) -> Dict: + """Load the current JSON file""" + try: + with open(self.json_file, 'r') as f: + return json.load(f) + except FileNotFoundError: + print(f"Error: {self.json_file} not found") + sys.exit(1) + + def save_json_file(self, data: Dict): + """Save updated data to JSON file""" + with open(self.json_file, 'w') as f: + json.dump(data, f, indent=2) + print(f"✓ Updated {self.json_file}") + + def update_json_data(self, current_data: Dict, new_release: Dict) -> tuple[Dict, bool]: + """Update JSON data with new release info""" + current_version = current_data.get("latest_version", "") + new_version = new_release["version"] + + # Check if this is actually a new version + if new_version == current_version: + print(f"No new release. Current version {current_version} is up to date.") + return current_data, False + + print(f"New release found: {new_version} (current: {current_version})") + + # Always update latest_version and last_updated + current_data["latest_version"] = new_version + current_data["last_updated"] = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") + + # If add_to_list is enabled, add to versions array + if self.add_to_list: + print(f"✓ Adding version {new_version} to versions list") + if "versions" not in current_data: + current_data["versions"] = [] + + # Insert at the beginning of the array + current_data["versions"].insert(0, new_release) + else: + print(f"✓ Updated latest_version only (add_to_list is disabled)") + + return current_data, True + + def create_git_branch(self, version: str) -> str: + """Create a new git branch for the update""" + branch_name = f"update-java-sdk-v{version}" + + try: + # Checkout master branch + print("→ Checking out master branch...") + subprocess.run(["git", "checkout", "master"], check=True, capture_output=True, text=True) + print("✓ Checked out master branch") + + # Pull latest changes + print("→ Pulling latest changes...") + subprocess.run(["git", "pull"], check=True, capture_output=True) + print("✓ Pulled latest changes") + + # Create and checkout new branch + print(f"→ Creating new branch: {branch_name}...") + subprocess.run(["git", "checkout", "-b", branch_name], check=True, capture_output=True) + print(f"✓ Created and checked out branch: {branch_name}") + return branch_name + except subprocess.CalledProcessError as e: + # Check if error is due to uncommitted changes + if e.returncode == 1 and e.stderr: + error_msg = e.stderr if isinstance(e.stderr, str) else e.stderr.decode('utf-8', errors='ignore') + if 'would be overwritten' in error_msg or 'pathspec' not in error_msg: + print("❌ Error: You have uncommitted changes on your current branch.") + print(" Git cannot switch to master with uncommitted changes.") + print() + print(" 💡 Solution: Stash your changes first:") + print(" git stash") + print(f" python update_sdk_versions.py --add-to-list --enable-pr --token \"YOUR_TOKEN\"") + print(" git stash pop") + print() + return None + + print(f"Error creating git branch: {e}") + return None + + def commit_and_push(self, version: str, branch_name: str): + """Commit changes and push to remote""" + try: + # Add the JSON file + print(f"→ Staging {self.json_file}...") + subprocess.run(["git", "add", self.json_file], check=True, capture_output=True) + print(f"✓ Staged {self.json_file}") + + # Commit with descriptive message + commit_message = f"Update Java SDK to version {version}" + print(f"→ Committing changes: {commit_message}...") + subprocess.run(["git", "commit", "-m", commit_message], check=True, capture_output=True) + print(f"✓ Committed changes: {commit_message}") + + # Push to remote + print(f"→ Pushing to origin/{branch_name}...") + subprocess.run(["git", "push", "-u", "origin", branch_name], check=True, capture_output=True) + print(f"✓ Pushed to origin/{branch_name}") + + except subprocess.CalledProcessError as e: + print(f"Error committing/pushing changes: {e}") + raise + + def create_pull_request(self, version: str, branch_name: str): + """Create a pull request via GitHub API""" + if not self.github_token: + print("⚠ No GitHub token found. Skipping PR creation.") + print(f" Please create PR manually for branch: {branch_name}") + return + + owner = self.config.get("pr_repo_owner", "CyberSource") + repo = self.config.get("pr_repo_name", "CAPIC-cybersource-rest-samples-java") + base_branch = self.config.get("pr_base_branch", "main") + + url = f"https://api.github.com/repos/{owner}/{repo}/pulls" + + headers = { + "Authorization": f"token {self.github_token}", + "Accept": "application/vnd.github.v3+json" + } + + pr_data = { + "title": f"Update Java SDK to version {version}", + "body": f"Automated update: CyberSource Java REST Client SDK to version {version}\n\n" + f"- Updated `latest_version` to {version}\n" + f"- Updated `last_updated` timestamp\n" + + (f"- Added version {version} to versions list\n" if self.add_to_list else ""), + "head": branch_name, + "base": base_branch + } + + try: + # Disable SSL verification for corporate proxy + import urllib3 + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + response = requests.post(url, headers=headers, json=pr_data, verify=False) + #passed ssl false above + response.raise_for_status() + pr_url = response.json().get("html_url") + print(f"✓ Pull request created: {pr_url}") + except requests.RequestException as e: + print(f"Error creating pull request: {e}") + if hasattr(e, 'response') and e.response is not None: + print(f" Response: {e.response.text}") + + def run(self, create_pr: bool = True): + """Main execution flow""" + print("=" * 60) + print("CyberSource SDK Version Updater") + print("=" * 60) + print(f"Mode: {'Add to list' if self.add_to_list else 'Update latest only'}") + print() + + # Fetch latest release + print("Fetching latest release from GitHub...") + release = self.get_latest_release() + if not release: + print("Failed to fetch release data") + sys.exit(1) + + # Parse release data + new_release = self.parse_release_data(release) + print(f"Latest GitHub release: {new_release['version']}") + print() + + # Load current JSON + current_data = self.load_json_file() + + # Update JSON data + updated_data, has_changes = self.update_json_data(current_data, new_release) + + if not has_changes: + print("\nNo updates needed.") + sys.exit(0) + + # Save updated JSON + self.save_json_file(updated_data) + print() + + # Git operations (if enabled) + if create_pr: + print("Creating git branch and PR...") + branch_name = self.create_git_branch(new_release["version"]) + + if branch_name: + self.commit_and_push(new_release["version"], branch_name) + self.create_pull_request(new_release["version"], branch_name) + + print() + print("=" * 60) + print("✓ Update completed successfully!") + print("=" * 60) + + +def main(): + parser = argparse.ArgumentParser(description="Update CyberSource SDK versions") + parser.add_argument("--add-to-list", action="store_true", + help="Add new version to versions array (default: only update latest)") + parser.add_argument("--enable-pr", action="store_true", + help="Enable creating git branch and pull request") + parser.add_argument("--token", type=str, default=None, + help="GitHub Personal Access Token (fine-grained or classic) for PR creation") + parser.add_argument("--config", default="config.json", + help="Path to config file (default: config.json)") + + args = parser.parse_args() + + # Create updater instance with optional token + updater = SDKVersionUpdater(config_path=args.config, github_token=args.token) + + # Override add_to_list if specified via command line + if args.add_to_list: + updater.add_to_list = True + + # Run the updater (only create PR if --enable-pr is set) + updater.run(create_pr=args.enable_pr) + + +if __name__ == "__main__": + main() From 806d9e19e7ab55b34bf6f120c96e9a8e49d7bed9 Mon Sep 17 00:00:00 2001 From: mahmishr Date: Thu, 8 Jan 2026 15:44:29 +0530 Subject: [PATCH 02/13] Update update-sdk-versions.py --- update-sdk-versions.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/update-sdk-versions.py b/update-sdk-versions.py index e8d7f95..2d24b1b 100644 --- a/update-sdk-versions.py +++ b/update-sdk-versions.py @@ -103,7 +103,7 @@ def update_json_data(self, current_data: Dict, new_release: Dict) -> tuple[Dict, # Always update latest_version and last_updated current_data["latest_version"] = new_version - current_data["last_updated"] = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") + current_data["last_updated"] = datetime.now(datetime.UTC).strftime("%Y-%m-%dT%H:%M:%SZ") # If add_to_list is enabled, add to versions array if self.add_to_list: @@ -252,18 +252,24 @@ def run(self, create_pr: bool = True): print("\nNo updates needed.") sys.exit(0) - # Save updated JSON - self.save_json_file(updated_data) - print() - - # Git operations (if enabled) + # Git operations (if enabled) - must be done BEFORE saving the file + branch_name = None if create_pr: print("Creating git branch and PR...") branch_name = self.create_git_branch(new_release["version"]) - if branch_name: - self.commit_and_push(new_release["version"], branch_name) - self.create_pull_request(new_release["version"], branch_name) + if not branch_name: + print("\n⚠ Warning: Could not create git branch. Saving changes locally only.") + print() + + # Save updated JSON (after branch creation but before commit) + self.save_json_file(updated_data) + print() + + # Commit and push if branch was created successfully + if create_pr and branch_name: + self.commit_and_push(new_release["version"], branch_name) + self.create_pull_request(new_release["version"], branch_name) print() print("=" * 60) From 9fc728993366b753746ffdcec0493b4f44f54452 Mon Sep 17 00:00:00 2001 From: mahmishr Date: Thu, 8 Jan 2026 15:46:32 +0530 Subject: [PATCH 03/13] Update update-sdk-versions.py --- update-sdk-versions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/update-sdk-versions.py b/update-sdk-versions.py index 2d24b1b..d1bd1e8 100644 --- a/update-sdk-versions.py +++ b/update-sdk-versions.py @@ -8,7 +8,7 @@ import os import sys import argparse -from datetime import datetime +from datetime import datetime, timezone from typing import Dict, Optional import requests import subprocess @@ -103,7 +103,7 @@ def update_json_data(self, current_data: Dict, new_release: Dict) -> tuple[Dict, # Always update latest_version and last_updated current_data["latest_version"] = new_version - current_data["last_updated"] = datetime.now(datetime.UTC).strftime("%Y-%m-%dT%H:%M:%SZ") + current_data["last_updated"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") # If add_to_list is enabled, add to versions array if self.add_to_list: From 2f813594de68fa2cf192e9ec0c24dc79a0613eb7 Mon Sep 17 00:00:00 2001 From: mahmishr Date: Thu, 8 Jan 2026 15:57:01 +0530 Subject: [PATCH 04/13] Update update-sdk-versions.py --- update-sdk-versions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/update-sdk-versions.py b/update-sdk-versions.py index d1bd1e8..b2cc69a 100644 --- a/update-sdk-versions.py +++ b/update-sdk-versions.py @@ -120,7 +120,7 @@ def update_json_data(self, current_data: Dict, new_release: Dict) -> tuple[Dict, def create_git_branch(self, version: str) -> str: """Create a new git branch for the update""" - branch_name = f"update-java-sdk-v{version}" + branch_name = f"update2-java-sdk-v{version}" try: # Checkout master branch From c63919eba92c3fee39e9e449bc5ec34887e578dd Mon Sep 17 00:00:00 2001 From: mahmishr Date: Thu, 8 Jan 2026 16:01:58 +0530 Subject: [PATCH 05/13] Update update-sdk-versions.py --- update-sdk-versions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/update-sdk-versions.py b/update-sdk-versions.py index b2cc69a..c1aa6c7 100644 --- a/update-sdk-versions.py +++ b/update-sdk-versions.py @@ -120,7 +120,7 @@ def update_json_data(self, current_data: Dict, new_release: Dict) -> tuple[Dict, def create_git_branch(self, version: str) -> str: """Create a new git branch for the update""" - branch_name = f"update2-java-sdk-v{version}" + branch_name = f"update3-java-sdk-v{version}" try: # Checkout master branch @@ -187,8 +187,8 @@ def create_pull_request(self, version: str, branch_name: str): return owner = self.config.get("pr_repo_owner", "CyberSource") - repo = self.config.get("pr_repo_name", "CAPIC-cybersource-rest-samples-java") - base_branch = self.config.get("pr_base_branch", "main") + repo = self.config.get("pr_repo_name", "cybersource-rest-samples-java") + base_branch = self.config.get("pr_base_branch", "master") url = f"https://api.github.com/repos/{owner}/{repo}/pulls" From 89218e279d7bb3c08e95d9b30a8aade9374907c0 Mon Sep 17 00:00:00 2001 From: mahmishr Date: Thu, 8 Jan 2026 16:04:34 +0530 Subject: [PATCH 06/13] Update config.json --- config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.json b/config.json index 9486389..4dfbaaa 100644 --- a/config.json +++ b/config.json @@ -3,6 +3,6 @@ "json_file": "java-sdk-versions.json", "add_to_list": false, "pr_repo_owner": "CyberSource", - "pr_repo_name": "CAPIC-cybersource-rest-samples-java", + "pr_repo_name": "cybersource-rest-samples-java", "pr_base_branch": "master" } \ No newline at end of file From b84b9f77d56a59268b2edde545e386d22a841bde Mon Sep 17 00:00:00 2001 From: mahmishr Date: Thu, 8 Jan 2026 16:06:14 +0530 Subject: [PATCH 07/13] Update update-sdk-versions.py --- update-sdk-versions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/update-sdk-versions.py b/update-sdk-versions.py index c1aa6c7..f59e8ba 100644 --- a/update-sdk-versions.py +++ b/update-sdk-versions.py @@ -120,7 +120,7 @@ def update_json_data(self, current_data: Dict, new_release: Dict) -> tuple[Dict, def create_git_branch(self, version: str) -> str: """Create a new git branch for the update""" - branch_name = f"update3-java-sdk-v{version}" + branch_name = f"autogenerated-v{version}-update" try: # Checkout master branch From f9bf08b9ac7acb9be869691fa5a2ebb3c9060004 Mon Sep 17 00:00:00 2001 From: mahmishr Date: Mon, 12 Jan 2026 10:46:52 +0530 Subject: [PATCH 08/13] update --- config.json | 71 ++++- update-sdk-versions-multi.py | 598 +++++++++++++++++++++++++++++++++++ 2 files changed, 665 insertions(+), 4 deletions(-) create mode 100644 update-sdk-versions-multi.py diff --git a/config.json b/config.json index 4dfbaaa..57dfb2c 100644 --- a/config.json +++ b/config.json @@ -1,8 +1,71 @@ { "github_token": "", - "json_file": "java-sdk-versions.json", "add_to_list": false, - "pr_repo_owner": "CyberSource", - "pr_repo_name": "cybersource-rest-samples-java", - "pr_base_branch": "master" + "languages": { + "java": { + "enabled": true, + "sdk_repo": "cybersource-rest-client-java", + "samples_repo": "cybersource-rest-samples-java", + "json_file": "java-sdk-versions.json", + "tag_pattern": "full_prefix", + "tag_prefix": "cybersource-rest-client-java-", + "pr_base_branch": "mcp-files", + "pr_target_branch": "master" + }, + "python": { + "enabled": true, + "sdk_repo": "cybersource-rest-client-python", + "samples_repo": "cybersource-rest-samples-python", + "json_file": "python-sdk-versions.json", + "tag_pattern": "bare_version", + "pr_base_branch": "mcp-files", + "pr_target_branch": "master" + }, + "php": { + "enabled": true, + "sdk_repo": "cybersource-rest-client-php", + "samples_repo": "cybersource-rest-samples-php", + "json_file": "php-sdk-versions.json", + "tag_pattern": "bare_version", + "pr_base_branch": "mcp-files", + "pr_target_branch": "master" + }, + "node": { + "enabled": true, + "sdk_repo": "cybersource-rest-client-node", + "samples_repo": "cybersource-rest-samples-node", + "json_file": "node-sdk-versions.json", + "tag_pattern": "bare_version", + "pr_base_branch": "mcp-files", + "pr_target_branch": "master" + }, + "ruby": { + "enabled": true, + "sdk_repo": "cybersource-rest-client-ruby", + "samples_repo": "cybersource-rest-samples-ruby", + "json_file": "ruby-sdk-versions.json", + "tag_pattern": "v_prefix", + "tag_prefix": "v", + "pr_base_branch": "mcp-files", + "pr_target_branch": "master" + }, + "dotnet": { + "enabled": true, + "sdk_repo": "cybersource-rest-client-dotnet", + "samples_repo": "cybersource-rest-samples-csharp", + "json_file": "dotnet-sdk-versions.json", + "tag_pattern": "bare_version", + "pr_base_branch": "mcp-files", + "pr_target_branch": "master" + }, + "dotnetstandard": { + "enabled": true, + "sdk_repo": "cybersource-rest-client-dotnetstandard", + "samples_repo": "cybersource-rest-samples-csharp", + "json_file": "dotnetstandard-sdk-versions.json", + "tag_pattern": "bare_version", + "pr_base_branch": "mcp-files", + "pr_target_branch": "master" + } + } } \ No newline at end of file diff --git a/update-sdk-versions-multi.py b/update-sdk-versions-multi.py new file mode 100644 index 0000000..6469636 --- /dev/null +++ b/update-sdk-versions-multi.py @@ -0,0 +1,598 @@ +#!/usr/bin/env python3 +""" +CyberSource SDK Version Updater (Multi-Language) +Fetches latest releases from GitHub and updates SDK version JSON files for all configured languages +""" +import os +import sys +import json +import argparse +import requests +import subprocess +import shutil +from datetime import datetime, timezone +from typing import Dict, Optional, Tuple, List + + +class SDKVersionUpdater: + def __init__(self, config_path: str = "config.json", github_token: Optional[str] = None): + """Initialize the updater with configuration""" + self.config = self.load_config(config_path) + # Priority: CLI argument > environment variable > config file + self.github_token = github_token or os.environ.get("GITHUB_TOKEN") or self.config.get("github_token", "") + self.add_to_list = self.config.get("add_to_list", False) + self.languages = self.config.get("languages", {}) + + def load_config(self, config_path: str) -> Dict: + """Load configuration from JSON file""" + if os.path.exists(config_path): + with open(config_path, 'r') as f: + return json.load(f) + return {} + + def validate_config(self) -> bool: + """Validate configuration before execution""" + if not self.languages: + print("❌ Error: No languages configured in config.json") + return False + + enabled_languages = [lang for lang, cfg in self.languages.items() if cfg.get("enabled", False)] + if not enabled_languages: + print("❌ Error: No languages are enabled in config.json") + return False + + print(f"→ Validating {len(enabled_languages)} enabled language(s): {', '.join(enabled_languages)}") + + # Validate each enabled language has required fields + for lang_name, lang_config in self.languages.items(): + if not lang_config.get("enabled", False): + continue + + required_fields = ["sdk_repo", "samples_repo", "json_file", "tag_pattern"] + missing = [f for f in required_fields if not lang_config.get(f)] + if missing: + print(f"❌ Error: Language '{lang_name}' missing required fields: {', '.join(missing)}") + return False + + print(f"✓ Configuration validated for {len(enabled_languages)} language(s)") + return True + + def get_latest_release(self, lang_config: Dict) -> Optional[Dict]: + """Fetch the latest release from GitHub API""" + owner = "CyberSource" + repo = lang_config["sdk_repo"] + url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest" + + headers = {} + if self.github_token: + headers["Authorization"] = f"token {self.github_token}" + + try: + # Disable SSL verification for corporate proxy #kept import here to be removed later + import urllib3 + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + response = requests.get(url, headers=headers, verify=False) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + print(f"Error fetching release from GitHub: {e}") + return None + + def parse_release_data(self, release: Dict, lang_config: Dict) -> Dict: + """Parse GitHub release data into our format""" + tag_name = release.get("tag_name", "") + tag_pattern = lang_config.get("tag_pattern", "bare_version") + + # Extract version from tag based on pattern + if tag_pattern == "full_prefix": + # e.g., "cybersource-rest-client-java-0.0.84" -> "0.0.84" + prefix = lang_config.get("tag_prefix", "") + version = tag_name.replace(prefix, "") + elif tag_pattern == "v_prefix": + # e.g., "v0.0.80" -> "0.0.80" + version = tag_name.lstrip("v") + else: # bare_version + # e.g., "0.0.72" -> "0.0.72" + version = tag_name + + # Get release date + published_at = release.get("published_at", "") + release_date = published_at.split("T")[0] if published_at else datetime.now().strftime("%Y-%m-%d") + + # Construct download URL + sdk_repo = lang_config["sdk_repo"] + download_url = f"https://github.com/CyberSource/{sdk_repo}/archive/refs/tags/{tag_name}.zip" + + return { + "version": version, + "release_date": release_date, + "tag_name": tag_name, + "download_url": download_url + } + + def load_json_file(self, file_path: str, lang_name: str, lang_config: Dict) -> Optional[Dict]: + """Load the current JSON file, return None if not found""" + try: + with open(file_path, 'r') as f: + return json.load(f) + except FileNotFoundError: + print(f"⚠️ File not found for {lang_name}: {os.path.basename(file_path)}") + print(f" Skipping {lang_name} SDK update") + return None + except json.JSONDecodeError as e: + print(f"❌ Error: Invalid JSON in {os.path.basename(file_path)}: {e}") + return None + + def save_json_file(self, data: Dict, file_path: str): + """Save updated data to JSON file""" + with open(file_path, 'w') as f: + json.dump(data, f, indent=2) + print(f"✓ Updated {os.path.basename(file_path)}") + + def update_json_data(self, current_data: Dict, new_release: Dict) -> Tuple[Dict, bool]: + """Update JSON data with new release info""" + current_version = current_data.get("latest_version", "") + new_version = new_release["version"] + + # Check if this is actually a new version + if new_version == current_version: + print(f"No new release. Current version {current_version} is up to date.") + return current_data, False + + print(f"New release found: {new_version} (current: {current_version})") + + # Always update latest_version and last_updated + current_data["latest_version"] = new_version + current_data["last_updated"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + # If add_to_list is enabled, add to versions array + if self.add_to_list: + print(f"✓ Adding version {new_version} to versions list") + if "versions" not in current_data: + current_data["versions"] = [] + + # Insert at the beginning of the array + current_data["versions"].insert(0, new_release) + else: + print(f"✓ Updated latest_version only (add_to_list is disabled)") + + return current_data, True + + def create_isolated_workspace(self, version: str) -> str: + """Create timestamped workspace for safe parallel execution""" + timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") + workspace = f"./workspace_sdk_update_{version}_{timestamp}" + + print(f"→ Creating isolated workspace: {workspace}") + os.makedirs(workspace, exist_ok=True) + print(f"✓ Created workspace: {workspace}") + + return workspace + + def clone_repository(self, workspace: str, lang_config: Dict) -> str: + """Clone the target repository into workspace""" + owner = "CyberSource" + repo = lang_config["samples_repo"] + base_branch = lang_config.get("pr_base_branch", "master") + + repo_url = f"https://github.com/{owner}/{repo}.git" + clone_dir = os.path.join(workspace, repo) + + print(f"→ Cloning repository: {repo_url}") + print(f" Branch: {base_branch}") + + try: + subprocess.run( + ["git", "clone", "--branch", base_branch, "--single-branch", repo_url, clone_dir], + check=True, + capture_output=True, + text=True + ) + print(f"✓ Cloned repository to: {clone_dir}") + return clone_dir + except subprocess.CalledProcessError as e: + print(f"❌ Error cloning repository: {e.stderr}") + raise + + def branch_exists_on_remote(self, repo_dir: str, branch_name: str) -> bool: + """Check if branch already exists on remote""" + try: + result = subprocess.run( + ["git", "ls-remote", "--heads", "origin", branch_name], + cwd=repo_dir, + capture_output=True, + text=True, + check=True + ) + return branch_name in result.stdout + except subprocess.CalledProcessError: + return False + + def create_git_branch(self, repo_dir: str, version: str, lang_name: str, lang_config: Dict) -> Optional[str]: + """Create a new git branch for the update""" + # Generate timestamp for unique branch name (format: YYYYMMDD-HHMMSS in GMT) + timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S") + branch_name = f"autogenerated-{lang_name}-{version}-update-{timestamp}" + base_branch = lang_config.get("pr_base_branch", "master") + + try: + # Checkout base branch + print(f"→ Checking out {base_branch} branch...") + subprocess.run( + ["git", "checkout", base_branch], + cwd=repo_dir, + check=True, + capture_output=True, + text=True + ) + print(f"✓ Checked out {base_branch} branch") + + # Pull latest changes + print("→ Pulling latest changes...") + subprocess.run( + ["git", "pull"], + cwd=repo_dir, + check=True, + capture_output=True + ) + print("✓ Pulled latest changes") + + # Create and checkout new branch + print(f"→ Creating new branch: {branch_name}...") + subprocess.run( + ["git", "checkout", "-b", branch_name], + cwd=repo_dir, + check=True, + capture_output=True + ) + print(f"✓ Created and checked out branch: {branch_name}") + return branch_name + except subprocess.CalledProcessError as e: + print(f"❌ Error creating git branch: {e}") + if e.stderr: + error_msg = e.stderr if isinstance(e.stderr, str) else e.stderr.decode('utf-8', errors='ignore') + print(f" Details: {error_msg}") + return None + + def commit_and_push(self, repo_dir: str, version: str, branch_name: str, lang_name: str, json_file: str): + """Commit changes and push to remote""" + try: + # Add the JSON file + print(f"→ Staging {json_file}...") + subprocess.run( + ["git", "add", json_file], + cwd=repo_dir, + check=True, + capture_output=True + ) + print(f"✓ Staged {json_file}") + + # Commit with descriptive message + lang_display = lang_name.upper() if lang_name in ["php", "node"] else lang_name.capitalize() + commit_message = f"Update {lang_display} SDK to version {version}" + print(f"→ Committing changes: {commit_message}...") + subprocess.run( + ["git", "commit", "-m", commit_message], + cwd=repo_dir, + check=True, + capture_output=True + ) + print(f"✓ Committed changes: {commit_message}") + + # Push to remote + print(f"→ Pushing to origin/{branch_name}...") + subprocess.run( + ["git", "push", "-u", "origin", branch_name], + cwd=repo_dir, + check=True, + capture_output=True, + text=True + ) + print(f"✓ Pushed to origin/{branch_name}") + + except subprocess.CalledProcessError as e: + print(f"❌ Error committing/pushing changes: {e}") + if e.stderr: + error_msg = e.stderr if isinstance(e.stderr, str) else e.stderr.decode('utf-8', errors='ignore') + print(f" Details: {error_msg}") + print() + print("💡 Possible solutions:") + print(" 1. Ensure you have push access to the repository") + print(" 2. Check if you need to configure Git credentials") + print(" 3. Verify your GitHub token has 'repo' permissions") + raise + + def create_pull_request(self, version: str, branch_name: str, lang_name: str, lang_config: Dict) -> Optional[str]: + """Create a pull request via GitHub API""" + if not self.github_token: + print("⚠ No GitHub token found. Skipping PR creation.") + print(f" Please create PR manually for branch: {branch_name}") + return None + + owner = "CyberSource" + repo = lang_config["samples_repo"] + target_branch = lang_config.get("pr_target_branch", "master") + + url = f"https://api.github.com/repos/{owner}/{repo}/pulls" + + headers = { + "Authorization": f"token {self.github_token}", + "Accept": "application/vnd.github.v3+json" + } + + lang_display = lang_name.upper() if lang_name in ["php", "node"] else lang_name.capitalize() + pr_data = { + "title": f"Update {lang_display} SDK to version {version}", + "body": f"Automated update: CyberSource {lang_display} REST Client SDK to version {version}\n\n" + f"- Updated `latest_version` to {version}\n" + f"- Updated `last_updated` timestamp\n" + + (f"- Added version {version} to versions list\n" if self.add_to_list else ""), + "head": branch_name, + "base": target_branch + } + + try: + # Disable SSL verification for corporate proxy #kept import here to be removed later + import urllib3 + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + response = requests.post(url, headers=headers, json=pr_data, verify=False) + response.raise_for_status() + pr_url = response.json().get("html_url") + print(f"✓ Pull request created: {pr_url}") + return pr_url + except requests.RequestException as e: + print(f"❌ Error creating pull request: {e}") + if hasattr(e, 'response') and e.response is not None: + print(f" Response: {e.response.text}") + return None + + def cleanup_workspace(self, workspace: str, auto_cleanup: bool = False): + """Clean up temporary workspace""" + if not os.path.exists(workspace): + return + + if not auto_cleanup: + print() + response = input(f"Delete workspace '{workspace}'? (y/n): ").strip().lower() + if response != 'y': + print(f"ℹ Workspace preserved at: {os.path.abspath(workspace)}") + return + + try: + shutil.rmtree(workspace) + print(f"✓ Cleaned up workspace: {workspace}") + except Exception as e: + print(f"⚠ Warning: Could not delete workspace: {e}") + + def display_summary(self, lang_name: str, version: str, branch_name: Optional[str], pr_url: Optional[str], json_file: str): + """Display operation summary""" + print() + print("=" * 70) + print(f"📋 {lang_name.upper()} OPERATION SUMMARY") + print("=" * 70) + print(f"✅ SDK Version: {version}") + print(f"✅ JSON Updated: {json_file}") + if branch_name: + print(f"✅ Branch Created: {branch_name}") + if pr_url: + print(f"✅ Pull Request: {pr_url}") + elif branch_name and not pr_url: + print("⚠️ Pull Request: Not created (check errors above)") + else: + print("ℹ️ Pull Request: Skipped (--enable-pr not specified)") + + print("=" * 70) + + def run(self, create_pr: bool = True, use_isolated_clone: bool = True, auto_cleanup: bool = False): + """Main execution flow for all enabled languages""" + print("=" * 70) + print("🚀 CyberSource SDK Version Updater (Multi-Language)") + print("=" * 70) + print(f"Mode: {'Add to list' if self.add_to_list else 'Update latest only'}") + print(f"Clone: {'Isolated workspace' if use_isolated_clone else 'Current directory'}") + print() + + # Validate configuration + if not self.validate_config(): + sys.exit(1) + print() + + # Get enabled languages + enabled_languages = [(name, config) for name, config in self.languages.items() + if config.get("enabled", False)] + + print(f"Processing {len(enabled_languages)} language(s)") + print() + + # Track results for final summary + results = { + "processed": [], + "skipped": [], + "failed": [] + } + + # Process each enabled language + for lang_name, lang_config in enabled_languages: + print("=" * 70) + print(f"🔹 Processing: {lang_name.upper()}") + print("=" * 70) + + try: + # Fetch latest release + print(f"📡 Fetching latest release for {lang_name}...") + release = self.get_latest_release(lang_config) + if not release: + print(f"❌ Failed to fetch release data for {lang_name}") + results["failed"].append(f"{lang_name}: Failed to fetch release") + print() + continue + + # Parse release data + new_release = self.parse_release_data(release, lang_config) + print(f"✓ Latest GitHub release: {new_release['version']}") + print() + + repo_dir = None + branch_name = None + pr_url = None + workspace = None + json_file = lang_config["json_file"] + + try: + if use_isolated_clone: + # Create isolated workspace and clone repository + print("🔧 Setting up isolated workspace...") + workspace = self.create_isolated_workspace(f"{lang_name}-{new_release['version']}") + repo_dir = self.clone_repository(workspace, lang_config) + json_path = os.path.join(repo_dir, json_file) + print() + else: + # Use current directory + repo_dir = os.getcwd() + json_path = json_file + + # Load current JSON from the repository + print(f"📄 Loading {json_file}...") + current_data = self.load_json_file(json_path, lang_name, lang_config) + + if current_data is None: + # File not found, skip this language + results["skipped"].append(f"{lang_name}: JSON file not found") + if workspace: + self.cleanup_workspace(workspace, auto_cleanup=True) + print() + continue + + # Update JSON data + updated_data, has_changes = self.update_json_data(current_data, new_release) + print() + + if not has_changes: + print(f"ℹ️ No updates needed for {lang_name}.") + results["skipped"].append(f"{lang_name}: Already up-to-date ({new_release['version']})") + if workspace: + self.cleanup_workspace(workspace, auto_cleanup=True) + print() + continue + + # Git operations (if enabled) + if create_pr: + print("🌿 Creating git branch and preparing PR...") + branch_name = self.create_git_branch(repo_dir, new_release["version"], lang_name, lang_config) + + if not branch_name: + print() + print(f"⚠️ Warning: Could not create git branch for {lang_name}. Skipping.") + results["failed"].append(f"{lang_name}: Failed to create branch") + if workspace: + self.cleanup_workspace(workspace, auto_cleanup=True) + print() + continue + + # Save updated JSON + print("💾 Saving changes...") + self.save_json_file(updated_data, json_path) + print() + + # Commit and push if branch was created successfully + if create_pr and branch_name: + print("📤 Committing and pushing changes...") + self.commit_and_push(repo_dir, new_release["version"], branch_name, lang_name, json_file) + print() + + print("🔀 Creating pull request...") + pr_url = self.create_pull_request(new_release["version"], branch_name, lang_name, lang_config) + print() + + # Display summary for this language + self.display_summary(lang_name, new_release["version"], branch_name, pr_url, json_file) + + results["processed"].append(f"{lang_name}: {new_release['version']}") + + except Exception as e: + print(f"❌ Error processing {lang_name}: {e}") + results["failed"].append(f"{lang_name}: {str(e)}") + + finally: + # Cleanup workspace if created + if workspace: + self.cleanup_workspace(workspace, auto_cleanup=auto_cleanup) + + print() + + except Exception as e: + print(f"❌ Unexpected error for {lang_name}: {e}") + results["failed"].append(f"{lang_name}: {str(e)}") + print() + + # Display final summary for all languages + print() + print("=" * 70) + print("📊 FINAL SUMMARY - ALL LANGUAGES") + print("=" * 70) + + if results["processed"]: + print(f"\n✅ Successfully Processed ({len(results['processed'])}):") + for item in results["processed"]: + print(f" • {item}") + + if results["skipped"]: + print(f"\n⏭️ Skipped ({len(results['skipped'])}):") + for item in results["skipped"]: + print(f" • {item}") + + if results["failed"]: + print(f"\n❌ Failed ({len(results['failed'])}):") + for item in results["failed"]: + print(f" • {item}") + + print() + print("=" * 70) + + +def main(): + parser = argparse.ArgumentParser( + description="Update CyberSource SDK versions for multiple languages", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + + # Create PRs for all enabled languages with isolated clones + python update-sdk-versions.py --add-to-list --enable-pr --token YOUR_TOKEN + + """ + ) + parser.add_argument("--add-to-list", action="store_true", + help="Add new version to versions array (default: only update latest)") + parser.add_argument("--enable-pr", action="store_true", + help="Enable creating git branch and pull request") + parser.add_argument("--no-clone", action="store_true", + help="Don't create isolated clone, use current directory") + parser.add_argument("--auto-cleanup", action="store_true", + help="Automatically cleanup workspace without prompting") + parser.add_argument("--token", type=str, default=None, + help="GitHub Personal Access Token for PR creation") + parser.add_argument("--config", default="config.json", + help="Path to config file (default: config.json)") + + args = parser.parse_args() + + # Create updater instance with optional token + updater = SDKVersionUpdater(config_path=args.config, github_token=args.token) + + # Override add_to_list if specified via command line + if args.add_to_list: + updater.add_to_list = True + + # Run the updater + updater.run( + create_pr=args.enable_pr, + use_isolated_clone=not args.no_clone, + auto_cleanup=args.auto_cleanup + ) + + +if __name__ == "__main__": + main() From f6200637a7e71ef7b266e19b7197dba89eb0897a Mon Sep 17 00:00:00 2001 From: mahmishr Date: Mon, 12 Jan 2026 10:51:42 +0530 Subject: [PATCH 09/13] Refactor SDK version updater script --- update-sdk-versions.py | 304 ----------------------------------------- 1 file changed, 304 deletions(-) diff --git a/update-sdk-versions.py b/update-sdk-versions.py index f59e8ba..8b13789 100644 --- a/update-sdk-versions.py +++ b/update-sdk-versions.py @@ -1,305 +1 @@ -#!/usr/bin/env python3 -""" -CyberSource SDK Version Updater -Fetches latest releases from GitHub and updates java-sdk-versions.json -""" -import json -import os -import sys -import argparse -from datetime import datetime, timezone -from typing import Dict, Optional -import requests -import subprocess - - -class SDKVersionUpdater: - def __init__(self, config_path: str = "config.json", github_token: Optional[str] = None): - """Initialize the updater with configuration""" - self.config = self.load_config(config_path) - # Priority: CLI argument > environment variable > config file - self.github_token = github_token or os.environ.get("GITHUB_TOKEN") or self.config.get("github_token", "") - self.json_file = self.config.get("json_file", "java-sdk-versions.json") - self.add_to_list = self.config.get("add_to_list", False) - - def load_config(self, config_path: str) -> Dict: - """Load configuration from JSON file""" - if os.path.exists(config_path): - with open(config_path, 'r') as f: - return json.load(f) - return {} - - def get_latest_release(self) -> Optional[Dict]: - """Fetch the latest release from GitHub API""" - owner = "CyberSource" - repo = "cybersource-rest-client-java" - url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest" - - headers = {} - if self.github_token: - headers["Authorization"] = f"token {self.github_token}" - - try: - # Disable SSL verification for corporate proxy - import urllib3 - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - response = requests.get(url, headers=headers, verify=False) - #disabled ssl verify above - response.raise_for_status() - return response.json() - except requests.RequestException as e: - print(f"Error fetching release from GitHub: {e}") - return None - - def parse_release_data(self, release: Dict) -> Dict: - """Parse GitHub release data into our format""" - tag_name = release.get("tag_name", "") - - # Extract version from tag (e.g., "cybersource-rest-client-java-0.0.84" -> "0.0.84") - version = tag_name.replace("cybersource-rest-client-java-", "") - - # Get release date - published_at = release.get("published_at", "") - release_date = published_at.split("T")[0] if published_at else datetime.now().strftime("%Y-%m-%d") - - # Construct download URL - download_url = f"https://github.com/CyberSource/cybersource-rest-client-java/archive/refs/tags/{tag_name}.zip" - - return { - "version": version, - "release_date": release_date, - "tag_name": tag_name, - "download_url": download_url - } - - def load_json_file(self) -> Dict: - """Load the current JSON file""" - try: - with open(self.json_file, 'r') as f: - return json.load(f) - except FileNotFoundError: - print(f"Error: {self.json_file} not found") - sys.exit(1) - - def save_json_file(self, data: Dict): - """Save updated data to JSON file""" - with open(self.json_file, 'w') as f: - json.dump(data, f, indent=2) - print(f"✓ Updated {self.json_file}") - - def update_json_data(self, current_data: Dict, new_release: Dict) -> tuple[Dict, bool]: - """Update JSON data with new release info""" - current_version = current_data.get("latest_version", "") - new_version = new_release["version"] - - # Check if this is actually a new version - if new_version == current_version: - print(f"No new release. Current version {current_version} is up to date.") - return current_data, False - - print(f"New release found: {new_version} (current: {current_version})") - - # Always update latest_version and last_updated - current_data["latest_version"] = new_version - current_data["last_updated"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") - - # If add_to_list is enabled, add to versions array - if self.add_to_list: - print(f"✓ Adding version {new_version} to versions list") - if "versions" not in current_data: - current_data["versions"] = [] - - # Insert at the beginning of the array - current_data["versions"].insert(0, new_release) - else: - print(f"✓ Updated latest_version only (add_to_list is disabled)") - - return current_data, True - - def create_git_branch(self, version: str) -> str: - """Create a new git branch for the update""" - branch_name = f"autogenerated-v{version}-update" - - try: - # Checkout master branch - print("→ Checking out master branch...") - subprocess.run(["git", "checkout", "master"], check=True, capture_output=True, text=True) - print("✓ Checked out master branch") - - # Pull latest changes - print("→ Pulling latest changes...") - subprocess.run(["git", "pull"], check=True, capture_output=True) - print("✓ Pulled latest changes") - - # Create and checkout new branch - print(f"→ Creating new branch: {branch_name}...") - subprocess.run(["git", "checkout", "-b", branch_name], check=True, capture_output=True) - print(f"✓ Created and checked out branch: {branch_name}") - return branch_name - except subprocess.CalledProcessError as e: - # Check if error is due to uncommitted changes - if e.returncode == 1 and e.stderr: - error_msg = e.stderr if isinstance(e.stderr, str) else e.stderr.decode('utf-8', errors='ignore') - if 'would be overwritten' in error_msg or 'pathspec' not in error_msg: - print("❌ Error: You have uncommitted changes on your current branch.") - print(" Git cannot switch to master with uncommitted changes.") - print() - print(" 💡 Solution: Stash your changes first:") - print(" git stash") - print(f" python update_sdk_versions.py --add-to-list --enable-pr --token \"YOUR_TOKEN\"") - print(" git stash pop") - print() - return None - - print(f"Error creating git branch: {e}") - return None - - def commit_and_push(self, version: str, branch_name: str): - """Commit changes and push to remote""" - try: - # Add the JSON file - print(f"→ Staging {self.json_file}...") - subprocess.run(["git", "add", self.json_file], check=True, capture_output=True) - print(f"✓ Staged {self.json_file}") - - # Commit with descriptive message - commit_message = f"Update Java SDK to version {version}" - print(f"→ Committing changes: {commit_message}...") - subprocess.run(["git", "commit", "-m", commit_message], check=True, capture_output=True) - print(f"✓ Committed changes: {commit_message}") - - # Push to remote - print(f"→ Pushing to origin/{branch_name}...") - subprocess.run(["git", "push", "-u", "origin", branch_name], check=True, capture_output=True) - print(f"✓ Pushed to origin/{branch_name}") - - except subprocess.CalledProcessError as e: - print(f"Error committing/pushing changes: {e}") - raise - - def create_pull_request(self, version: str, branch_name: str): - """Create a pull request via GitHub API""" - if not self.github_token: - print("⚠ No GitHub token found. Skipping PR creation.") - print(f" Please create PR manually for branch: {branch_name}") - return - - owner = self.config.get("pr_repo_owner", "CyberSource") - repo = self.config.get("pr_repo_name", "cybersource-rest-samples-java") - base_branch = self.config.get("pr_base_branch", "master") - - url = f"https://api.github.com/repos/{owner}/{repo}/pulls" - - headers = { - "Authorization": f"token {self.github_token}", - "Accept": "application/vnd.github.v3+json" - } - - pr_data = { - "title": f"Update Java SDK to version {version}", - "body": f"Automated update: CyberSource Java REST Client SDK to version {version}\n\n" - f"- Updated `latest_version` to {version}\n" - f"- Updated `last_updated` timestamp\n" - + (f"- Added version {version} to versions list\n" if self.add_to_list else ""), - "head": branch_name, - "base": base_branch - } - - try: - # Disable SSL verification for corporate proxy - import urllib3 - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - response = requests.post(url, headers=headers, json=pr_data, verify=False) - #passed ssl false above - response.raise_for_status() - pr_url = response.json().get("html_url") - print(f"✓ Pull request created: {pr_url}") - except requests.RequestException as e: - print(f"Error creating pull request: {e}") - if hasattr(e, 'response') and e.response is not None: - print(f" Response: {e.response.text}") - - def run(self, create_pr: bool = True): - """Main execution flow""" - print("=" * 60) - print("CyberSource SDK Version Updater") - print("=" * 60) - print(f"Mode: {'Add to list' if self.add_to_list else 'Update latest only'}") - print() - - # Fetch latest release - print("Fetching latest release from GitHub...") - release = self.get_latest_release() - if not release: - print("Failed to fetch release data") - sys.exit(1) - - # Parse release data - new_release = self.parse_release_data(release) - print(f"Latest GitHub release: {new_release['version']}") - print() - - # Load current JSON - current_data = self.load_json_file() - - # Update JSON data - updated_data, has_changes = self.update_json_data(current_data, new_release) - - if not has_changes: - print("\nNo updates needed.") - sys.exit(0) - - # Git operations (if enabled) - must be done BEFORE saving the file - branch_name = None - if create_pr: - print("Creating git branch and PR...") - branch_name = self.create_git_branch(new_release["version"]) - - if not branch_name: - print("\n⚠ Warning: Could not create git branch. Saving changes locally only.") - print() - - # Save updated JSON (after branch creation but before commit) - self.save_json_file(updated_data) - print() - - # Commit and push if branch was created successfully - if create_pr and branch_name: - self.commit_and_push(new_release["version"], branch_name) - self.create_pull_request(new_release["version"], branch_name) - - print() - print("=" * 60) - print("✓ Update completed successfully!") - print("=" * 60) - - -def main(): - parser = argparse.ArgumentParser(description="Update CyberSource SDK versions") - parser.add_argument("--add-to-list", action="store_true", - help="Add new version to versions array (default: only update latest)") - parser.add_argument("--enable-pr", action="store_true", - help="Enable creating git branch and pull request") - parser.add_argument("--token", type=str, default=None, - help="GitHub Personal Access Token (fine-grained or classic) for PR creation") - parser.add_argument("--config", default="config.json", - help="Path to config file (default: config.json)") - - args = parser.parse_args() - - # Create updater instance with optional token - updater = SDKVersionUpdater(config_path=args.config, github_token=args.token) - - # Override add_to_list if specified via command line - if args.add_to_list: - updater.add_to_list = True - - # Run the updater (only create PR if --enable-pr is set) - updater.run(create_pr=args.enable_pr) - - -if __name__ == "__main__": - main() From 9cd768c098ea3c15bd15bdfa2292b9ad078c3e8c Mon Sep 17 00:00:00 2001 From: mahmishr Date: Mon, 12 Jan 2026 10:52:01 +0530 Subject: [PATCH 10/13] Remove requests package from requirements --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 37912b8..8b13789 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -requests>=2.31.0 \ No newline at end of file + From d46151d38f3eb5a54edf86b22365b8b5a032e123 Mon Sep 17 00:00:00 2001 From: mahmishr Date: Mon, 12 Jan 2026 10:52:41 +0530 Subject: [PATCH 11/13] Delete update-sdk-versions.py --- update-sdk-versions.py | 1 - 1 file changed, 1 deletion(-) delete mode 100644 update-sdk-versions.py diff --git a/update-sdk-versions.py b/update-sdk-versions.py deleted file mode 100644 index 8b13789..0000000 --- a/update-sdk-versions.py +++ /dev/null @@ -1 +0,0 @@ - From 3274face0a32d56a9ecc35ff1d6bd1d4595306c2 Mon Sep 17 00:00:00 2001 From: mahmishr Date: Mon, 12 Jan 2026 10:52:58 +0530 Subject: [PATCH 12/13] Delete requirements.txt --- requirements.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 8b13789..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ - From f71e12a75ccc847ce62088f2697886fe4932d16b Mon Sep 17 00:00:00 2001 From: mahmishr Date: Wed, 14 Jan 2026 13:23:34 +0530 Subject: [PATCH 13/13] comments resolved --- config.json | 47 ++-- update-sdk-versions-multi.py | 473 +++++++++++++++++++++-------------- 2 files changed, 299 insertions(+), 221 deletions(-) diff --git a/config.json b/config.json index 57dfb2c..b774f9d 100644 --- a/config.json +++ b/config.json @@ -1,71 +1,56 @@ { + "_comment_1": "Set 'enabled' to false for any language you want to skip during updates", + "_comment_2": "Keep pr_base_branch and pr_target_branch the same - usually 'master'", + "_comment_3": "Usage: python update-sdk-versions-multi.py --enable-pr --token ghp_xxx", + "_comment_4": "With version history: python update-sdk-versions-multi.py --add-to-versions-list --enable-pr --token ghp_xxx", + "github_token": "", - "add_to_list": false, + "add_to_versions_list": false, + "sdk_support_repo": "cybersource-mcp-sdk-support-files", + "pr_base_branch": "test-all-sdk", + "pr_target_branch": "test-all-sdk", "languages": { "java": { "enabled": true, "sdk_repo": "cybersource-rest-client-java", - "samples_repo": "cybersource-rest-samples-java", "json_file": "java-sdk-versions.json", - "tag_pattern": "full_prefix", - "tag_prefix": "cybersource-rest-client-java-", - "pr_base_branch": "mcp-files", - "pr_target_branch": "master" + "tag_format": "cybersource-rest-client-java-{version}" }, "python": { "enabled": true, "sdk_repo": "cybersource-rest-client-python", - "samples_repo": "cybersource-rest-samples-python", "json_file": "python-sdk-versions.json", - "tag_pattern": "bare_version", - "pr_base_branch": "mcp-files", - "pr_target_branch": "master" + "tag_format": "{version}" }, "php": { "enabled": true, "sdk_repo": "cybersource-rest-client-php", - "samples_repo": "cybersource-rest-samples-php", "json_file": "php-sdk-versions.json", - "tag_pattern": "bare_version", - "pr_base_branch": "mcp-files", - "pr_target_branch": "master" + "tag_format": "{version}" }, "node": { "enabled": true, "sdk_repo": "cybersource-rest-client-node", - "samples_repo": "cybersource-rest-samples-node", "json_file": "node-sdk-versions.json", - "tag_pattern": "bare_version", - "pr_base_branch": "mcp-files", - "pr_target_branch": "master" + "tag_format": "{version}" }, "ruby": { "enabled": true, "sdk_repo": "cybersource-rest-client-ruby", - "samples_repo": "cybersource-rest-samples-ruby", "json_file": "ruby-sdk-versions.json", - "tag_pattern": "v_prefix", - "tag_prefix": "v", - "pr_base_branch": "mcp-files", - "pr_target_branch": "master" + "tag_format": "v{version}" }, "dotnet": { "enabled": true, "sdk_repo": "cybersource-rest-client-dotnet", - "samples_repo": "cybersource-rest-samples-csharp", "json_file": "dotnet-sdk-versions.json", - "tag_pattern": "bare_version", - "pr_base_branch": "mcp-files", - "pr_target_branch": "master" + "tag_format": "{version}" }, "dotnetstandard": { "enabled": true, "sdk_repo": "cybersource-rest-client-dotnetstandard", - "samples_repo": "cybersource-rest-samples-csharp", "json_file": "dotnetstandard-sdk-versions.json", - "tag_pattern": "bare_version", - "pr_base_branch": "mcp-files", - "pr_target_branch": "master" + "tag_format": "{version}" } } } \ No newline at end of file diff --git a/update-sdk-versions-multi.py b/update-sdk-versions-multi.py index 6469636..683d8b5 100644 --- a/update-sdk-versions-multi.py +++ b/update-sdk-versions-multi.py @@ -1,7 +1,14 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python3 """ CyberSource SDK Version Updater (Multi-Language) Fetches latest releases from GitHub and updates SDK version JSON files for all configured languages + +Usage: + Basic (updates latest_version and last_updated only): + python update-sdk-versions-multi.py --enable-pr --token ghp_xxx + + With version history (also adds to versions array): + python update-sdk-versions-multi.py --add-to-versions-list --enable-pr --token ghp_xxx """ import os import sys @@ -20,8 +27,11 @@ def __init__(self, config_path: str = "config.json", github_token: Optional[str] self.config = self.load_config(config_path) # Priority: CLI argument > environment variable > config file self.github_token = github_token or os.environ.get("GITHUB_TOKEN") or self.config.get("github_token", "") - self.add_to_list = self.config.get("add_to_list", False) + self.add_to_versions_list = self.config.get("add_to_versions_list", False) self.languages = self.config.get("languages", {}) + self.sdk_support_repo = self.config.get("sdk_support_repo", "cybersource-mcp-sdk-support-files") + self.pr_base_branch = self.config.get("pr_base_branch", "test-all-sdk") + self.pr_target_branch = self.config.get("pr_target_branch", "test-all-sdk") def load_config(self, config_path: str) -> Dict: """Load configuration from JSON file""" @@ -32,29 +42,33 @@ def load_config(self, config_path: str) -> Dict: def validate_config(self) -> bool: """Validate configuration before execution""" + if not self.sdk_support_repo: + print("Error: No sdk_support_repo configured in config.json") + return False + if not self.languages: - print("❌ Error: No languages configured in config.json") + print("Error: No languages configured in config.json") return False enabled_languages = [lang for lang, cfg in self.languages.items() if cfg.get("enabled", False)] if not enabled_languages: - print("❌ Error: No languages are enabled in config.json") + print("Error: No languages are enabled in config.json") return False - print(f"→ Validating {len(enabled_languages)} enabled language(s): {', '.join(enabled_languages)}") + print(f"Validating {len(enabled_languages)} enabled language(s): {', '.join(enabled_languages)}") # Validate each enabled language has required fields for lang_name, lang_config in self.languages.items(): if not lang_config.get("enabled", False): continue - required_fields = ["sdk_repo", "samples_repo", "json_file", "tag_pattern"] + required_fields = ["sdk_repo", "json_file", "tag_format"] missing = [f for f in required_fields if not lang_config.get(f)] if missing: - print(f"❌ Error: Language '{lang_name}' missing required fields: {', '.join(missing)}") + print(f"Error: Language '{lang_name}' missing required fields: {', '.join(missing)}") return False - print(f"✓ Configuration validated for {len(enabled_languages)} language(s)") + print(f"Configuration validated for {len(enabled_languages)} language(s)") return True def get_latest_release(self, lang_config: Dict) -> Optional[Dict]: @@ -82,18 +96,22 @@ def get_latest_release(self, lang_config: Dict) -> Optional[Dict]: def parse_release_data(self, release: Dict, lang_config: Dict) -> Dict: """Parse GitHub release data into our format""" tag_name = release.get("tag_name", "") - tag_pattern = lang_config.get("tag_pattern", "bare_version") - - # Extract version from tag based on pattern - if tag_pattern == "full_prefix": - # e.g., "cybersource-rest-client-java-0.0.84" -> "0.0.84" - prefix = lang_config.get("tag_prefix", "") - version = tag_name.replace(prefix, "") - elif tag_pattern == "v_prefix": - # e.g., "v0.0.80" -> "0.0.80" - version = tag_name.lstrip("v") - else: # bare_version - # e.g., "0.0.72" -> "0.0.72" + tag_format = lang_config.get("tag_format", "{version}") + + # Extract version from tag using tag_format + # tag_format examples: "cybersource-rest-client-java-{version}", "{version}", "v{version}" + if "{version}" in tag_format: + # Remove the format parts to extract version + version = tag_name + # Replace parts before {version} + prefix = tag_format.split("{version}")[0] + if prefix: + version = version.replace(prefix, "", 1) + # Replace parts after {version} + suffix = tag_format.split("{version}")[1] if len(tag_format.split("{version}")) > 1 else "" + if suffix: + version = version.replace(suffix, "", 1) + else: version = tag_name # Get release date @@ -117,18 +135,18 @@ def load_json_file(self, file_path: str, lang_name: str, lang_config: Dict) -> O with open(file_path, 'r') as f: return json.load(f) except FileNotFoundError: - print(f"⚠️ File not found for {lang_name}: {os.path.basename(file_path)}") + print(f"File not found for {lang_name}: {os.path.basename(file_path)}") print(f" Skipping {lang_name} SDK update") return None except json.JSONDecodeError as e: - print(f"❌ Error: Invalid JSON in {os.path.basename(file_path)}: {e}") + print(f"Error: Invalid JSON in {os.path.basename(file_path)}: {e}") return None def save_json_file(self, data: Dict, file_path: str): """Save updated data to JSON file""" with open(file_path, 'w') as f: json.dump(data, f, indent=2) - print(f"✓ Updated {os.path.basename(file_path)}") + print(f"Updated {os.path.basename(file_path)}") def update_json_data(self, current_data: Dict, new_release: Dict) -> Tuple[Dict, bool]: """Update JSON data with new release info""" @@ -146,40 +164,40 @@ def update_json_data(self, current_data: Dict, new_release: Dict) -> Tuple[Dict, current_data["latest_version"] = new_version current_data["last_updated"] = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") - # If add_to_list is enabled, add to versions array - if self.add_to_list: - print(f"✓ Adding version {new_version} to versions list") + # If add_to_versions_list is enabled, add to versions array + if self.add_to_versions_list: + print(f"Adding version {new_version} to versions list") if "versions" not in current_data: current_data["versions"] = [] # Insert at the beginning of the array current_data["versions"].insert(0, new_release) else: - print(f"✓ Updated latest_version only (add_to_list is disabled)") + print(f"Updated latest_version only (add_to_versions_list is disabled)") return current_data, True - def create_isolated_workspace(self, version: str) -> str: + def create_isolated_workspace(self) -> str: """Create timestamped workspace for safe parallel execution""" timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S") - workspace = f"./workspace_sdk_update_{version}_{timestamp}" + workspace = f"./workspace_sdk_update_{timestamp}" - print(f"→ Creating isolated workspace: {workspace}") + print(f"Creating isolated workspace: {workspace}") os.makedirs(workspace, exist_ok=True) - print(f"✓ Created workspace: {workspace}") + print(f"Created workspace: {workspace}") return workspace - def clone_repository(self, workspace: str, lang_config: Dict) -> str: - """Clone the target repository into workspace""" + def clone_repository(self, workspace: str) -> str: + """Clone the central repository into workspace""" owner = "CyberSource" - repo = lang_config["samples_repo"] - base_branch = lang_config.get("pr_base_branch", "master") + repo = self.sdk_support_repo + base_branch = self.pr_base_branch repo_url = f"https://github.com/{owner}/{repo}.git" clone_dir = os.path.join(workspace, repo) - print(f"→ Cloning repository: {repo_url}") + print(f"Cloning repository: {repo_url}") print(f" Branch: {base_branch}") try: @@ -189,10 +207,10 @@ def clone_repository(self, workspace: str, lang_config: Dict) -> str: capture_output=True, text=True ) - print(f"✓ Cloned repository to: {clone_dir}") + print(f"Cloned repository to: {clone_dir}") return clone_dir except subprocess.CalledProcessError as e: - print(f"❌ Error cloning repository: {e.stderr}") + print(f"Error cloning repository: {e.stderr}") raise def branch_exists_on_remote(self, repo_dir: str, branch_name: str) -> bool: @@ -209,16 +227,16 @@ def branch_exists_on_remote(self, repo_dir: str, branch_name: str) -> bool: except subprocess.CalledProcessError: return False - def create_git_branch(self, repo_dir: str, version: str, lang_name: str, lang_config: Dict) -> Optional[str]: + def create_git_branch(self, repo_dir: str) -> Optional[str]: """Create a new git branch for the update""" # Generate timestamp for unique branch name (format: YYYYMMDD-HHMMSS in GMT) timestamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S") - branch_name = f"autogenerated-{lang_name}-{version}-update-{timestamp}" - base_branch = lang_config.get("pr_base_branch", "master") + branch_name = f"autogenerated-sdk-updates-{timestamp}" + base_branch = self.pr_base_branch try: # Checkout base branch - print(f"→ Checking out {base_branch} branch...") + print(f"Checking out {base_branch} branch...") subprocess.run( ["git", "checkout", base_branch], cwd=repo_dir, @@ -226,62 +244,62 @@ def create_git_branch(self, repo_dir: str, version: str, lang_name: str, lang_co capture_output=True, text=True ) - print(f"✓ Checked out {base_branch} branch") + print(f"Checked out {base_branch} branch") # Pull latest changes - print("→ Pulling latest changes...") + print("Pulling latest changes...") subprocess.run( ["git", "pull"], cwd=repo_dir, check=True, capture_output=True ) - print("✓ Pulled latest changes") + print("Pulled latest changes") # Create and checkout new branch - print(f"→ Creating new branch: {branch_name}...") + print(f"Creating new branch: {branch_name}...") subprocess.run( ["git", "checkout", "-b", branch_name], cwd=repo_dir, check=True, capture_output=True ) - print(f"✓ Created and checked out branch: {branch_name}") + print(f"Created and checked out branch: {branch_name}") return branch_name except subprocess.CalledProcessError as e: - print(f"❌ Error creating git branch: {e}") + print(f"Error creating git branch: {e}") if e.stderr: error_msg = e.stderr if isinstance(e.stderr, str) else e.stderr.decode('utf-8', errors='ignore') print(f" Details: {error_msg}") return None - def commit_and_push(self, repo_dir: str, version: str, branch_name: str, lang_name: str, json_file: str): + def commit_and_push(self, repo_dir: str, branch_name: str, updated_files: List[str]): """Commit changes and push to remote""" try: - # Add the JSON file - print(f"→ Staging {json_file}...") - subprocess.run( - ["git", "add", json_file], - cwd=repo_dir, - check=True, - capture_output=True - ) - print(f"✓ Staged {json_file}") + # Add all updated JSON files + print(f"Staging {len(updated_files)} file(s)...") + for json_file in updated_files: + subprocess.run( + ["git", "add", json_file], + cwd=repo_dir, + check=True, + capture_output=True + ) + print(f" Staged {json_file}") # Commit with descriptive message - lang_display = lang_name.upper() if lang_name in ["php", "node"] else lang_name.capitalize() - commit_message = f"Update {lang_display} SDK to version {version}" - print(f"→ Committing changes: {commit_message}...") + commit_message = "Update SDK versions" + print(f"Committing changes: {commit_message}...") subprocess.run( ["git", "commit", "-m", commit_message], cwd=repo_dir, check=True, capture_output=True ) - print(f"✓ Committed changes: {commit_message}") + print(f"Committed changes: {commit_message}") # Push to remote - print(f"→ Pushing to origin/{branch_name}...") + print(f"Pushing to origin/{branch_name}...") subprocess.run( ["git", "push", "-u", "origin", branch_name], cwd=repo_dir, @@ -289,30 +307,30 @@ def commit_and_push(self, repo_dir: str, version: str, branch_name: str, lang_na capture_output=True, text=True ) - print(f"✓ Pushed to origin/{branch_name}") + print(f"Pushed to origin/{branch_name}") except subprocess.CalledProcessError as e: - print(f"❌ Error committing/pushing changes: {e}") + print(f"Error committing/pushing changes: {e}") if e.stderr: error_msg = e.stderr if isinstance(e.stderr, str) else e.stderr.decode('utf-8', errors='ignore') print(f" Details: {error_msg}") print() - print("💡 Possible solutions:") + print("Possible solutions:") print(" 1. Ensure you have push access to the repository") print(" 2. Check if you need to configure Git credentials") print(" 3. Verify your GitHub token has 'repo' permissions") raise - def create_pull_request(self, version: str, branch_name: str, lang_name: str, lang_config: Dict) -> Optional[str]: + def create_pull_request(self, branch_name: str, updated_languages: List[Tuple[str, str]]) -> Optional[str]: """Create a pull request via GitHub API""" if not self.github_token: - print("⚠ No GitHub token found. Skipping PR creation.") + print("No GitHub token found. Skipping PR creation.") print(f" Please create PR manually for branch: {branch_name}") return None owner = "CyberSource" - repo = lang_config["samples_repo"] - target_branch = lang_config.get("pr_target_branch", "master") + repo = self.sdk_support_repo + target_branch = self.pr_target_branch url = f"https://api.github.com/repos/{owner}/{repo}/pulls" @@ -321,13 +339,15 @@ def create_pull_request(self, version: str, branch_name: str, lang_name: str, la "Accept": "application/vnd.github.v3+json" } - lang_display = lang_name.upper() if lang_name in ["php", "node"] else lang_name.capitalize() + # Build PR body with all updated languages + updates_list = "\n".join([f"- {lang}: {version}" for lang, version in updated_languages]) + pr_data = { - "title": f"Update {lang_display} SDK to version {version}", - "body": f"Automated update: CyberSource {lang_display} REST Client SDK to version {version}\n\n" - f"- Updated `latest_version` to {version}\n" - f"- Updated `last_updated` timestamp\n" - + (f"- Added version {version} to versions list\n" if self.add_to_list else ""), + "title": "Update SDK versions", + "body": f"Automated SDK version updates:\n\n{updates_list}\n\n" + f"- Updated `latest_version` fields\n" + f"- Updated `last_updated` timestamps\n" + + ("- Added versions to history lists\n" if self.add_to_versions_list else ""), "head": branch_name, "base": target_branch } @@ -340,58 +360,53 @@ def create_pull_request(self, version: str, branch_name: str, lang_name: str, la response = requests.post(url, headers=headers, json=pr_data, verify=False) response.raise_for_status() pr_url = response.json().get("html_url") - print(f"✓ Pull request created: {pr_url}") + print(f"Pull request created: {pr_url}") return pr_url except requests.RequestException as e: - print(f"❌ Error creating pull request: {e}") + print(f"Error creating pull request: {e}") if hasattr(e, 'response') and e.response is not None: print(f" Response: {e.response.text}") return None - def cleanup_workspace(self, workspace: str, auto_cleanup: bool = False): + def cleanup_workspace(self, workspace: str): """Clean up temporary workspace""" if not os.path.exists(workspace): return - if not auto_cleanup: - print() - response = input(f"Delete workspace '{workspace}'? (y/n): ").strip().lower() - if response != 'y': - print(f"ℹ Workspace preserved at: {os.path.abspath(workspace)}") - return - try: shutil.rmtree(workspace) - print(f"✓ Cleaned up workspace: {workspace}") + print(f"Cleaned up workspace: {workspace}") except Exception as e: - print(f"⚠ Warning: Could not delete workspace: {e}") + print(f"Warning: Could not delete workspace: {e}") - def display_summary(self, lang_name: str, version: str, branch_name: Optional[str], pr_url: Optional[str], json_file: str): + def display_summary(self, updated_languages: List[Tuple[str, str]], branch_name: Optional[str], pr_url: Optional[str]): """Display operation summary""" print() print("=" * 70) - print(f"📋 {lang_name.upper()} OPERATION SUMMARY") + print("OPERATION SUMMARY") print("=" * 70) - print(f"✅ SDK Version: {version}") - print(f"✅ JSON Updated: {json_file}") + print(f"Languages Updated: {len(updated_languages)}") + for lang, version in updated_languages: + print(f" - {lang}: {version}") if branch_name: - print(f"✅ Branch Created: {branch_name}") + print(f"Branch Created: {branch_name}") if pr_url: - print(f"✅ Pull Request: {pr_url}") + print(f"Pull Request: {pr_url}") elif branch_name and not pr_url: - print("⚠️ Pull Request: Not created (check errors above)") + print("Pull Request: Not created (check errors above)") else: - print("ℹ️ Pull Request: Skipped (--enable-pr not specified)") + print("Pull Request: Skipped (--enable-pr not specified)") print("=" * 70) - def run(self, create_pr: bool = True, use_isolated_clone: bool = True, auto_cleanup: bool = False): + def run(self, create_pr: bool = True, use_isolated_clone: bool = True): """Main execution flow for all enabled languages""" print("=" * 70) - print("🚀 CyberSource SDK Version Updater (Multi-Language)") + print("CyberSource SDK Version Updater (Multi-Language)") print("=" * 70) - print(f"Mode: {'Add to list' if self.add_to_list else 'Update latest only'}") + print(f"Mode: {'Add to list' if self.add_to_versions_list else 'Update latest only'}") print(f"Clone: {'Isolated workspace' if use_isolated_clone else 'Current directory'}") + print(f"Target Repo: {self.sdk_support_repo}") print() # Validate configuration @@ -403,65 +418,124 @@ def run(self, create_pr: bool = True, use_isolated_clone: bool = True, auto_clea enabled_languages = [(name, config) for name, config in self.languages.items() if config.get("enabled", False)] - print(f"Processing {len(enabled_languages)} language(s)") + print(f"Checking {len(enabled_languages)} language(s) for updates...") print() - # Track results for final summary - results = { - "processed": [], - "skipped": [], - "failed": [] - } + # PHASE 1: Check which languages have updates available + print("=" * 70) + print("PHASE 1: Fetching latest releases from GitHub") + print("=" * 70) + print() + + languages_to_update = [] - # Process each enabled language for lang_name, lang_config in enabled_languages: - print("=" * 70) - print(f"🔹 Processing: {lang_name.upper()}") - print("=" * 70) - try: + print(f"Fetching {lang_name}...", end=" ") + # Fetch latest release - print(f"📡 Fetching latest release for {lang_name}...") release = self.get_latest_release(lang_config) if not release: - print(f"❌ Failed to fetch release data for {lang_name}") - results["failed"].append(f"{lang_name}: Failed to fetch release") - print() + print("FAILED") continue # Parse release data new_release = self.parse_release_data(release, lang_config) - print(f"✓ Latest GitHub release: {new_release['version']}") + github_version = new_release["version"] + + print(f"GitHub version: {github_version}") + languages_to_update.append((lang_name, lang_config, new_release)) + + except Exception as e: + print(f"ERROR: {e}") + + print() + print("=" * 70) + print(f"Fetched {len(languages_to_update)} release(s) from GitHub") + print("=" * 70) + + # If no releases fetched, exit early + if not languages_to_update: + print() + print("Failed to fetch any releases from GitHub.") + return + + # PHASE 2: Process languages that have updates + print() + print("=" * 70) + print(f"PHASE 2: Processing {len(languages_to_update)} language(s) with updates") + print("=" * 70) + print() + + # Track results for final summary + results = { + "processed": [], + "skipped": [], + "failed": [] + } + + # Track updated languages for single PR + updated_languages = [] + updated_files = [] + + # Setup workspace and clone central repo once if creating PR + workspace = None + repo_dir = None + branch_name = None + + try: + if create_pr and use_isolated_clone: + # Create isolated workspace and clone central repository once + print("Setting up central workspace...") + workspace = self.create_isolated_workspace() + repo_dir = self.clone_repository(workspace) print() - repo_dir = None - branch_name = None - pr_url = None - workspace = None - json_file = lang_config["json_file"] + # Create git branch once for all updates + print("Creating git branch for updates...") + branch_name = self.create_git_branch(repo_dir) + if not branch_name: + print("Failed to create git branch. Aborting.") + if workspace: + self.cleanup_workspace(workspace) + sys.exit(1) + print() + elif create_pr and not use_isolated_clone: + # Use current directory + repo_dir = os.getcwd() + print("Creating git branch for updates...") + branch_name = self.create_git_branch(repo_dir) + if not branch_name: + print("Failed to create git branch. Aborting.") + sys.exit(1) + print() + + # Process each enabled language + for lang_name, lang_config, new_release in languages_to_update: + print("=" * 70) + print(f"Processing: {lang_name.upper()}") + print("=" * 70) try: - if use_isolated_clone: - # Create isolated workspace and clone repository - print("🔧 Setting up isolated workspace...") - workspace = self.create_isolated_workspace(f"{lang_name}-{new_release['version']}") - repo_dir = self.clone_repository(workspace, lang_config) + # We already have the release data from Phase 1 + print(f"Processing update: {new_release['version']}") + print() + + json_file = lang_config["json_file"] + + # Determine JSON path + if repo_dir: json_path = os.path.join(repo_dir, json_file) - print() else: - # Use current directory - repo_dir = os.getcwd() json_path = json_file # Load current JSON from the repository - print(f"📄 Loading {json_file}...") + print(f"Loading {json_file}...") current_data = self.load_json_file(json_path, lang_name, lang_config) if current_data is None: # File not found, skip this language results["skipped"].append(f"{lang_name}: JSON file not found") - if workspace: - self.cleanup_workspace(workspace, auto_cleanup=True) print() continue @@ -470,86 +544,105 @@ def run(self, create_pr: bool = True, use_isolated_clone: bool = True, auto_clea print() if not has_changes: - print(f"ℹ️ No updates needed for {lang_name}.") + print(f"No updates needed for {lang_name}.") results["skipped"].append(f"{lang_name}: Already up-to-date ({new_release['version']})") - if workspace: - self.cleanup_workspace(workspace, auto_cleanup=True) print() continue - # Git operations (if enabled) - if create_pr: - print("🌿 Creating git branch and preparing PR...") - branch_name = self.create_git_branch(repo_dir, new_release["version"], lang_name, lang_config) - - if not branch_name: - print() - print(f"⚠️ Warning: Could not create git branch for {lang_name}. Skipping.") - results["failed"].append(f"{lang_name}: Failed to create branch") - if workspace: - self.cleanup_workspace(workspace, auto_cleanup=True) - print() - continue - # Save updated JSON - print("💾 Saving changes...") + print("Saving changes...") self.save_json_file(updated_data, json_path) print() - # Commit and push if branch was created successfully - if create_pr and branch_name: - print("📤 Committing and pushing changes...") - self.commit_and_push(repo_dir, new_release["version"], branch_name, lang_name, json_file) - print() - - print("🔀 Creating pull request...") - pr_url = self.create_pull_request(new_release["version"], branch_name, lang_name, lang_config) - print() - - # Display summary for this language - self.display_summary(lang_name, new_release["version"], branch_name, pr_url, json_file) - + # Track successful updates + updated_languages.append((lang_name, new_release["version"])) + updated_files.append(json_file) results["processed"].append(f"{lang_name}: {new_release['version']}") + print(f"{lang_name} updated successfully") + print() + except Exception as e: - print(f"❌ Error processing {lang_name}: {e}") + print(f"Error processing {lang_name}: {e}") results["failed"].append(f"{lang_name}: {str(e)}") - - finally: - # Cleanup workspace if created - if workspace: - self.cleanup_workspace(workspace, auto_cleanup=auto_cleanup) - + print() + + # Create single PR with all updates if any languages were updated + pr_url = None + if create_pr and branch_name and updated_languages: print() + print("=" * 70) + print("Committing and pushing all changes...") + print("=" * 70) + + try: + self.commit_and_push(repo_dir, branch_name, updated_files) + print() + + print("Creating pull request...") + pr_url = self.create_pull_request(branch_name, updated_languages) + print() + except Exception as e: + print(f"Error creating PR: {e}") + results["failed"].append(f"PR Creation: {str(e)}") - except Exception as e: - print(f"❌ Unexpected error for {lang_name}: {e}") - results["failed"].append(f"{lang_name}: {str(e)}") + # Display summary + if updated_languages: + self.display_summary(updated_languages, branch_name, pr_url) + + finally: + # Cleanup workspace if created + if workspace: + print() + self.cleanup_workspace(workspace) print() # Display final summary for all languages print() print("=" * 70) - print("📊 FINAL SUMMARY - ALL LANGUAGES") + print("FINAL SUMMARY") print("=" * 70) if results["processed"]: - print(f"\n✅ Successfully Processed ({len(results['processed'])}):") + print(f"\nSuccessfully Processed ({len(results['processed'])}):") for item in results["processed"]: - print(f" • {item}") + print(f" - {item}") if results["skipped"]: - print(f"\n⏭️ Skipped ({len(results['skipped'])}):") + print(f"\nSkipped ({len(results['skipped'])}):") for item in results["skipped"]: - print(f" • {item}") + print(f" - {item}") if results["failed"]: - print(f"\n❌ Failed ({len(results['failed'])}):") + print(f"\nFailed ({len(results['failed'])}):") for item in results["failed"]: - print(f" • {item}") + print(f" - {item}") print() print("=" * 70) + + # Final action summary + if updated_languages and pr_url: + print() + print("=" * 70) + print(f"{len(updated_languages)} language(s) updated successfully!") + print() + print(f"Pull Request: {pr_url}") + print() + print("Please review, approve and merge this PR.") + print("=" * 70) + elif updated_languages and branch_name and not pr_url: + print() + print("=" * 70) + print(f"{len(updated_languages)} language(s) updated successfully!") + print() + print(f"Branch: {branch_name}") + print() + print("PR creation failed. Please create PR manually.") + print("=" * 70) + elif not updated_languages and not results["failed"]: + print() + print("All SDKs are up to date. No updates needed.") def main(): @@ -559,19 +652,20 @@ def main(): epilog=""" Examples: - # Create PRs for all enabled languages with isolated clones - python update-sdk-versions.py --add-to-list --enable-pr --token YOUR_TOKEN + # Create single PR for all enabled languages with isolated clone + python update-sdk-versions-multi.py --add-to-versions-list --enable-pr --token YOUR_TOKEN + + # Update without creating PR + python update-sdk-versions-multi.py --add-to-versions-list """ ) - parser.add_argument("--add-to-list", action="store_true", + parser.add_argument("--add-to-versions-list", action="store_true", help="Add new version to versions array (default: only update latest)") parser.add_argument("--enable-pr", action="store_true", help="Enable creating git branch and pull request") parser.add_argument("--no-clone", action="store_true", help="Don't create isolated clone, use current directory") - parser.add_argument("--auto-cleanup", action="store_true", - help="Automatically cleanup workspace without prompting") parser.add_argument("--token", type=str, default=None, help="GitHub Personal Access Token for PR creation") parser.add_argument("--config", default="config.json", @@ -582,15 +676,14 @@ def main(): # Create updater instance with optional token updater = SDKVersionUpdater(config_path=args.config, github_token=args.token) - # Override add_to_list if specified via command line - if args.add_to_list: - updater.add_to_list = True + # Override add_to_versions_list if specified via command line + if args.add_to_versions_list: + updater.add_to_versions_list = True # Run the updater updater.run( create_pr=args.enable_pr, - use_isolated_clone=not args.no_clone, - auto_cleanup=args.auto_cleanup + use_isolated_clone=not args.no_clone )