Skip to content

Commit

Permalink
Remove toolchain directories from the cache
Browse files Browse the repository at this point in the history
  • Loading branch information
dsame committed Oct 25, 2023
1 parent bfd2fb3 commit 75d73b8
Show file tree
Hide file tree
Showing 13 changed files with 897 additions and 305 deletions.
40 changes: 40 additions & 0 deletions .github/workflows/toolchain.yml
@@ -0,0 +1,40 @@
name: Validate 'setup-go'

on:
push:
branches:
- main
paths-ignore:
- '**.md'
pull_request:
paths-ignore:
- '**.md'
schedule:
- cron: 0 0 * * *

jobs:
local-cache:
name: Setup local-cache version
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, windows-latest, ubuntu-latest]
go: [1.21]
steps:
- name: Checkout
uses: actions/checkout@v4

- name: substitute go.mod with toolchain
run: |
cp __tests__/toolchain.go.mod go.mod
shell: bash

- name: setup-go ${{ matrix.go }}
uses: ./
with:
go-version: ${{ matrix.go }}

- name: verify go
run: __tests__/verify-go.sh ${{ matrix.go }}
shell: bash
177 changes: 177 additions & 0 deletions __tests__/cache-utils.test.ts
Expand Up @@ -3,6 +3,8 @@ import * as cache from '@actions/cache';
import * as core from '@actions/core';
import * as cacheUtils from '../src/cache-utils';
import {PackageManagerInfo} from '../src/package-managers';
import fs, {ObjectEncodingOptions, PathLike} from 'fs';
import {getToolchainDirectoriesFromCachedDirectories} from '../src/cache-utils';

describe('getCommandOutput', () => {
//Arrange
Expand Down Expand Up @@ -209,3 +211,178 @@ describe('isCacheFeatureAvailable', () => {
expect(warningSpy).toHaveBeenCalledWith(warningMessage);
});
});

describe('parseGoModForToolchainVersion', () => {
const readFileSyncSpy = jest.spyOn(fs, 'readFileSync');

afterEach(() => {
jest.clearAllMocks();
});

it('should return null when go.mod file not exist', async () => {
//Arrange
//Act
const toolchainVersion = cacheUtils.parseGoModForToolchainVersion(
'/tmp/non/exist/foo.bar'
);
//Assert
expect(toolchainVersion).toBeNull();
});

it('should return null when go.mod file is empty', async () => {
//Arrange
readFileSyncSpy.mockImplementation(() => '');
//Act
const toolchainVersion = cacheUtils.parseGoModForToolchainVersion('go.mod');
//Assert
expect(toolchainVersion).toBeNull();
});

it('should return null when go.mod file does not contain toolchain version', async () => {
//Arrange
readFileSyncSpy.mockImplementation(() =>
`
module example-mod
go 1.21.0
require golang.org/x/tools v0.13.0
require (
golang.org/x/mod v0.12.0 // indirect
golang.org/x/sys v0.12.0 // indirect
)
`.replace(/^\s+/gm, '')
);
//Act
const toolchainVersion = cacheUtils.parseGoModForToolchainVersion('go.mod');
//Assert
expect(toolchainVersion).toBeNull();
});

it('should return go version when go.mod file contains go version', () => {
//Arrange
readFileSyncSpy.mockImplementation(() =>
`
module example-mod
go 1.21.0
toolchain go1.21.1
require golang.org/x/tools v0.13.0
require (
golang.org/x/mod v0.12.0 // indirect
golang.org/x/sys v0.12.0 // indirect
)
`.replace(/^\s+/gm, '')
);

//Act
const toolchainVersion = cacheUtils.parseGoModForToolchainVersion('go.mod');
//Assert
expect(toolchainVersion).toBe('1.21.1');
});

it('should return go version when go.mod file contains more than one go version', () => {
//Arrange
readFileSyncSpy.mockImplementation(() =>
`
module example-mod
go 1.21.0
toolchain go1.21.0
toolchain go1.21.1
require golang.org/x/tools v0.13.0
require (
golang.org/x/mod v0.12.0 // indirect
golang.org/x/sys v0.12.0 // indirect
)
`.replace(/^\s+/gm, '')
);

//Act
const toolchainVersion = cacheUtils.parseGoModForToolchainVersion('go.mod');
//Assert
expect(toolchainVersion).toBe('1.21.1');
});
});

