From dbaa9edbc3bfa4e950f35fb888cf60cb97488b9f Mon Sep 17 00:00:00 2001 From: Sergey Dolin Date: Tue, 24 Oct 2023 22:59:58 +0200 Subject: [PATCH] Remove toolchain directories from the cache --- .github/workflows/toolchain.yml | 40 ++++++++ .idea/.gitignore | 5 + .idea/modules.xml | 8 ++ .idea/setup-go.iml | 12 +++ .idea/vcs.xml | 6 ++ __tests__/cache-utils.test.ts | 177 ++++++++++++++++++++++++++++++++ __tests__/toolchain.go.mod | 13 +++ __tests__/utils.test.ts | 52 ++++++++++ dist/cache-save/index.js | 52 +++++++++- dist/setup/index.js | 67 +++++++++++- src/cache-restore.ts | 19 +++- src/cache-save.ts | 21 +++- src/cache-utils.ts | 48 +++++++++ src/constants.ts | 3 +- src/installer.ts | 8 +- src/utils.ts | 10 ++ 16 files changed, 527 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/toolchain.yml create mode 100644 .idea/.gitignore create mode 100644 .idea/modules.xml create mode 100644 .idea/setup-go.iml create mode 100644 .idea/vcs.xml create mode 100644 __tests__/toolchain.go.mod create mode 100644 __tests__/utils.test.ts 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..4b40f33b3 --- /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' + */