diff --git a/.github/workflows/toolchain.yml b/.github/workflows/toolchain.yml
new file mode 100644
index 000000000..0cf5aed76
--- /dev/null
+++ b/.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
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 000000000..b58b603fe
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,5 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 000000000..af1f3d969
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/setup-go.iml b/.idea/setup-go.iml
new file mode 100644
index 000000000..0c8867d7e
--- /dev/null
+++ b/.idea/setup-go.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 000000000..35eb1ddfb
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/__tests__/cache-utils.test.ts b/__tests__/cache-utils.test.ts
index 695c561cc..3d6512c9d 100644
--- a/__tests__/cache-utils.test.ts
+++ b/__tests__/cache-utils.test.ts
@@ -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
@@ -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'
+ ]);
+ });
+});
diff --git a/__tests__/toolchain.go.mod b/__tests__/toolchain.go.mod
new file mode 100644
index 000000000..87f30f62a
--- /dev/null
+++ b/__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
+)
diff --git a/__tests__/utils.test.ts b/__tests__/utils.test.ts
new file mode 100644
index 000000000..945d086a7
--- /dev/null
+++ b/__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();
+ });
+ });
+});
diff --git a/dist/cache-save/index.js b/dist/cache-save/index.js
index 19f39d251..9083fc045 100644
--- a/dist/cache-save/index.js
+++ b/dist/cache-save/index.js
@@ -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;
@@ -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) {
@@ -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;
/***/ }),
@@ -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) {
diff --git a/dist/setup/index.js b/dist/setup/index.js
index 25798ccb6..c1f49f329 100644
--- a/dist/setup/index.js
+++ b/dist/setup/index.js
@@ -61191,6 +61191,7 @@ const path_1 = __importDefault(__nccwpck_require__(1017));
const fs_1 = __importDefault(__nccwpck_require__(7147));
const constants_1 = __nccwpck_require__(9042);
const cache_utils_1 = __nccwpck_require__(1678);
+const utils_1 = __nccwpck_require__(1314);
const restoreCache = (versionSpec, packageManager, cacheDependencyPath) => __awaiter(void 0, void 0, void 0, function* () {
const packageManagerInfo = yield cache_utils_1.getPackageManagerInfo(packageManager);
const platform = process.env.RUNNER_OS;
@@ -61198,6 +61199,15 @@ const restoreCache = (versionSpec, packageManager, cacheDependencyPath) => __awa
const dependencyFilePath = cacheDependencyPath
? cacheDependencyPath
: findDependencyFile(packageManagerInfo);
+ // In order to do not duplicate evaluation of dependency paths, we get
+ // toolchain Version here and pass to the saveCache via the state
+ if (!utils_1.isSelfHosted()) {
+ const toolchainVersion = cacheDependencyPath && path_1.default.basename(cacheDependencyPath) === 'go.mod'
+ ? cache_utils_1.parseGoModForToolchainVersion(cacheDependencyPath)
+ : null;
+ toolchainVersion &&
+ core.saveState(constants_1.State.ToolchainVersion, toolchainVersion);
+ }
const fileHash = yield glob.hashFiles(dependencyFilePath);
if (!fileHash) {
throw new Error('Some specified paths were not resolved, unable to cache dependencies.');
@@ -61264,12 +61274,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) {
@@ -61324,6 +61338,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;
/***/ }),
@@ -61339,6 +61389,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) {
@@ -61495,8 +61546,7 @@ function cacheWindowsDir(extPath, tool, version, arch) {
if (os_1.default.platform() !== 'win32')
return false;
// make sure the action runs in the hosted environment
- if (process.env['RUNNER_ENVIRONMENT'] !== 'github-hosted' &&
- process.env['AGENT_ISSELFHOSTED'] === '1')
+ if (utils_1.isSelfHosted())
return false;
const defaultToolCacheRoot = process.env['RUNNER_TOOL_CACHE'];
if (!defaultToolCacheRoot)
@@ -61975,12 +62025,21 @@ exports.getArch = getArch;
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
-exports.StableReleaseAlias = void 0;
+exports.isSelfHosted = exports.StableReleaseAlias = void 0;
var StableReleaseAlias;
(function (StableReleaseAlias) {
StableReleaseAlias["Stable"] = "stable";
StableReleaseAlias["OldStable"] = "oldstable";
})(StableReleaseAlias = exports.StableReleaseAlias || (exports.StableReleaseAlias = {}));
+const isSelfHosted = () => process.env['AGENT_ISSELFHOSTED'] === '1' ||
+ (process.env['AGENT_ISSELFHOSTED'] === undefined &&
+ process.env['RUNNER_ENVIRONMENT'] !== 'github-hosted');
+exports.isSelfHosted = isSelfHosted;
+/* the above is simplified from:
+ process.env['RUNNER_ENVIRONMENT'] === undefined && process.env['AGENT_ISSELFHOSTED'] === '1'
+ ||
+ process.env['AGENT_ISSELFHOSTED'] === undefined && process.env['RUNNER_ENVIRONMENT'] !== 'github-hosted'
+ */
/***/ }),
diff --git a/src/cache-restore.ts b/src/cache-restore.ts
index 183df9ea5..8728c9504 100644
--- a/src/cache-restore.ts
+++ b/src/cache-restore.ts
@@ -6,7 +6,12 @@ import fs from 'fs';
import {State, Outputs} from './constants';
import {PackageManagerInfo} from './package-managers';
-import {getCacheDirectoryPath, getPackageManagerInfo} from './cache-utils';
+import {
+ getCacheDirectoryPath,
+ getPackageManagerInfo,
+ parseGoModForToolchainVersion
+} from './cache-utils';
+import {isSelfHosted} from './utils';
export const restoreCache = async (
versionSpec: string,
@@ -21,6 +26,18 @@ export const restoreCache = async (
const dependencyFilePath = cacheDependencyPath
? cacheDependencyPath
: findDependencyFile(packageManagerInfo);
+
+ // In order to do not duplicate evaluation of dependency paths, we get
+ // toolchain Version here and pass to the saveCache via the state
+ if (!isSelfHosted()) {
+ const toolchainVersion =
+ cacheDependencyPath && path.basename(cacheDependencyPath) === 'go.mod'
+ ? parseGoModForToolchainVersion(cacheDependencyPath)
+ : null;
+ toolchainVersion &&
+ core.saveState(State.ToolchainVersion, toolchainVersion);
+ }
+
const fileHash = await glob.hashFiles(dependencyFilePath);
if (!fileHash) {
diff --git a/src/cache-save.ts b/src/cache-save.ts
index 584d0a697..4d2e47001 100644
--- a/src/cache-save.ts
+++ b/src/cache-save.ts
@@ -2,7 +2,11 @@ import * as core from '@actions/core';
import * as cache from '@actions/cache';
import fs from 'fs';
import {State} from './constants';
-import {getCacheDirectoryPath, getPackageManagerInfo} from './cache-utils';
+import {
+ getCacheDirectoryPath,
+ getPackageManagerInfo,
+ getToolchainDirectoriesFromCachedDirectories
+} from './cache-utils';
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in
// @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to
@@ -73,6 +77,21 @@ const cachePackages = async () => {
return;
}
+ const toolchainVersion = core.getState(State.ToolchainVersion);
+ // toolchainVersion is always null for self-hosted runners
+ if (toolchainVersion) {
+ const toolchainDirectories = getToolchainDirectoriesFromCachedDirectories(
+ toolchainVersion,
+ cachePaths
+ );
+ toolchainDirectories.forEach(toolchainDirectory => {
+ core.warning(
+ `Toolchain version ${toolchainVersion} will be removed from cache: ${toolchainDirectory}`
+ );
+ fs.rmSync(toolchainDirectory, {recursive: true});
+ });
+ }
+
const cacheId = await cache.saveCache(cachePaths, primaryKey);
if (cacheId === -1) {
return;
diff --git a/src/cache-utils.ts b/src/cache-utils.ts
index 545c97af1..aff6a12ae 100644
--- a/src/cache-utils.ts
+++ b/src/cache-utils.ts
@@ -2,6 +2,7 @@ import * as cache from '@actions/cache';
import * as core from '@actions/core';
import * as exec from '@actions/exec';
import {supportedPackageManagers, PackageManagerInfo} from './package-managers';
+import fs from 'fs';
export const getCommandOutput = async (toolCommand: string) => {
let {stdout, stderr, exitCode} = await exec.getExecOutput(
@@ -83,3 +84,50 @@ export function isCacheFeatureAvailable(): boolean {
);
return false;
}
+
+export function parseGoModForToolchainVersion(
+ goModPath: string
+): string | null {
+ try {
+ const goMod = fs.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;
+}
+
+function isDirent(item: fs.Dirent | string): item is fs.Dirent {
+ return item instanceof fs.Dirent;
+}
+
+export function getToolchainDirectoriesFromCachedDirectories(
+ goVersion: string,
+ cacheDirectories: string[]
+): string[] {
+ 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
+ .readdirSync(dir)
+ .map(subdir => (isDirent(subdir) ? subdir.name : dir))
+ .filter(subdir => re.test(subdir))
+ .map(subdir => `${dir}/${subdir}`)
+ )
+ );
+}
diff --git a/src/constants.ts b/src/constants.ts
index b43d18c00..f5d8d1ab5 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -1,6 +1,7 @@
export enum State {
CachePrimaryKey = 'CACHE_KEY',
- CacheMatchedKey = 'CACHE_RESULT'
+ CacheMatchedKey = 'CACHE_RESULT',
+ ToolchainVersion = 'TOOLCACHE_VERSION'
}
export enum Outputs {
diff --git a/src/installer.ts b/src/installer.ts
index d8ac3a125..2a233859a 100644
--- a/src/installer.ts
+++ b/src/installer.ts
@@ -6,7 +6,7 @@ import * as httpm from '@actions/http-client';
import * as sys from './system';
import fs from 'fs';
import os from 'os';
-import {StableReleaseAlias} from './utils';
+import {isSelfHosted, StableReleaseAlias} from './utils';
type InstallationType = 'dist' | 'manifest';
@@ -175,11 +175,7 @@ async function cacheWindowsDir(
if (os.platform() !== 'win32') return false;
// make sure the action runs in the hosted environment
- if (
- process.env['RUNNER_ENVIRONMENT'] !== 'github-hosted' &&
- process.env['AGENT_ISSELFHOSTED'] === '1'
- )
- return false;
+ if (isSelfHosted()) return false;
const defaultToolCacheRoot = process.env['RUNNER_TOOL_CACHE'];
if (!defaultToolCacheRoot) return false;
diff --git a/src/utils.ts b/src/utils.ts
index 79d03bcad..a5301be18 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -2,3 +2,13 @@ export enum StableReleaseAlias {
Stable = 'stable',
OldStable = 'oldstable'
}
+
+export const isSelfHosted = (): boolean =>
+ process.env['AGENT_ISSELFHOSTED'] === '1' ||
+ (process.env['AGENT_ISSELFHOSTED'] === undefined &&
+ process.env['RUNNER_ENVIRONMENT'] !== 'github-hosted');
+/* the above is simplified from:
+ process.env['RUNNER_ENVIRONMENT'] === undefined && process.env['AGENT_ISSELFHOSTED'] === '1'
+ ||
+ process.env['AGENT_ISSELFHOSTED'] === undefined && process.env['RUNNER_ENVIRONMENT'] !== 'github-hosted'
+ */