xref: /aosp_15_r20/external/dagger2/util/cleanup-github-caches.py (revision f585d8a307d0621d6060bd7e80091fdcbf94fe27)
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