1"""Cleans out the GitHub Actions cache by deleting obsolete caches. 2 3 Usage: 4 python cleanup-github-caches.py 5""" 6 7import collections 8import datetime 9import json 10import os 11import re 12import subprocess 13import sys 14 15 16def main(argv): 17 if len(argv) > 1: 18 raise ValueError('Expected no arguments: {}'.format(argv)) 19 20 # Group caches by their Git reference, e.g "refs/pull/3968/merge" 21 caches_by_ref = collections.defaultdict(list) 22 for cache in get_caches(): 23 caches_by_ref[cache['ref']].append(cache) 24 25 # Caclulate caches that should be deleted. 26 caches_to_delete = [] 27 for ref, caches in caches_by_ref.items(): 28 # If the pull request is already "closed", then delete all caches. 29 if (ref != 'refs/heads/master' and ref != 'master'): 30 match = re.findall(r'refs/pull/(\d+)/merge', ref) 31 if match: 32 pull_request_number = match[0] 33 pull_request = get_pull_request(pull_request_number) 34 if pull_request['state'] == 'closed': 35 caches_to_delete += caches 36 continue 37 else: 38 raise ValueError('Could not find pull request number:', ref) 39 40 # Check for caches with the same key prefix and delete the older caches. 41 caches_by_key = {} 42 for cache in caches: 43 key_prefix = re.findall('(.*)-.*', cache['key'])[0] 44 if key_prefix in caches_by_key: 45 prev_cache = caches_by_key[key_prefix] 46 if (get_created_at(cache) > get_created_at(prev_cache)): 47 caches_to_delete.append(prev_cache) 48 caches_by_key[key_prefix] = cache 49 else: 50 caches_to_delete.append(cache) 51 else: 52 caches_by_key[key_prefix] = cache 53 54 for cache in caches_to_delete: 55 print('Deleting cache ({}): {}'.format(cache['ref'], cache['key'])) 56 print(delete_cache(cache)) 57 58 59def get_created_at(cache): 60 created_at = cache['created_at'].split('.')[0] 61 # GitHub changed its date format so support both the old and new format for 62 # now. 63 for date_format in ('%Y-%m-%dT%H:%M:%SZ', '%Y-%m-%dT%H:%M:%S'): 64 try: 65 return datetime.datetime.strptime(created_at, date_format) 66 except ValueError: 67 pass 68 raise ValueError('no valid date format found: "%s"' % created_at) 69 70 71def delete_cache(cache): 72 # pylint: disable=line-too-long 73 """Deletes the given cache from GitHub Actions. 74 75 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 76 77 Args: 78 cache: The cache to delete. 79 80 Returns: 81 The response of the api call. 82 """ 83 return call_github_api( 84 """-X DELETE \ 85 https://api.github.com/repos/google/dagger/actions/caches/{0} 86 """.format(cache['id']) 87 ) 88 89 90def get_caches(): 91 # pylint: disable=line-too-long 92 """Gets the list of existing caches from GitHub Actions. 93 94 See https://docs.github.com/en/rest/actions/cache?apiVersion=2022-11-28#list-github-actions-caches-for-a-repository 95 96 Returns: 97 The list of existing caches. 98 """ 99 result = call_github_api( 100 'https://api.github.com/repos/google/dagger/actions/caches' 101 ) 102 return json.loads(result)['actions_caches'] 103 104 105def get_pull_request(pr_number): 106 # pylint: disable=line-too-long 107 """Gets the pull request with given number from GitHub Actions. 108 109 See https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#get-a-pull-request 110 111 Args: 112 pr_number: The pull request number used to get the pull request. 113 114 Returns: 115 The pull request. 116 """ 117 result = call_github_api( 118 'https://api.github.com/repos/google/dagger/pulls/{0}'.format(pr_number) 119 ) 120 return json.loads(result) 121 122 123def call_github_api(endpoint): 124 auth_cmd = '' 125 if 'GITHUB_TOKEN' in os.environ: 126 token = os.environ.get('GITHUB_TOKEN') 127 auth_cmd = '-H "Authorization: Bearer {0}"'.format(token) 128 cmd = """curl -L \ 129 {auth_cmd} \ 130 -H \"Accept: application/vnd.github+json\" \ 131 -H \"X-GitHub-Api-Version: 2022-11-28\" \ 132 {endpoint}""".format(auth_cmd=auth_cmd, endpoint=endpoint) 133 return subprocess.run( 134 [cmd], 135 check=True, 136 shell=True, 137 capture_output=True 138 ).stdout.decode('utf-8') 139 140 141if __name__ == '__main__': 142 main(sys.argv) 143