Skip to content

Commit 75d73b8

Browse files
committed
Remove toolchain directories from the cache
1 parent bfd2fb3 commit 75d73b8

13 files changed

+897
-305
lines changed

.github/workflows/toolchain.yml

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Validate 'setup-go'
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths-ignore:
8+
- '**.md'
9+
pull_request:
10+
paths-ignore:
11+
- '**.md'
12+
schedule:
13+
- cron: 0 0 * * *
14+
15+
jobs:
16+
local-cache:
17+
name: Setup local-cache version
18+
runs-on: ${{ matrix.os }}
19+
strategy:
20+
fail-fast: false
21+
matrix:
22+
os: [macos-latest, windows-latest, ubuntu-latest]
23+
go: [1.21]
24+
steps:
25+
- name: Checkout
26+
uses: actions/checkout@v4
27+
28+
- name: substitute go.mod with toolchain
29+
run: |
30+
cp __tests__/toolchain.go.mod go.mod
31+
shell: bash
32+
33+
- name: setup-go ${{ matrix.go }}
34+
uses: ./
35+
with:
36+
go-version: ${{ matrix.go }}
37+
38+
- name: verify go
39+
run: __tests__/verify-go.sh ${{ matrix.go }}
40+
shell: bash

__tests__/cache-utils.test.ts

+177
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import * as cache from '@actions/cache';
33
import * as core from '@actions/core';
44
import * as cacheUtils from '../src/cache-utils';
55
import {PackageManagerInfo} from '../src/package-managers';
6+
import fs, {ObjectEncodingOptions, PathLike} from 'fs';
7+
import {getToolchainDirectoriesFromCachedDirectories} from '../src/cache-utils';
68

