Skip to content

Commit 2e2a290

Browse files
committed
build(ci): prune obsolete AppMap test binaries from GitHub Actions cache
Optimize cache usage in GitHub Actions by cleaning up older, obsolete versions of AppMap and Scanner test binaries before saving the cache. This prevents the cache from growing unboundedly due to every workflow run saving a cumulative historical set of binaries. - Add `.github/workflows/clean-cache.js` with zero-dependency SemVer parsing and cleanup logic. - Integrate the cleanup step in `.github/workflows/build.yml` after test execution, configuring it with `continue-on-error: true` to prevent any unexpected errors from failing the build.
1 parent 480de29 commit 2e2a290

2 files changed

Lines changed: 111 additions & 0 deletions

File tree

.github/workflows/build.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ jobs:
7373
build/reports/
7474
**/build/reports/
7575
76+
- name: Clean AppMap test binaries cache
77+
if: always()
78+
continue-on-error: true
79+
run: node .github/workflows/clean-cache.js
80+
7681
- name: Save AppMaps
7782
uses: actions/cache/save@v5
7883
if: runner.os == 'Linux' && matrix.platform_version == '251'

.github/workflows/clean-cache.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const os = require('os');
4+
5+
function parseSemVer(versionStr) {
6+
if (versionStr.startsWith('v')) {
7+
versionStr = versionStr.substring(1);
8+
}
9+
const [main, prerelease] = versionStr.split('-');
10+
const parts = main.split('.').map(Number);
11+
return {
12+
parts,
13+
prerelease: prerelease || null
14+
};
15+
}
16+
17+
function compareSemVer(v1, v2) {
18+
const s1 = parseSemVer(v1);
19+
const s2 = parseSemVer(v2);
20+
21+
for (let i = 0; i < 3; i++) {
22+
const p1 = s1.parts[i] || 0;
23+
const p2 = s2.parts[i] || 0;
24+
if (p1 !== p2) {
25+
return p1 - p2;
26+
}
27+
}
28+
29+
if (s1.prerelease && !s2.prerelease) return -1;
30+
if (!s1.prerelease && s2.prerelease) return 1;
31+
32+
if (s1.prerelease && s2.prerelease) {
33+
return s1.prerelease.localeCompare(s2.prerelease, undefined, { numeric: true, sensitivity: 'base' });
34+
}
35+
36+
return 0;
37+
}
38+
39+
const cacheDir = process.argv[2] || path.join(os.homedir(), '.cache', 'appmap-test');
40+
41+
console.log(`Target cache directory: ${cacheDir}`);
42+
43+
if (!fs.existsSync(cacheDir)) {
44+
console.log(`Cache directory does not exist. Nothing to clean.`);
45+
process.exit(0);
46+
}
47+
48+
try {
49+
const files = fs.readdirSync(cacheDir);
50+
// Matches names like appmap-linux-x64-1.2.3 and appmap-win-x64-1.2.3.exe
51+
// The lazy quantifier "+?" ensures we do not greedily swallow .exe if present
52+
const nameRegex = /^(appmap|scanner)-([a-z0-9]+)-([a-z0-9]+)-([0-9a-zA-Z_.-]+?)(?:\.exe)?$/i;
53+
54+
const groups = {};
55+
56+
for (const file of files) {
57+
const match = file.match(nameRegex);
58+
if (!match) {
59+
console.log(`Skipping non-matching file: ${file}`);
60+
continue;
61+
}
62+
63+
const tool = match[1].toLowerCase();
64+
const platform = match[2].toLowerCase();
65+
const arch = match[3].toLowerCase();
66+
const version = match[4];
67+
68+
const key = `${tool}-${platform}-${arch}`;
69+
if (!groups[key]) {
70+
groups[key] = [];
71+
}
72+
groups[key].push({
73+
filename: file,
74+
version: version
75+
});
76+
}
77+
78+
for (const key of Object.keys(groups)) {
79+
const groupFiles = groups[key];
80+
if (groupFiles.length <= 1) {
81+
console.log(`Group ${key} has ${groupFiles.length} file(s). No cleanup needed.`);
82+
continue;
83+
}
84+
85+
// Sort descending (highest version first)
86+
groupFiles.sort((a, b) => compareSemVer(b.version, a.version));
87+
88+
const keep = groupFiles[0];
89+
console.log(`Group ${key}: keeping ${keep.filename} (${keep.version})`);
90+
91+
for (let i = 1; i < groupFiles.length; i++) {
92+
const discard = groupFiles[i];
93+
const fullPath = path.join(cacheDir, discard.filename);
94+
console.log(` Deleting obsolete version: ${discard.filename} (${discard.version})`);
95+
try {
96+
fs.unlinkSync(fullPath);
97+
} catch (err) {
98+
console.error(` Failed to delete ${discard.filename}:`, err);
99+
}
100+
}
101+
}
102+
console.log('Cache cleanup complete.');
103+
} catch (err) {
104+
console.error('Error during cache cleanup:', err);
105+
process.exit(1);
106+
}

0 commit comments

Comments
 (0)