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