blob: 8e70e9538342419e93628d5a10265ca746cdc4f5 [file] [log] [blame]
#!/bin/python3
import requests
import re
import argparse
import logging
from typing import Dict, List
logging.basicConfig(level=logging.INFO)
class GithubRequestSender:
def __init__(self, token: str, repository: str):
self.headers = {
'Accept': 'application/vnd.github+json',
'Authorization': 'Bearer ' + token,
'X-GitHub-Api-Version': '2022-11-28'
}
self.repository = repository
def _send_get_request(self, url: str):
response = requests.get(url, headers=self.headers)
response.raise_for_status()
return response.json()
def _send_delete_request(self, url: str, params: Dict[str, str]):
response = requests.delete(url, headers=self.headers, params=params)
response.raise_for_status()
def list_open_pull_requests(self):
url = "https://api.github.com/repos/" + self.repository + "/pulls?state=open"
return self._send_get_request(url)
def list_caches(self):
url = "https://api.github.com/repos/" + self.repository + "/actions/caches"
return self._send_get_request(url)
def delete_cache(self, key: str):
url = "https://api.github.com/repos/" + self.repository + "/actions/caches"
self._send_delete_request(url, params={"key": key})
class CacheEntry:
def __init__(self, id: int, key: str):
self.id = id
self.key = key
def __str__(self) -> str:
return "{id: " + str(self.id) + ", key: " + self.key + "}"
def __repr__(self):
return str(self)
class GithubActionsCacheCleaner:
def __init__(self, token: str, repository: str):
self.github_request_sender = GithubRequestSender(token, repository)
def _list_open_pr_ids(self) -> List[str]:
open_tickets = []
for request in self.github_request_sender.list_open_pull_requests():
open_tickets.append(request['number'])
return open_tickets
def _get_cache_entries(self) -> List[CacheEntry]:
entries = []
json_result = self.github_request_sender.list_caches()
for json_entry in json_result["actions_caches"]:
entries.append(CacheEntry(int(json_entry["id"]), json_entry["key"]))
return entries
def _is_pr_already_closed(self, entry, open_tickets):
match = re.search(r'refs/pull/([\d]+)/merge', entry.key)
return match and int(match[1]) not in open_tickets
def _remove_non_latest_branch_caches(self, entry: CacheEntry, latest_branch_cache_map: Dict[str, CacheEntry], removable_entries: List[str]):
cache_mapping_key = "-".join(entry.key.split("-")[0:-1])
if cache_mapping_key not in latest_branch_cache_map:
latest_branch_cache_map[cache_mapping_key] = entry
else:
if latest_branch_cache_map[cache_mapping_key].id < entry.id:
removable_entries.append(latest_branch_cache_map[cache_mapping_key].key)
latest_branch_cache_map[cache_mapping_key] = entry
else:
removable_entries.append(entry.key)
def _get_removable_cache_entries(self) -> List[str]:
removable_entries = []
latest_branch_cache_map = dict()
open_prs = self._list_open_pr_ids()
for entry in self._get_cache_entries():
if self._is_pr_already_closed(entry, open_prs):
removable_entries.append(entry.key)
continue
self._remove_non_latest_branch_caches(entry, latest_branch_cache_map, removable_entries)
return removable_entries
def _remove_cache_entries(self, entries_to_remove: List[str]):
for key in entries_to_remove:
logging.info('Removing cache entry: %s', key)
self.github_request_sender.delete_cache(key)
def remove_obsolete_cache_entries(self):
self._remove_cache_entries(self._get_removable_cache_entries())
if __name__ == "__main__":
parser = argparse.ArgumentParser(prog='Github Actions Cache Cleaner', description='Cleans up obsolete cache entries of github actions')
parser.add_argument('-r', '--repository', help='The name of the repository in <owner>/<repository> format', required=True)
parser.add_argument('-t', '--token', help='Github token for API access', required=True)
args = parser.parse_args()
cache_cleaner = GithubActionsCacheCleaner(args.token, args.repository)
cache_cleaner.remove_obsolete_cache_entries()