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