describe('getToolchainDirectoriesFromCachedDirectories', () => {
const readdirSyncSpy = jest.spyOn(fs, 'readdirSync');
const existsSyncSpy = jest.spyOn(fs, 'existsSync');
const lstatSync = jest.spyOn(fs, 'lstatSync');

afterEach(() => {
jest.clearAllMocks();
});

it('should return empty array when cacheDirectories is empty', async () => {
const toolcacheDirectories = getToolchainDirectoriesFromCachedDirectories(
'foo',
[]
);
expect(toolcacheDirectories).toEqual([]);
});

it('should return empty array when cacheDirectories does not contain /go/pkg', async () => {
readdirSyncSpy.mockImplementation(dir =>
[`${dir}1`, `${dir}2`, `${dir}3`].map(s => {
const de = new fs.Dirent();
de.name = s;
de.isDirectory = () => true;
return de;
})
);

const toolcacheDirectories = getToolchainDirectoriesFromCachedDirectories(
'1.1.1',
['foo', 'bar']
);
expect(toolcacheDirectories).toEqual([]);
});

it('should return empty array when cacheDirectories does not contain toolchain@v[0-9.]+-go{goVersion}', async () => {
readdirSyncSpy.mockImplementation(dir =>
[`${dir}1`, `${dir}2`, `${dir}3`].map(s => {
const de = new fs.Dirent();
de.name = s;
de.isDirectory = () => true;
return de;
})
);

const toolcacheDirectories = getToolchainDirectoriesFromCachedDirectories(
'foo',
['foo/go/pkg/mod', 'bar']
);
expect(toolcacheDirectories).toEqual([]);
});

it('should return one entry when cacheDirectories contains toolchain@v[0-9.]+-go{goVersion} in /pkg/mod', async () => {
let seqNo = 1;
readdirSyncSpy.mockImplementation(dir =>
[`toolchain@v0.0.1-go1.1.1.arch-${seqNo++}`].map(s => {
const de = new fs.Dirent();
de.name = s;
de.isDirectory = () => true;
return de;
})
);
existsSyncSpy.mockReturnValue(true);
// @ts-ignore - jest does not have relaxed mocks, so we ignore not-implemented methods
lstatSync.mockImplementation(() => ({isDirectory: () => true}));

const toolcacheDirectories = getToolchainDirectoriesFromCachedDirectories(
'1.1.1',
['/foo/go/pkg/mod', 'bar']
);
expect(toolcacheDirectories).toEqual([
'/foo/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.1.1.arch-1'
]);
});
});
13 changes: 13 additions & 0 deletions __tests__/toolchain.go.mod
@@ -0,0 +1,13 @@
module example-mod

go 1.21.0

toolchain go1.21.0
toolchain go1.21.1

require golang.org/x/tools v0.13.0

require (
golang.org/x/mod v0.12.0 // indirect
golang.org/x/sys v0.12.0 // indirect
)
52 changes: 52 additions & 0 deletions __tests__/utils.test.ts
@@ -0,0 +1,52 @@
import {isSelfHosted} from '../src/utils';

