blob: 41e4ede555915e3262bbc0799e2e3d3c2b58a6e7 [file] [log] [blame] [edit]
"""Cleans out the GitHub Actions cache by deleting obsolete caches.
Usage:
python cleanup-github-caches.py
"""
import collections
import datetime
import json
import os
import re
import subprocess
import sys
def main(argv):
if len(argv) > 1:
raise ValueError('Expected no arguments: {}'.format(argv))
# Group caches by their Git reference, e.g "refs/pull/3968/merge"
caches_by_ref = collections.defaultdict(list)
for cache in get_caches():
caches_by_ref[cache['ref']].append(cache)
# Caclulate caches that should be deleted.
caches_to_delete = []
for ref, caches in caches_by_ref.items():
# If the pull request is already "closed", then delete all caches.
if (ref != 'refs/heads/master' and ref != 'master'):
match = re.findall(r'refs/pull/(\d+)/merge', ref)
if match:
pull_request_number = match[0]
pull_request = get_pull_request(pull_request_number)
if pull_request['state'] == 'closed':
caches_to_delete += caches
continue
else:
raise ValueError('Could not find pull request number:', ref)
# Check for caches with the same key prefix and delete the older caches.
caches_by_key = {}
for cache in caches:
key_prefix = re.findall('(.*)-.*', cache['key'])[0]
if key_prefix in caches_by_key:
prev_cache = caches_by_key[key_prefix]
if (get_created_at(cache) > get_created_at(prev_cache)):
caches_to_delete.append(prev_cache)
caches_by_key[key_prefix] = cache
else:
caches_to_delete.append(cache)
else:
caches_by_key[key_prefix] = cache
for cache in caches_to_delete:
print('Deleting cache ({}): {}'.format(cache['ref'], cache['key']))
print(delete_cache(cache))
def get_created_at(cache):
created_at = cache['created_at'].split('.')[0]
# GitHub changed its date format so support both the old and new format for
# now.
for date_format in ('%Y-%m-%dT%H:%M:%SZ', '%Y-%m-%dT%H:%M:%S'):
try:
return datetime.datetime.strptime(created_at, date_format)
except ValueError:
pass
raise ValueError('no valid date format found: "%s"' % created_at)
def delete_cache(cache):
# pylint: disable=line-too-long
"""Deletes the given cache from GitHub Actions.
See https://docs.github.com/en/rest/actions/cache?apiVersion=2022-11-28#delete-a-github-actions-cache-for-a-repository-using-a-cache-id
Args:
cache: The cache to delete.
Returns:
The response of the api call.
"""
return call_github_api(
"""-X DELETE \
https://api.github.com/repos/google/dagger/actions/caches/{0}
""".format(cache['id'])
)
def get_caches():
# pylint: disable=line-too-long
"""Gets the list of existing caches from GitHub Actions.
See https://docs.github.com/en/rest/actions/cache?apiVersion=2022-11-28#list-github-actions-caches-for-a-repository
Returns:
The list of existing caches.
"""
result = call_github_api(
'https://api.github.com/repos/google/dagger/actions/caches'
)
return json.loads(result)['actions_caches']
def get_pull_request(pr_number):
# pylint: disable=line-too-long
"""Gets the pull request with given number from GitHub Actions.
See https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#get-a-pull-request
Args:
pr_number: The pull request number used to get the pull request.
Returns:
The pull request.
"""
result = call_github_api(
'https://api.github.com/repos/google/dagger/pulls/{0}'.format(pr_number)
)
return json.loads(result)
def call_github_api(endpoint):
auth_cmd = ''
if 'GITHUB_TOKEN' in os.environ:
token = os.environ.get('GITHUB_TOKEN')
auth_cmd = '-H "Authorization: Bearer {0}"'.format(token)
cmd = """curl -L \
{auth_cmd} \
-H \"Accept: application/vnd.github+json\" \
-H \"X-GitHub-Api-Version: 2022-11-28\" \
{endpoint}""".format(auth_cmd=auth_cmd, endpoint=endpoint)
return subprocess.run(
[cmd],
check=True,
shell=True,
capture_output=True
).stdout.decode('utf-8')
if __name__ == '__main__':
main(sys.argv)