| import os | |
| from dev.release.gh import GitHub | |
| from dev.release.git import Git | |
| from dev.release.release_issue import ( | |
| RELEASE_TITLE_RE, | |
| add_backports_to_body, | |
| add_rc_task_to_body, | |
| add_sync_changelog_task_to_body, | |
| load_release_tracking_template, | |
| parse_checklist_state, | |
| ) | |
| from dev.release.utils import determine_next_version | |
| class AddBackports: | |
| """Class to add PRs to the release tracking issue.""" | |
| def __init__(self, args, gh: GitHub, git: Git | None = None): | |
| self.args = args | |
| self.gh = gh | |
| self.git = git or Git(os.getcwd()) | |
| def run(self) -> int: | |
| """Executes the add-backports subcommand.""" | |
| args = self.args | |
| issue_num = args.issue | |
| if not issue_num: | |
| print( | |
| "No issue specified. Trying to auto-discover active release" | |
| " tracking issue..." | |
| ) | |
| try: | |
| open_issues = self.gh.get_open_tracking_issues() | |
| if len(open_issues) > 1: | |
| print( | |
| "::error::Multiple open release tracking issues found." | |
| " Cannot determine active one:" | |
| ) | |
| for issue in open_issues: | |
| print(f"- #{issue['number']}: {issue['title']}") | |
| return 1 | |
| elif len(open_issues) == 1: | |
| issue_num = open_issues[0]["number"] | |
| print( | |
| f"Auto-discovered active release tracking issue: #{issue_num}" | |
| ) | |
| else: | |
| print( | |
| "No open release tracking issue found. Creating a new" | |
| " patch release tracking issue..." | |
| ) | |
| patch_version = determine_next_version(git=self.git, is_patch=True) | |
| template_content = load_release_tracking_template( | |
| version=patch_version | |
| ) | |
| issue_num = self.gh.create_release_tracking_issue( | |
| patch_version, template_content | |
| ) | |
| print( | |
| f"::notice::Created patch release tracking issue #{issue_num} for" | |
| f" v{patch_version}" | |
| ) | |
| except Exception as e: | |
| print( | |
| f"::error::Error auto-discovering or creating tracking issue: {e}" | |
| ) | |
| return 1 | |
| resolved_prs = [] | |
| for pr_ref in args.prs: | |
| try: | |
| pr_num = self.gh.resolve_pr_number(pr_ref) | |
| resolved_prs.append(pr_num) | |
| except Exception as e: | |
| print(f"::error::Error resolving PR ref '{pr_ref}': {e}") | |
| return 1 | |
| print( | |
| f"Adding backports {resolved_prs} (resolved from {args.prs}) to tracking issue #{issue_num}..." | |
| ) | |
| try: | |
| body = self.gh.get_issue_body(issue_num) | |
| items_to_add = [{"ref": f"#{pr}"} for pr in resolved_prs] | |
| body = add_backports_to_body(body, items_to_add) | |
| for pr in resolved_prs: | |
| body = add_sync_changelog_task_to_body(body, pr) | |
| state = parse_checklist_state(body) | |
| rc_tags = state.get("rc_tags", {}) | |
| has_pending_rc = any( | |
| not task.checked and task.status != "done" for task in rc_tags.values() | |
| ) | |
| next_rc_num = max(rc_tags.keys()) + 1 if rc_tags else 0 | |
| issue_title = self.gh.get_issue_title(issue_num) | |
| version_match = RELEASE_TITLE_RE.search(issue_title) | |
| is_patch = False | |
| if version_match: | |
| version = version_match.group(1) | |
| is_patch = not version.endswith(".0") | |
| if not has_pending_rc and (rc_tags or not is_patch): | |
| print( | |
| f"No pending RC task found. Adding 'Tag" | |
| f" RC{next_rc_num}' to checklist..." | |
| ) | |
| body = add_rc_task_to_body(body, next_rc_num) | |
| except ValueError as e: | |
| print(f"::error::{e}") | |
| return 1 | |
| except Exception as e: | |
| print(f"::error::Failed to update tracking issue: {e}") | |
| return 1 | |
| try: | |
| self.gh.update_issue_body(issue_num, body) | |
| print( | |
| f"::notice::Successfully updated tracking issue #{issue_num} checklist." | |
| ) | |
| except Exception as e: | |
| print(f"::error::Failed to update tracking issue body: {e}") | |
| return 1 | |
| return 0 | |
| @classmethod | |
| def add_parser(cls, subparsers): | |
| """Adds parser for add-backports subcommand.""" | |
| parser = subparsers.add_parser( | |
| "add-backports", | |
| help="Add PRs to the release tracking issue backports checklist.", | |
| ) | |
| parser.add_argument( | |
| "prs", | |
| type=str, | |
| nargs="+", | |
| help="PR references (numbers, #numbers, or URLs) to add (positional, space-separated).", | |
| ) | |
| parser.add_argument( | |
| "--issue", | |
| type=int, | |
| help="The tracking issue number. If omitted, will try to auto-discover the active release tracking issue.", | |
| ) | |
| parser.set_defaults(command=cls.run_from_args) | |
| @classmethod | |
| def run_from_args(cls, args): | |
| """Instantiates and runs the command from parsed args.""" | |
| gh = GitHub() | |
| git = Git(os.getcwd()) | |
| return cls(args, gh, git).run() |