79
describe('getCommandOutput', () => {
810
//Arrange
@@ -209,3 +211,178 @@ describe('isCacheFeatureAvailable', () => {
209211
expect(warningSpy).toHaveBeenCalledWith(warningMessage);
210212
});
211213
});
214+
215+
describe('parseGoModForToolchainVersion', () => {
216+
const readFileSyncSpy = jest.spyOn(fs, 'readFileSync');
217+
218+
afterEach(() => {
219+
jest.clearAllMocks();
220+
});
221+
222+
it('should return null when go.mod file not exist', async () => {
223+
//Arrange
224+
//Act
225+
const toolchainVersion = cacheUtils.parseGoModForToolchainVersion(
226+
'/tmp/non/exist/foo.bar'
227+
);
228+
//Assert
229+
expect(toolchainVersion).toBeNull();
230+
});
231+
232+
it('should return null when go.mod file is empty', async () => {
233+
//Arrange
234+
readFileSyncSpy.mockImplementation(() => '');
235+
//Act
236+
const toolchainVersion = cacheUtils.parseGoModForToolchainVersion('go.mod');
237+
//Assert
238+
expect(toolchainVersion).toBeNull();
239+
});
240+
241+
it('should return null when go.mod file does not contain toolchain version', async () => {
242+
//Arrange
243+
readFileSyncSpy.mockImplementation(() =>
244+
`
245+
module example-mod
246+
247+
go 1.21.0
248+
249+
require golang.org/x/tools v0.13.0
250+
251+
require (
252+
golang.org/x/mod v0.12.0 // indirect
253+
golang.org/x/sys v0.12.0 // indirect
254+
)
255+
`.replace(/^\s+/gm, '')
256+
);
257+
//Act
258+
const toolchainVersion = cacheUtils.parseGoModForToolchainVersion('go.mod');
259+
//Assert
260+
expect(toolchainVersion).toBeNull();
261+
});
262+
263+
it('should return go version when go.mod file contains go version', () => {
264+
//Arrange
265+
readFileSyncSpy.mockImplementation(() =>
266+
`
267+
module example-mod
268+
269+
go 1.21.0
270+
271+
toolchain go1.21.1
272+
273+
require golang.org/x/tools v0.13.0
274+
275+
require (
276+
golang.org/x/mod v0.12.0 // indirect
277+
golang.org/x/sys v0.12.0 // indirect
278+
)
279+
`.replace(/^\s+/gm, '')
280+
);
281+
282+
//Act
283+
const toolchainVersion = cacheUtils.parseGoModForToolchainVersion('go.mod');
284+
//Assert
285+
expect(toolchainVersion).toBe('1.21.1');
286+
});
287+
288+
it('should return go version when go.mod file contains more than one go version', () => {
289+
//Arrange
290+
readFileSyncSpy.mockImplementation(() =>
291+
`
292+
module example-mod
293+
294+
go 1.21.0
295+
296+
toolchain go1.21.0
297+
toolchain go1.21.1
298+
299+
require golang.org/x/tools v0.13.0
300+
301+
require (
302+
golang.org/x/mod v0.12.0 // indirect
303+
golang.org/x/sys v0.12.0 // indirect
304+
)
305+
`.replace(/^\s+/gm, '')
306+
);
307+
308+
//Act
309+
const toolchainVersion = cacheUtils.parseGoModForToolchainVersion('go.mod');
310+
//Assert
311+
expect(toolchainVersion).toBe('1.21.1');
312+
});
313+
});
314+
315+
describe('getToolchainDirectoriesFromCachedDirectories', () => {
316+
const readdirSyncSpy = jest.spyOn(fs, 'readdirSync');
317+
const existsSyncSpy = jest.spyOn(fs, 'existsSync');
318+
const lstatSync = jest.spyOn(fs, 'lstatSync');
319+
320+
afterEach(() => {
321+
jest.clearAllMocks();
322+
});
323+
324+
it('should return empty array when cacheDirectories is empty', async () => {
325+
const toolcacheDirectories = getToolchainDirectoriesFromCachedDirectories(
326+
'foo',
327+
[]
328+
);
329+
expect(toolcacheDirectories).toEqual([]);
330+
});
331+
332+
it('should return empty array when cacheDirectories does not contain /go/pkg', async () => {
333+
readdirSyncSpy.mockImplementation(dir =>
334+
[`${dir}1`, `${dir}2`, `${dir}3`].map(s => {
335+
const de = new fs.Dirent();
336+
de.name = s;
337+
de.isDirectory = () => true;
338+
return de;
339+
})
340+
);
341+
342+
const toolcacheDirectories = getToolchainDirectoriesFromCachedDirectories(
343+
'1.1.1',
344+
['foo', 'bar']
345+
);
346+
expect(toolcacheDirectories).toEqual([]);
347+
});
348+
349+
it('should return empty array when cacheDirectories does not contain toolchain@v[0-9.]+-go{goVersion}', async () => {
350+
readdirSyncSpy.mockImplementation(dir =>
351+
[`${dir}1`, `${dir}2`, `${dir}3`].map(s => {
352+
const de = new fs.Dirent();
353+
de.name = s;
354+
de.isDirectory = () => true;
355+
return de;
356+
})
357+
);
358+
359+
const toolcacheDirectories = getToolchainDirectoriesFromCachedDirectories(
360+
'foo',
361+
['foo/go/pkg/mod', 'bar']
362+
);
363+
expect(toolcacheDirectories).toEqual([]);
364+
});
365+
366+
it('should return one entry when cacheDirectories contains toolchain@v[0-9.]+-go{goVersion} in /pkg/mod', async () => {
367+
let seqNo = 1;
368+
readdirSyncSpy.mockImplementation(dir =>
369+
[`toolchain@v0.0.1-go1.1.1.arch-${seqNo++}`].map(s => {
370+
const de = new fs.Dirent();
371+
de.name = s;
372+
de.isDirectory = () => true;
373+
return de;
374+
})
375+
);
376+
existsSyncSpy.mockReturnValue(true);
377+
// @ts-ignore - jest does not have relaxed mocks, so we ignore not-implemented methods
378+
lstatSync.mockImplementation(() => ({isDirectory: () => true}));
379+
380+
const toolcacheDirectories = getToolchainDirectoriesFromCachedDirectories(
381+
'1.1.1',
382+
['/foo/go/pkg/mod', 'bar']
383+
);
384+
expect(toolcacheDirectories).toEqual([
385+
'/foo/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.1.1.arch-1'
386+
]);
387+
});
388+
});

