Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove toolchain directories from the cache #438

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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