mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-07-12 13:42:07 +08:00
698e0ca00f
* migration: new branch names * more migration * update channel type * no more var * update * more * more
221 lines
7.7 KiB
Python
Executable File
221 lines
7.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import subprocess
|
|
import sys
|
|
import os
|
|
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')
|
|
parser.add_argument('--pr-data', type=str, help='PR data in JSON format')
|
|
parser.add_argument('--source-branch', type=str, default='master',
|
|
help='Source branch for merging')
|
|
parser.add_argument('--target-branch', type=str, default='master-dev-test',
|
|
help='Target branch for merging')
|
|
parser.add_argument('--squash-script-path', type=str, required=True,
|
|
help='Path to the squash_and_merge.py script')
|
|
return parser
|
|
|
|
|
|
def validate_squash_script(script_path):
|
|
if not os.path.isfile(script_path):
|
|
raise FileNotFoundError(f"Squash script not found at: {script_path}")
|
|
if not os.access(script_path, os.X_OK):
|
|
raise PermissionError(f"Squash script is not executable: {script_path}")
|
|
|
|
|
|
def sort_prs_by_creation(pr_data):
|
|
"""Sort PRs by creation date"""
|
|
nodes = (pr_data.get('data', {}).get('search', {}).get('nodes', []))
|
|
|
|
return sorted(
|
|
nodes,
|
|
key=lambda x: datetime.fromisoformat(x.get('createdAt', '').replace('Z', '+00:00'))
|
|
)
|
|
|
|
|
|
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:
|
|
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
|
|
)
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"Failed to add/update comment on PR #{pr_number}: {e.stderr}")
|
|
except json.JSONDecodeError:
|
|
print(f"Failed to parse comments data for PR #{pr_number}")
|
|
|
|
|
|
def validate_pr(pr):
|
|
"""Validate a PR and return (is_valid, skip_reason)"""
|
|
pr_number = pr.get('number', 'UNKNOWN')
|
|
branch = pr.get('headRefName', '')
|
|
|
|
if not branch:
|
|
return False, f"missing branch name for PR #{pr_number}"
|
|
|
|
# Check if checks have passed
|
|
commits = pr.get('commits', {}).get('nodes', [])
|
|
if not commits:
|
|
return False, "no commit data found"
|
|
|
|
# First check if we have the rollup status
|
|
status = commits[0].get('commit', {}).get('statusCheckRollup', {})
|
|
|
|
# If status is not SUCCESS, we need to check individual check runs
|
|
if not status or status.get('state') != 'SUCCESS':
|
|
# Get detailed check runs for this PR
|
|
checks_output = subprocess.run(
|
|
['gh', 'pr', 'checks', str(pr_number), '--json', 'name,state'],
|
|
capture_output=True, text=True
|
|
)
|
|
|
|
try:
|
|
checks_data = json.loads(checks_output.stdout)
|
|
|
|
# Check if all checks are successful except for our reset-and-squash check
|
|
for check in checks_data:
|
|
check_name = check.get('name', '')
|
|
check_state = check.get('state', '')
|
|
|
|
# Skip our own check and any skipped checks
|
|
if check_name == 'reset-and-squash' or check_state == 'SKIPPED':
|
|
continue
|
|
|
|
# If any other check is not successful, the PR is not valid
|
|
if check_state != 'SUCCESS':
|
|
return False, f"check '{check_name}' has state '{check_state}'"
|
|
|
|
except json.JSONDecodeError:
|
|
# If we can't parse the JSON, fall back to the original check
|
|
return False, "unable to verify check status"
|
|
|
|
# Check for merge conflicts
|
|
merge_status = subprocess.run(['gh', 'pr', 'view', str(pr_number), '--json', 'mergeable,mergeStateStatus'],
|
|
capture_output=True, text=True)
|
|
merge_data = json.loads(merge_status.stdout)
|
|
if not merge_data.get('mergeable'):
|
|
return False, "merge conflicts detected"
|
|
|
|
return True, None
|
|
|
|
|
|
def process_pr(pr_data, source_branch, target_branch, squash_script_path):
|
|
try:
|
|
nodes = sort_prs_by_creation(pr_data)
|
|
if not nodes:
|
|
print("No PRs to squash")
|
|
return 0
|
|
|
|
print(f"Deleting target branch {target_branch}")
|
|
subprocess.run(['git', 'branch', '-D', target_branch], check=False)
|
|
subprocess.run(['git', 'branch', target_branch, f'origin/{source_branch}'], check=True)
|
|
success_count = 0
|
|
for pr in nodes:
|
|
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"Adding remote {origin} for PR #{pr_number}")
|
|
subprocess.run(['git', 'remote', 'add', origin, head_repository.get('url')], check=False)
|
|
|
|
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)
|
|
# 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)
|
|
|
|
# Run squash script
|
|
result = subprocess.run([
|
|
squash_script_path,
|
|
'--target', target_branch,
|
|
'--base', source_branch,
|
|
'--title', f"{title} (PR-{pr_number})",
|
|
branch,
|
|
], capture_output=True, text=True)
|
|
|
|
print(result.stdout)
|
|
if result.returncode == 0:
|
|
print(f"Successfully processed PR #{pr_number}")
|
|
success_count += 1
|
|
continue
|
|
|
|
print(f"Error processing PR #{pr_number}:")
|
|
print(f"Command failed with exit code {result.returncode}")
|
|
output = result.stdout
|
|
print(f"Error output: {output}")
|
|
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
|
|
|
|
except Exception as e:
|
|
import traceback
|
|
print(f"Error in process_pr: {str(e)}")
|
|
print("Full traceback:")
|
|
print(traceback.format_exc())
|
|
return 0
|
|
|
|
|
|
def main():
|
|
parser = setup_argument_parser()
|
|
try:
|
|
args = parser.parse_args()
|
|
validate_squash_script(args.squash_script_path)
|
|
pr_data_json = json.loads(args.pr_data)
|
|
|
|
# Process the PRs
|
|
success_count = process_pr(pr_data_json, args.source_branch, args.target_branch, args.squash_script_path)
|
|
print(f"Successfully processed {success_count} PRs")
|
|
|
|
except Exception as e:
|
|
print(f"Fatal error: {str(e)}", file=sys.stderr)
|
|
return 1
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|