__tests__/toolchain.go.mod

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module example-mod
2+
3+
go 1.21.0
4+
5+
toolchain go1.21.0
6+
toolchain go1.21.1
7+
8+
require golang.org/x/tools v0.13.0
9+
10+
require (
11+
golang.org/x/mod v0.12.0 // indirect
12+
golang.org/x/sys v0.12.0 // indirect
13+
)

__tests__/utils.test.ts

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {isSelfHosted} from '../src/utils';
2+
3+
describe('utils', () => {
4+
describe('isSelfHosted', () => {
5+
let AGENT_ISSELFHOSTED: string | undefined;
6+
let RUNNER_ENVIRONMENT: string | undefined;
7+
8+
beforeEach(() => {
9+
AGENT_ISSELFHOSTED = process.env['AGENT_ISSELFHOSTED'];
10+
delete process.env['AGENT_ISSELFHOSTED'];
11+
RUNNER_ENVIRONMENT = process.env['RUNNER_ENVIRONMENT'];
12+
delete process.env['RUNNER_ENVIRONMENT'];
13+
});
14+
15+
afterEach(() => {
16+
if (AGENT_ISSELFHOSTED === undefined) {
17+
delete process.env['AGENT_ISSELFHOSTED'];
18+
} else {
19+
process.env['AGENT_ISSELFHOSTED'] = AGENT_ISSELFHOSTED;
20+
}
21+
if (RUNNER_ENVIRONMENT === undefined) {
22+
delete process.env['RUNNER_ENVIRONMENT'];
23+
} else {
24+
process.env['RUNNER_ENVIRONMENT'] = RUNNER_ENVIRONMENT;
25+
}
26+
});
27+
28+
it('isSelfHosted should be true if no environment variables set', () => {
29+
expect(isSelfHosted()).toBeTruthy();
30+
});
31+
32+
it('isSelfHosted should be true if environment variable is not set to denote GitHub hosted', () => {
33+
process.env['RUNNER_ENVIRONMENT'] = 'some';
34+
expect(isSelfHosted()).toBeTruthy();
35+
});
36+
37+
it('isSelfHosted should be true if environment variable set to denote Azure Pipelines self hosted', () => {
38+
process.env['AGENT_ISSELFHOSTED'] = '1';
39+
expect(isSelfHosted()).toBeTruthy();
40+
});
41+
42+
it('isSelfHosted should be false if environment variable set to denote GitHub hosted', () => {
43+
process.env['RUNNER_ENVIRONMENT'] = 'github-hosted';
44+
expect(isSelfHosted()).toBeFalsy();
45+
});
46+
47+
it('isSelfHosted should be false if environment variable is not set to denote Azure Pipelines self hosted', () => {
48+
process.env['AGENT_ISSELFHOSTED'] = 'some';
49+
expect(isSelfHosted()).toBeFalsy();
50+
});
51+
});
52+
});

dist/cache-save/index.js

