Skip to content

Commit 9a23176

Browse files
committed
Implement pruning of actions caches
1 parent f7dd33b commit 9a23176

File tree

6 files changed

+247
-2
lines changed

6 files changed

+247
-2
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/.* export-ignore
2+
/bin/ export-ignore
23
/tests/ export-ignore
34
/tools/ export-ignore
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: Prune GitHub Actions Caches
2+
3+
on:
4+
pull_request:
5+
types:
6+
- closed
7+
8+
jobs:
9+
prune:
10+
runs-on: ubuntu-24.04
11+
12+
steps:
13+
- name: Run prune-cache script
14+
run: |
15+
bin/prune-cache $REPO --pr-branch $BRANCH
16+
env:
17+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
18+
REPO: ${{ github.repository }}
19+
BRANCH: ${{ github.event.pull_request.number }}

.php-cs-fixer.dist.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,15 @@
2929
$finder = Finder::create()
3030
->files()
3131
->in([
32+
__DIR__.'/.github',
33+
__DIR__.'/bin',
3234
__DIR__.'/src',
3335
__DIR__.'/tests',
3436
__DIR__.'/tools',
3537
])
3638
->append([
3739
__FILE__,
40+
'bin/prune-cache',
3841
])
3942
;
4043

bin/prune-cache

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
declare(strict_types=1);
5+
6+
/**
7+
* This file is part of the Nexus framework.
8+
*
9+
* (c) John Paul E. Balandan, CPA <[email protected]>
10+
*
11+
* For the full copyright and license information, please view
12+
* the LICENSE file that was distributed with this source code.
13+
*/
14+
15+
require __DIR__.'/prune-cache.php';

