| """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) |