backtrace/.github/scripts/merge_action_updates.py

67 lines
3.2 KiB
Python

"""Merge Dependabot Action patch/minor updates after all head checks pass."""
import json
import os
import re
import time
import urllib.request
repository = os.environ['GITHUB_REPOSITORY']
event = json.load(open(os.environ['GITHUB_EVENT_PATH']))
def call(path, method='GET', body=None):
req = urllib.request.Request('https://api.github.com/repos/' + repository + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={'Authorization': 'Bearer ' + os.environ['GH_TOKEN'], 'Accept': 'application/vnd.github+json', 'Content-Type': 'application/json'})
with urllib.request.urlopen(req, timeout=40) as response:
value = response.read()
return json.loads(value) if value else None
def versioned_changes(files):
count = 0
for file in files:
path = file['filename']
if not path.startswith('.github/') or not path.endswith(('.yml', '.yaml')) or 'patch' not in file:
return False
old, new = {}, {}
for line in file['patch'].splitlines():
if not line.startswith(('-', '+')) or line.startswith(('---', '+++')):
continue
match = re.match(r'[+-]\s*(?:-\s*)?uses:\s*([^\s@]+)@[0-9a-f]{40}\s+#\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?\s*$', line)
if not match:
# Updating any workflow behavior or an unversioned action
# remains a reviewed change.
return False
(old if line[0] == '-' else new)[match[1]] = tuple(int(x or 0) for x in match.groups()[1:])
if set(old) != set(new):
return False
for action in old:
if old[action][0] != new[action][0] or new[action] < old[action]:
return False
count += 1
return count > 0
run = event['workflow_run']
if run['conclusion'] == 'success' and run['event'] == 'pull_request':
for reference in run.get('pull_requests', []):
number = reference['number']
pr = call('/pulls/' + str(number))
if pr['state'] != 'open' or pr['user']['login'] != 'dependabot[bot]' or pr['head']['repo']['full_name'] != repository:
continue
if pr['head']['sha'] != run['head_sha'] or pr.get('draft'):
continue
files = call('/pulls/' + str(number) + '/files?per_page=100')
if pr['changed_files'] > 100 or not versioned_changes(files):
continue
sha = pr['head']['sha']
for attempt in range(90):
checks = call('/commits/' + sha + '/check-runs?per_page=100')['check_runs']
statuses = call('/commits/' + sha + '/status')['statuses']
if any(x['status'] == 'completed' and x['conclusion'] not in ('success', 'neutral', 'skipped') for x in checks) or any(x['state'] in ('failure', 'error') for x in statuses):
break
if checks and all(x['status'] == 'completed' for x in checks) and all(x['state'] == 'success' for x in statuses):
latest = call('/pulls/' + str(number))
if latest['head']['sha'] == sha and latest['state'] == 'open':
call('/pulls/' + str(number) + '/merge', 'PUT', {'sha': sha, 'merge_method': 'squash'})
break
time.sleep(20)