bin/prune-cache.php

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of the Nexus framework.
7+
*
8+
* (c) John Paul E. Balandan, CPA <[email protected]>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
// ============================================================================
15+
// This script flushes the GitHub Actions caches used by closed/merged PRs.
16+
// It works by querying the REST API endpoints for GitHub Actions cache.
17+
//
18+
// @see https://docs.github.com/en/rest/actions/cache?apiVersion=2022-11-28#about-the-cache-in-github-actions
19+
// ============================================================================
20+
21+
if ($argc < 2) {
22+
echo "\033[106;30m[USAGE]\033[0m \033[32mphp\033[0m \033[33m.github/prune-cache.php\033[0m <repo> [--pr-branch=BRANCH] [--schedule]\n";
23+
24+
exit(1);
25+
}
26+
27+
if ((bool) getenv('GITHUB_ACTIONS') && getenv('GH_TOKEN') === false) {
28+
echo "\033[97;41m[ERROR]\033[0m When running in GitHub Actions, please pass the \033[32mGH_TOKEN\033[0m environment variable.\n";
29+
30+
exit(1);
31+
}
32+
33+
$arguments = $argv;
34+
array_shift($arguments);
35+
36+
$repository = '';
37+
$branch = 0;
38+
$onSchedule = false;
39+
$parseOption = true;
40+
41+
foreach ($arguments as $index => $argument) {
42+
if ('--' === $argument) {
43+
$parseOption = false;
44+
45+
continue;
46+
}
47+
48+
if (str_starts_with($argument, '--') && $parseOption) {
49+
if (str_starts_with($argument, '--pr-branch=')) {
50+
$branch = (int) substr($argument, 13);
51+
52+
continue;
53+
}
54+
55+
if ('--pr-branch' === $argument) {
56+
$branch = (int) $arguments[$index + 1];
57+
58+
continue;
59+
}
60+
61+
if ('--schedule' === $argument) {
62+
$onSchedule = true;
63+
}
64+
65+
continue;
66+
}
67+
68+
if (0 === $index) {
69+
$repository = $argument;
70+
71+
continue;
72+
}
73+
}
74+
75+
$activeCacheUsageCommand = [
76+
'gh api',
77+
'-H "Accept: application/vnd.github+json"',
78+
'-H "X-GitHub-Api-Version: 2022-11-28"',
79+
sprintf('/repos/%s/actions/cache/usage', $repository),
80+
'2>/dev/null',
81+
];
82+
$cacheUsageOutput = (array) json_decode((string) shell_exec(implode(' ', $activeCacheUsageCommand)), true, flags: JSON_THROW_ON_ERROR);
83+
84+
if (
85+
isset($cacheUsageOutput['status'], $cacheUsageOutput['message'])
86+
&& is_string($cacheUsageOutput['status'])
87+
&& is_string($cacheUsageOutput['message'])
88+
&& '200' !== $cacheUsageOutput['status']
89+
) {
90+
echo sprintf(
91+
"\033[97;41m[ERROR]\033[0m %s (HTTP %d)\n",
92+
$cacheUsageOutput['message'],
93+
$cacheUsageOutput['status'],
94+
);
95+
96+
exit(1);
97+
}
98+
99+
assert(isset($cacheUsageOutput['active_caches_count'], $cacheUsageOutput['active_caches_size_in_bytes']));
100+
$activeCachesCount = (int) $cacheUsageOutput['active_caches_count'];
101+
$activeCachesSize = (float) $cacheUsageOutput['active_caches_size_in_bytes'];
102+
103+
echo sprintf(
104+
<<<EOF
105+
\033[32mRepository :\033[0m %s
106+
\033[32mActive caches:\033[0m %d (%s MB)
107+
108+
EOF,
109+
$repository,
110+
$activeCachesCount,
111+
number_format($activeCachesSize / 1_000_000, 2),
112+
);
113+
114+
if ($branch < 1 && ! $onSchedule) {
115+
exit(0);
116+
}
117+
118+
$cachesListCommand = static fn(int $page = 1): string => implode(' ', [
119+
'gh api',
120+
'-X GET',
121+
'-H "Accept: application/vnd.github+json"',
122+
'-H "X-GitHub-Api-Version: 2022-11-28"',
123+
'-F per_page=100',
124+
sprintf('-F page=%d', $page),
125+
$branch > 0 ? sprintf('-F ref=refs/pull/%d/merge', $branch) : '',
126+
sprintf('/repos/%s/actions/caches', $repository),
127+
'2>/dev/null',
128+
]);
129+
$cachesDeleteCommand = static fn(string $key, string $ref): string => implode(' ', [
130+
'gh api',
131+
'-X DELETE',
132+
'-H "Accept: application/vnd.github+json"',
133+
'-H "X-GitHub-Api-Version: 2022-11-28"',
134+
sprintf('-F ref=%s', $ref),
135+
sprintf('/repos/%s/actions/caches?key=%s', $repository, $key),
136+
'2>/dev/null',
137+
]);
138+
139+
/**
140+
* @var array{
141+
* total_count: int,
142+
* actions_caches: list<array{
143+
* id: int,
144+
* ref: string,
145+
* key: string,
146+
* version: string,
147+
* last_accessed_at: string,
148+
* created_at: string,
149+
* size_in_bytes: int,
150+
* }>
151+
* } $caches
152+
*/
153+
$caches = json_decode((string) shell_exec($cachesListCommand()), true, flags: JSON_THROW_ON_ERROR);
154+
$counter = 0;
155+
$roundTrips = $caches['total_count'] > 100 ? (int) ceil($caches['total_count'] / 100) : 1;
156+
157+
for ($page = 2; $page < $roundTrips; ++$page) {
158+
/**
159+
* @var array{
160+
* total_count: int,
161+
* actions_caches: list<array{
162+
* id: int,
163+
* ref: string,
164+
* key: string,
165+
* version: string,
166+
* last_accessed_at: string,
167+
* created_at: string,
168+
* size_in_bytes: int,
169+
* }>
170+
* } $output
171+
*/
172+
$output = json_decode((string) shell_exec($cachesListCommand($page)), true, flags: JSON_THROW_ON_ERROR);
173+
$caches['actions_caches'] = array_merge($caches['actions_caches'], $output['actions_caches']);
174+
}
175+
176+
foreach ($caches['actions_caches'] as $cache) {
177+
if (preg_match('#refs/pull/\d+/merge#', $cache['ref']) !== 1) {
178+
continue;
179+
}
180+
181+
$exitCode = 0;
182+
$result = [];
183+
$message = sprintf(
184+
"Deleting cache \033[33m%s\033[0m (\033[31m%s MB\033[0m) on ref \033[32m%s\033[0m...\n",
185+
substr($cache['key'], 0, 50),
186+
number_format($cache['size_in_bytes'] / 1_000_000, 2),
187+
$cache['ref'],
188+
);
189+
190+
echo $message;
191+
exec($cachesDeleteCommand($cache['key'], $cache['ref']), $result, $exitCode);
192+
193+
if (0 === $exitCode) {
194+
echo "\033[1A";
195+
echo sprintf("\033[%dC", mb_strlen($message) - 27);
196+
echo "\033[32mDone\033[0m";
197+
echo "\033[1B";
198+
echo "\033[0G";
199+
++$counter;
200+
}
201+
}
202+
203+
echo sprintf(
204+
"\nDeleted \033[32m%d caches\033[0m from %s.\n",
205+
$counter,
206+
$branch > 0 ? sprintf('PR #%d branch', $branch) : ($onSchedule ? 'merged PR branches' : 'all branches'),
207+
);

phpstan.dist.neon

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ parameters:
77
level: 9
88
tmpDir: build/phpstan
99
paths:
10+
- .github
11+
- bin
1012
- src
1113
- tests
1214
- tools
@@ -21,8 +23,6 @@ parameters:
2123
checkTooWideReturnTypesInProtectedAndPublicMethods: true
2224
checkUninitializedProperties: true
2325
checkBenevolentUnionTypes: true
24-
reportPossiblyNonexistentGeneralArrayOffset: true
25-
reportPossiblyNonexistentConstantArrayOffset: true
2626
reportAlwaysTrueInLastCondition: true
2727
reportAnyTypeWideningInVarTag: true
2828
checkMissingCallableSignature: true

0 commit comments

Comments
 (0)