#!/usr/bin/env python3 """ Script to add backport table comment to PR after merge. Used by add_backport_table.yml workflow. """ import os import json import urllib.parse from github import Github, Auth as GithubAuth def normalize_app_domain(app_domain: str) -> str: """Normalize app domain - remove https:// prefix if present.""" domain = app_domain.strip() if domain.startswith("https://"): domain = domain[8:] if domain.startswith("http://"): domain = domain[7:] return domain.rstrip('/') def generate_backport_table(pr_number: int, app_domain: str) -> str: """Generate backport execution table with buttons for different branches.""" domain = normalize_app_domain(app_domain) base_url = f"https://{domain}/workflow/trigger" repo_env = os.environ.get("GITHUB_REPOSITORY") if not repo_env or "/" not in repo_env: raise ValueError("GITHUB_REPOSITORY environment variable is not set or malformed (expected 'owner/repo')") owner, repo = repo_env.split("/", 1) workflow_id = "cherry_pick_v2.yml" return_url = f"https://github.com/{owner}/{repo}/pull/{pr_number}" # Load backport branches from config workspace = os.environ.get("GITHUB_WORKSPACE") if not workspace: raise ValueError("GITHUB_WORKSPACE environment variable is not set") backport_branches_path = os.path.join(workspace, ".github", "config", "backport_branches.json") if not os.path.exists(backport_branches_path): raise FileNotFoundError(f"Backport branches config file not found: {backport_branches_path}") with open(backport_branches_path, 'r') as f: branches = json.load(f) if not isinstance(branches, list) or len(branches) == 0: raise ValueError(f"Invalid backport branches config: expected non-empty list, got {type(branches)}") print(f"::notice::Loaded {len(branches)} backport branch entries from {backport_branches_path}") # Collect all unique branches from all entries for manual button all_unique_branches = set() for branch_entry in branches: # Split by comma and strip whitespace branch_list = [b.strip() for b in branch_entry.split(',')] all_unique_branches.update(branch_list) # Sort for consistent output all_unique_branches_sorted = sorted(all_unique_branches) all_branches = ",".join(all_unique_branches_sorted) # Use each entry as is - each entry is a comma-separated list of branches for one backport rows = [] for branch_entry in branches: # Use the branch entry as is (may contain multiple branches separated by comma) branch_value = branch_entry.strip() # Format branches for display: split and join with ", " (comma with space) branch_list = [b.strip() for b in branch_value.split(',')] branch_display = ", ".join(branch_list) # Use PR number - workflow_dispatch input name is commits_and_prs params = { "owner": owner, "repo": repo, "workflow_id": workflow_id, "ref": "main", "commits_and_prs": str(pr_number), # workflow_dispatch input parameter name "target_branches": branch_value, # Use original value for URL parameter "allow_unmerged": "true", "return_url": return_url } query_string = "&".join([f"{k}={urllib.parse.quote(str(v), safe='')}" for k, v in params.items()]) url_ui = f"{base_url}?{query_string}&ui=true" # Badge with only message (no label) - format: badge/message-color # Encode only spaces, keep emoji as is badge_text = "▶ Backport".replace(" ", "%20") button = f"[]({url_ui})" rows.append(f"| `{branch_display}` | {button} |") # Generate URL for backporting all unique branches (manual button) params_manual = { "owner": owner, "repo": repo, "workflow_id": workflow_id, "ref": "main", "commits_and_prs": str(pr_number), "target_branches": all_branches, "allow_unmerged": "true", "return_url": return_url } query_string_manual = "&".join([f"{k}={urllib.parse.quote(str(v), safe='')}" for k, v in params_manual.items()]) url_manual_ui = f"{base_url}?{query_string_manual}&ui=true" # Badge with only message for manual button # Encode only spaces, keep emoji and parentheses as is (shields.io handles them) badge_text_manual = "▶ Backport manual".replace(" ", "%20") table = "\n" table += "