diff --git a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml index 0a6a76099f..f6b7497420 100644 --- a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml +++ b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml @@ -97,6 +97,17 @@ jobs: headRefName title createdAt + labels(last:10) { + nodes { + name + } + } + headRepository { + name + nameWithOwner + url + isFork + } commits(last: 1) { nodes { commit { diff --git a/release/ci/squash_and_merge_prs.py b/release/ci/squash_and_merge_prs.py index 5cd1b95746..6d8f70f349 100755 --- a/release/ci/squash_and_merge_prs.py +++ b/release/ci/squash_and_merge_prs.py @@ -7,6 +7,7 @@ import argparse import json from datetime import datetime +TRUST_FORK_LABEL = "trust-fork-pr" def setup_argument_parser(): parser = argparse.ArgumentParser(description='Process and squash GitHub PRs') @@ -37,43 +38,25 @@ def sort_prs_by_creation(pr_data): ) -def add_pr_comment(pr_number, comment): +def add_pr_comments(pr_number, comments: list[str]): + """Adds or updates a comment with multiple comments to a PR using gh cli""" + comment = "\n___\n".join(comments) + _add_pr_comment(pr_number, comment) + + +def _add_pr_comment(pr_number, comment): """Add or update a comment to a PR using gh cli""" title = "## Squash and Merge" try: - result = subprocess.run( - ['gh', 'pr', 'view', str(pr_number), '--json', 'comments'], + full_comment = f"{title}\n\n{comment}" + subprocess.run( + ['gh', 'pr', 'comment', '--edit-last', '--create-if-none', f"#{pr_number}", '--body', full_comment], check=True, capture_output=True, text=True ) - comments_data = json.loads(result.stdout) - has_existing_comment = False - - for pr_comment in comments_data['comments']: - if pr_comment['body'].startswith(title): - has_existing_comment = True - break - - full_comment = f"{title}\n\n{comment}" - - if has_existing_comment: - subprocess.run( - ['gh', 'pr', 'comment', '--edit-last', f"#{pr_number}", '--body', full_comment], - check=True, - capture_output=True, - text=True - ) - else: - subprocess.run( - ['gh', 'pr', 'comment', f"#{pr_number}", '--body', full_comment], - check=True, - capture_output=True, - text=True - ) - except subprocess.CalledProcessError as e: print(f"Failed to add/update comment on PR #{pr_number}: {e.stderr}") except json.JSONDecodeError: @@ -104,8 +87,8 @@ def validate_pr(pr): if not merge_data.get('mergeable'): return False, "merge conflicts detected" - if (mergeStateStatus := merge_data.get('mergeStateStatus')) == "BEHIND": - return False, f"branch is `{mergeStateStatus}`" + # if (mergeStateStatus := merge_data.get('mergeStateStatus')) == "BEHIND": + # return False, f"branch is `{mergeStateStatus}`" return True, None @@ -122,24 +105,43 @@ def process_pr(pr_data, source_branch, target_branch, squash_script_path): subprocess.run(['git', 'branch', target_branch, f'origin/{source_branch}'], check=True) success_count = 0 for pr in nodes: - pr_number = pr.get('number', 'UNKNOWN') - branch = pr.get('headRefName', '') - title = pr.get('title', '') - is_valid, skip_reason = validate_pr(pr) - - if not is_valid: - print(f"Warning: {skip_reason} for PR #{pr_number}, skipping") - add_pr_comment(pr_number, - f"⚠️ This PR was skipped in the automated `{target_branch}` squash because **{skip_reason}**.") - continue - + pr_comments = [] try: + pr_number = pr.get('number', 'UNKNOWN') + branch = pr.get('headRefName', '') + title = pr.get('title', '') + head_repository = pr.get('headRepository', {}) + pr_labels = pr.get('labels', {}).get('nodes', []) + is_fork = head_repository.get('isFork', False) + trust_fork = any(label.get('name') == TRUST_FORK_LABEL for label in pr_labels) + is_valid, skip_reason = validate_pr(pr) + origin = "origin" if not head_repository.get('isFork', False) else head_repository.get('nameWithOwner', 'origin') + + if is_fork and trust_fork: + print(f"Removing label `{TRUST_FORK_LABEL}` from PR #{pr_number} as it is being processed") + subprocess.run(['gh', 'pr', 'edit', str(pr_number), '--remove-label', TRUST_FORK_LABEL], check=True) + pr_comments.append(f"ℹ️️ This PR is from a fork. The `{TRUST_FORK_LABEL}` label was removed as it is being processed right now.") + print(f"Adding remote {origin} for PR #{pr_number}") + subprocess.run(['git', 'remote', 'add', origin, head_repository.get('url')], check=False) + + if is_fork and not trust_fork: + pr_comments.append( + f"⚠️ This PR is from a fork. Please add the `{TRUST_FORK_LABEL}` label to include it in the squash." + + "\n**Note**: The label is removed after the squash is done and must be added again for the next execution for security reasons." + ) + continue + + if not is_valid: + print(f"Warning: {skip_reason} for PR #{pr_number}, skipping") + pr_comments.append(f"⚠️ This PR was skipped in the automated `{target_branch}` squash because **{skip_reason}**.") + continue + # Fetch PR branch - subprocess.run(['git', 'fetch', 'origin', branch], check=True) + subprocess.run(['git', 'fetch', origin, branch], check=True) # Delete branch if it exists (ignore errors if it doesn't) subprocess.run(['git', 'branch', '-D', branch], check=False) # Create new branch pointing to origin's branch - subprocess.run(['git', 'branch', branch, f'origin/{branch}'], check=True) + subprocess.run(['git', 'branch', branch, f'{origin}/{branch}'], check=True) # Run squash script result = subprocess.run([ @@ -160,13 +162,17 @@ def process_pr(pr_data, source_branch, target_branch, squash_script_path): print(f"Command failed with exit code {result.returncode}") output = result.stdout print(f"Error output: {output}") - add_pr_comment(pr_number, f"⚠️ Error during automated `{target_branch}` squash:\n```\n{output}\n```") + pr_comments.append(f"⚠️ Error during automated `{target_branch}` squash:\n```\n{output}\n```") subprocess.run(['git', 'reset', '--hard'], check=True) continue except Exception as e: print(f"Unexpected error processing PR #{pr_number}: {str(e)}") + pr_comments.append(f"⚠️ Unexpected error during automated `{target_branch}` squash:\n```\n{str(e)}\n```") subprocess.run(['git', 'reset', '--hard'], check=True) continue + finally: + if pr_comments: + add_pr_comments(pr_number, pr_comments) # This "commits" all the comments generated on this run before leaving loop on continue. return success_count