+51-1
Original file line numberDiff line numberDiff line change
@@ -58546,6 +58546,15 @@ const cachePackages = () => __awaiter(void 0, void 0, void 0, function* () {
5854658546
core.info(`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`);
5854758547
return;
5854858548
}
58549+
const toolchainVersion = core.getState(constants_1.State.ToolchainVersion);
58550+
// toolchainVersion is always null for self-hosted runners
58551+
if (toolchainVersion) {
58552+
const toolchainDirectories = cache_utils_1.getToolchainDirectoriesFromCachedDirectories(toolchainVersion, cachePaths);
58553+
toolchainDirectories.forEach(toolchainDirectory => {
58554+
core.warning(`Toolchain version ${toolchainVersion} will be removed from cache: ${toolchainDirectory}`);
58555+
fs_1.default.rmSync(toolchainDirectory, { recursive: true });
58556+
});
58557+
}
5854958558
const cacheId = yield cache.saveCache(cachePaths, primaryKey);
5855058559
if (cacheId === -1) {
5855158560
return;
@@ -58594,12 +58603,16 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
5859458603
step((generator = generator.apply(thisArg, _arguments || [])).next());
5859558604
});
5859658605
};
58606+
var __importDefault = (this && this.__importDefault) || function (mod) {
58607+
return (mod && mod.__esModule) ? mod : { "default": mod };
58608+
};
5859758609
Object.defineProperty(exports, "__esModule", ({ value: true }));
58598-
exports.isCacheFeatureAvailable = exports.isGhes = exports.getCacheDirectoryPath = exports.getPackageManagerInfo = exports.getCommandOutput = void 0;
58610+
exports.getToolchainDirectoriesFromCachedDirectories = exports.parseGoModForToolchainVersion = exports.isCacheFeatureAvailable = exports.isGhes = exports.getCacheDirectoryPath = exports.getPackageManagerInfo = exports.getCommandOutput = void 0;
5859958611
const cache = __importStar(__nccwpck_require__(7799));
5860058612
const core = __importStar(__nccwpck_require__(2186));
5860158613
const exec = __importStar(__nccwpck_require__(1514));
5860258614
const package_managers_1 = __nccwpck_require__(6663);
58615+
const fs_1 = __importDefault(__nccwpck_require__(7147));
5860358616
const getCommandOutput = (toolCommand) => __awaiter(void 0, void 0, void 0, function* () {
5860458617
let { stdout, stderr, exitCode } = yield exec.getExecOutput(toolCommand, undefined, { ignoreReturnCode: true });
5860558618
if (exitCode) {
@@ -58654,6 +58667,42 @@ function isCacheFeatureAvailable() {
5865458667
return false;
5865558668
}
5865658669
exports.isCacheFeatureAvailable = isCacheFeatureAvailable;
58670+
function parseGoModForToolchainVersion(goModPath) {
58671+
try {
58672+
const goMod = fs_1.default.readFileSync(goModPath, 'utf8');
58673+
const matches = Array.from(goMod.matchAll(/^toolchain\s+go(\S+)/gm));
58674+
if (matches && matches.length > 0) {
58675+
return matches[matches.length - 1][1];
58676+
}
58677+
}
58678+
catch (error) {
58679+
if (error.message && error.message.startsWith('ENOENT')) {
58680+
core.warning(`go.mod file not found at ${goModPath}, can't parse toolchain version`);
58681+
return null;
58682+
}
58683+
throw error;
58684+
}
58685+
return null;
58686+
}
58687+
exports.parseGoModForToolchainVersion = parseGoModForToolchainVersion;
58688+
function isDirent(item) {
58689+
return item instanceof fs_1.default.Dirent;
58690+
}
58691+
function getToolchainDirectoriesFromCachedDirectories(goVersion, cacheDirectories) {
58692+
const re = new RegExp(`^toolchain@v[0-9.]+-go${goVersion}\\.`);
58693+
return (cacheDirectories
58694+
// This line should be replaced with separating the cache directory from build artefact directory
58695+
// see PoC PR: https://github.com/actions/setup-go/pull/426
58696+
// Till then, the workaround is expected to work in most cases, and it won't cause any harm
58697+
.filter(dir => dir.endsWith('/pkg/mod'))
58698+
.map(dir => `${dir}/golang.org`)
58699+
.flatMap(dir => fs_1.default
58700+
.readdirSync(dir)
58701+
.map(subdir => (isDirent(subdir) ? subdir.name : dir))
58702+
.filter(subdir => re.test(subdir))
58703+
.map(subdir => `${dir}/${subdir}`)));
58704+
}
58705+
exports.getToolchainDirectoriesFromCachedDirectories = getToolchainDirectoriesFromCachedDirectories;
5865758706

5865858707

5865958708
/***/ }),
@@ -58669,6 +58718,7 @@ var State;
5866958718
(function (State) {
5867058719
State["CachePrimaryKey"] = "CACHE_KEY";
5867158720
State["CacheMatchedKey"] = "CACHE_RESULT";
58721+
State["ToolchainVersion"] = "TOOLCACHE_VERSION";
5867258722
})(State = exports.State || (exports.State = {}));
5867358723
var Outputs;
5867458724
(function (Outputs) {

0 commit comments

Comments
 (0)