describe('utils', () => {
describe('isSelfHosted', () => {
let AGENT_ISSELFHOSTED: string | undefined;
let RUNNER_ENVIRONMENT: string | undefined;

beforeEach(() => {
AGENT_ISSELFHOSTED = process.env['AGENT_ISSELFHOSTED'];
delete process.env['AGENT_ISSELFHOSTED'];
RUNNER_ENVIRONMENT = process.env['RUNNER_ENVIRONMENT'];
delete process.env['RUNNER_ENVIRONMENT'];
});

afterEach(() => {
if (AGENT_ISSELFHOSTED === undefined) {
delete process.env['AGENT_ISSELFHOSTED'];
} else {
process.env['AGENT_ISSELFHOSTED'] = AGENT_ISSELFHOSTED;
}
if (RUNNER_ENVIRONMENT === undefined) {
delete process.env['RUNNER_ENVIRONMENT'];
} else {
process.env['RUNNER_ENVIRONMENT'] = RUNNER_ENVIRONMENT;
}
});

it('isSelfHosted should be true if no environment variables set', () => {
expect(isSelfHosted()).toBeTruthy();
});

it('isSelfHosted should be true if environment variable is not set to denote GitHub hosted', () => {
process.env['RUNNER_ENVIRONMENT'] = 'some';
expect(isSelfHosted()).toBeTruthy();
});

it('isSelfHosted should be true if environment variable set to denote Azure Pipelines self hosted', () => {
process.env['AGENT_ISSELFHOSTED'] = '1';
expect(isSelfHosted()).toBeTruthy();
});

it('isSelfHosted should be false if environment variable set to denote GitHub hosted', () => {
process.env['RUNNER_ENVIRONMENT'] = 'github-hosted';
expect(isSelfHosted()).toBeFalsy();
});

it('isSelfHosted should be false if environment variable is not set to denote Azure Pipelines self hosted', () => {
process.env['AGENT_ISSELFHOSTED'] = 'some';
expect(isSelfHosted()).toBeFalsy();
});
});
});
52 changes: 51 additions & 1 deletion dist/cache-save/index.js
Expand Up @@ -58546,6 +58546,15 @@ const cachePackages = () => __awaiter(void 0, void 0, void 0, function* () {
core.info(`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`);
return;
}
const toolchainVersion = core.getState(constants_1.State.ToolchainVersion);
// toolchainVersion is always null for self-hosted runners
if (toolchainVersion) {
const toolchainDirectories = cache_utils_1.getToolchainDirectoriesFromCachedDirectories(toolchainVersion, cachePaths);
toolchainDirectories.forEach(toolchainDirectory => {
core.warning(`Toolchain version ${toolchainVersion} will be removed from cache: ${toolchainDirectory}`);
fs_1.default.rmSync(toolchainDirectory, { recursive: true });
});
}
const cacheId = yield cache.saveCache(cachePaths, primaryKey);
if (cacheId === -1) {
return;
Expand Down Expand Up @@ -58594,12 +58603,16 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.isCacheFeatureAvailable = exports.isGhes = exports.getCacheDirectoryPath = exports.getPackageManagerInfo = exports.getCommandOutput = void 0;
exports.getToolchainDirectoriesFromCachedDirectories = exports.parseGoModForToolchainVersion = exports.isCacheFeatureAvailable = exports.isGhes = exports.getCacheDirectoryPath = exports.getPackageManagerInfo = exports.getCommandOutput = void 0;
const cache = __importStar(__nccwpck_require__(7799));
const core = __importStar(__nccwpck_require__(2186));
const exec = __importStar(__nccwpck_require__(1514));
const package_managers_1 = __nccwpck_require__(6663);
const fs_1 = __importDefault(__nccwpck_require__(7147));
const getCommandOutput = (toolCommand) => __awaiter(void 0, void 0, void 0, function* () {
let { stdout, stderr, exitCode } = yield exec.getExecOutput(toolCommand, undefined, { ignoreReturnCode: true });
if (exitCode) {
Expand Down Expand Up @@ -58654,6 +58667,42 @@ function isCacheFeatureAvailable() {
return false;
}
exports.isCacheFeatureAvailable = isCacheFeatureAvailable;
function parseGoModForToolchainVersion(goModPath) {
try {
const goMod = fs_1.default.readFileSync(goModPath, 'utf8');
const matches = Array.from(goMod.matchAll(/^toolchain\s+go(\S+)/gm));
if (matches && matches.length > 0) {
return matches[matches.length - 1][1];
}
}
catch (error) {
if (error.message && error.message.startsWith('ENOENT')) {
core.warning(`go.mod file not found at ${goModPath}, can't parse toolchain version`);
return null;
}
throw error;
}
return null;
}
exports.parseGoModForToolchainVersion = parseGoModForToolchainVersion;
function isDirent(item) {
return item instanceof fs_1.default.Dirent;
}
function getToolchainDirectoriesFromCachedDirectories(goVersion, cacheDirectories) {
const re = new RegExp(`^toolchain@v[0-9.]+-go${goVersion}\\.`);
return (cacheDirectories
// This line should be replaced with separating the cache directory from build artefact directory
// see PoC PR: https://github.com/actions/setup-go/pull/426
// Till then, the workaround is expected to work in most cases, and it won't cause any harm
.filter(dir => dir.endsWith('/pkg/mod'))
.map(dir => `${dir}/golang.org`)
.flatMap(dir => fs_1.default
.readdirSync(dir)
.map(subdir => (isDirent(subdir) ? subdir.name : dir))
.filter(subdir => re.test(subdir))
.map(subdir => `${dir}/${subdir}`)));
}
exports.getToolchainDirectoriesFromCachedDirectories = getToolchainDirectoriesFromCachedDirectories;


/***/ }),
Expand All @@ -58669,6 +58718,7 @@ var State;
(function (State) {
State["CachePrimaryKey"] = "CACHE_KEY";
State["CacheMatchedKey"] = "CACHE_RESULT";
State["ToolchainVersion"] = "TOOLCACHE_VERSION";
})(State = exports.State || (exports.State = {}));
var Outputs;
(function (Outputs) {
Expand Down

0 comments on commit 75d73b8

Please sign in to comment.