|
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +Start integration test jobs for PRs by non-team members that are approved by team members. |
| 4 | +""" |
| 5 | + |
| 6 | +import argparse |
| 7 | +import json |
| 8 | +import subprocess |
| 9 | +import sys |
| 10 | +from pathlib import Path |
| 11 | +import re |
| 12 | + |
| 13 | + |
| 14 | +CLI_REPO = "databricks/cli" |
| 15 | +CODE_OWNERS = "pietern andrewnester shreyas-goenka denik anton-107".split() |
| 16 | +ALLOWED_HEAD_REPOSITORY = {"id": "R_kgDOHVGMwQ", "name": "cli"} |
| 17 | +ALLOWED_HEAD_OWNER = {"id": "MDEyOk9yZ2FuaXphdGlvbjQ5OTgwNTI=", "login": "databricks"} |
| 18 | + |
| 19 | + |
| 20 | +def run(cmd): |
| 21 | + sys.stderr.write("+ " + " ".join(cmd) + "\n") |
| 22 | + return subprocess.run(cmd, check=True) |
| 23 | + |
| 24 | + |
| 25 | +def run_json(cmd): |
| 26 | + sys.stderr.write("+ " + " ".join(cmd) + "\n") |
| 27 | + result = subprocess.run(cmd, stdout=subprocess.PIPE, encoding="utf-8", check=True) |
| 28 | + |
| 29 | + try: |
| 30 | + return json.loads(result.stdout) |
| 31 | + except Exception: |
| 32 | + sys.stderr.write(f"Failed to JSON parse:\n{result.stdout}\n") |
| 33 | + raise |
| 34 | + |
| 35 | + |
| 36 | +def get_approved_prs_by_non_team(): |
| 37 | + prs = run_json( |
| 38 | + ["gh", "pr", "-R", CLI_REPO, "list", "--limit", "300", "--json", "number,author,reviews,headRefOid,headRepository,headRepositoryOwner"] |
| 39 | + ) |
| 40 | + result = [] |
| 41 | + |
| 42 | + for pr in prs: |
| 43 | + pr_number = pr["number"] |
| 44 | + author = pr["author"]["login"] |
| 45 | + |
| 46 | + if author in CODE_OWNERS: |
| 47 | + continue |
| 48 | + |
| 49 | + head_repo = pr["headRepository"] |
| 50 | + if head_repo != ALLOWED_HEAD_REPOSITORY: |
| 51 | + print(f"#{pr_number} by {author} skipped due to headRepository: {head_repo}") |
| 52 | + continue |
| 53 | + |
| 54 | + head_owner = pr["headRepositoryOwner"] |
| 55 | + if head_owner != ALLOWED_HEAD_OWNER: |
| 56 | + print(f"#{pr_number} by {author} skipped due to headRepositoryOwner: {head_owner}") |
| 57 | + continue |
| 58 | + |
| 59 | + approved_by = [] |
| 60 | + for review in pr.get("reviews", []): |
| 61 | + approver = review["author"]["login"] |
| 62 | + if review["state"] == "APPROVED" and approver in CODE_OWNERS: |
| 63 | + approved_by.append(approver) |
| 64 | + |
| 65 | + if not approved_by: |
| 66 | + continue |
| 67 | + |
| 68 | + result.append( |
| 69 | + { |
| 70 | + "number": pr_number, |
| 71 | + "commit": pr["headRefOid"], |
| 72 | + "author": author, |
| 73 | + "approved_by": approved_by, |
| 74 | + } |
| 75 | + ) |
| 76 | + |
| 77 | + return prs, result |
| 78 | + |
| 79 | + |
| 80 | +def start_job(pr_number, commit_sha, author, approved_by, workflow, repo, force=False): |
| 81 | + pr_details = run_json(["gh", "pr", "-R", CLI_REPO, "view", str(pr_number), "--json", "title,url"]) |
| 82 | + pr_title = pr_details.get("title", "") |
| 83 | + commit_url = f"https://github.com/{CLI_REPO}/pull/{pr_number}/commits/{commit_sha}" |
| 84 | + approvers = ", ".join(approved_by) |
| 85 | + |
| 86 | + print(f"PR: #{pr_number} {pr_title}") |
| 87 | + print(f"Author: {author}") |
| 88 | + print(f"Approvers: {approvers}") |
| 89 | + print(f"Commit: {commit_url}") |
| 90 | + |
| 91 | + if force: |
| 92 | + response = "y" |
| 93 | + print("Starting integration tests.") |
| 94 | + else: |
| 95 | + response = input("Start integration tests? (y/n): ") |
| 96 | + |
| 97 | + if response.lower() == "y": |
| 98 | + result = run( |
| 99 | + [ |
| 100 | + "gh", |
| 101 | + "workflow", |
| 102 | + "run", |
| 103 | + workflow, |
| 104 | + "-R", |
| 105 | + repo, |
| 106 | + "-F", |
| 107 | + f"pull_request_number={pr_number}", |
| 108 | + "-F", |
| 109 | + f"commit_sha={commit_sha}", |
| 110 | + ], |
| 111 | + ) |
| 112 | + print(f"Started integration tests for PR #{pr_number}") |
| 113 | + |
| 114 | + |
| 115 | +def get_status(commit_sha): |
| 116 | + statuses = run_json(["gh", "api", f"repos/{CLI_REPO}/commits/{commit_sha}/statuses"]) |
| 117 | + result = [] |
| 118 | + for st in statuses: |
| 119 | + if st["context"] != "Integration Tests Check": |
| 120 | + continue |
| 121 | + result.append(f"{st['state']} {st['target_url']}") |
| 122 | + return result |
| 123 | + |
| 124 | + |
| 125 | +def main(): |
| 126 | + parser = argparse.ArgumentParser() |
| 127 | + parser.add_argument("--yes", action="store_true", default=False) |
| 128 | + parser.add_argument("--workflow", default="cli-isolated-pr") |
| 129 | + parser.add_argument("-R", "--repo") |
| 130 | + |
| 131 | + args = parser.parse_args() |
| 132 | + assert args.repo, "Must provide repo where workflow is run with -R" |
| 133 | + |
| 134 | + all_prs, approved_prs = get_approved_prs_by_non_team() |
| 135 | + |
| 136 | + if not approved_prs: |
| 137 | + print(f"Fetched {len(all_prs)} PRs. No approved PRs from non-team members found.") |
| 138 | + return |
| 139 | + |
| 140 | + print(f"Fetched {len(all_prs)} PRs.") |
| 141 | + |
| 142 | + for pr in approved_prs: |
| 143 | + pr_number = pr["number"] |
| 144 | + commit_sha = pr["commit"] |
| 145 | + |
| 146 | + status = get_status(commit_sha) |
| 147 | + |
| 148 | + if not status: |
| 149 | + start_job(pr_number, commit_sha, pr["author"], approved_by=pr["approved_by"], workflow=args.workflow, repo=args.repo, force=args.yes) |
| 150 | + else: |
| 151 | + commit_url = f"https://github.com/{CLI_REPO}/pull/{pr_number}/commits/{commit_sha}" |
| 152 | + print(f"Tests already running for PR #{pr_number} {commit_url}") |
| 153 | + print("\n".join(status)) |
| 154 | + print() |
| 155 | + |
| 156 | + |
| 157 | +if __name__ == "__main__": |
| 158 | + main() |
0 commit comments