From 613315d2424f12e3e98c9573aeeb4cd5c5282765 Mon Sep 17 00:00:00 2001 From: Matthew Hughes Date: Sun, 3 Mar 2024 09:06:34 +0000 Subject: [PATCH 1/4] Configure environment to avoid toolchain installs Force `go` to always use the local toolchain (i.e. the one the one that shipped with the go command being run) via setting the `GOTOOLCHAIN` environment variable to `local`[1]: > When GOTOOLCHAIN is set to local, the go command always runs the bundled Go toolchain. This is how things are setup in the official Docker images (e.g.[2], see also the discussion around that change[3]). The motivation behind this is to: * Reduce duplicate work: if the `toolchain` version in `go.mod` was greated than the `go` version, the version from the `go` directive would be installed, then Go would detect the `toolchain` version and additionally install that * Avoid Unexpected behaviour: if you specify this action runs with some Go version (e.g. `1.21.0`) but your go.mod contains a `toolchain` or `go` directive for a newer version (e.g. `1.22.0`) then, without any other configuration/environment setup, any go commands will be run using go `1.22.0` This will be a **breaking change** for some workflows. Given a `go.mod` like: module proj go 1.22.0 Then running any `go` command, e.g. `go mod tidy`, in an environment where only go versions before `1.22.0` were installed would previously trigger a toolchain download of Go `1.22.0` and that version being used to execute the command. With this change the above would error out with something like: > go: go.mod requires go >= 1.22.0 (running go 1.21.7; GOTOOLCHAIN=local) [1] https://go.dev/doc/toolchain#select [2] https://github.com/docker-library/golang/blob/dae3405a325073e8ad7c8c378ebdf2540d8565c4/Dockerfile-linux.template#L163 [3] https://github.com/docker-library/golang/issues/472 --- __tests__/setup-go.test.ts | 22 +++++++++++++++++----- dist/setup/index.js | 16 ++++++++++++++++ src/main.ts | 18 ++++++++++++++++++ 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/__tests__/setup-go.test.ts b/__tests__/setup-go.test.ts index f94f9ee..1596d7b 100644 --- a/__tests__/setup-go.test.ts +++ b/__tests__/setup-go.test.ts @@ -285,7 +285,7 @@ describe('setup-go', () => { expect(logSpy).toHaveBeenCalledWith(`Setup go version spec 1.13.0`); }); - it('does not export any variables for Go versions >=1.9', async () => { + it('does not export GOROOT for Go versions >=1.9', async () => { inputs['go-version'] = '1.13.0'; inSpy.mockImplementation(name => inputs[name]); @@ -298,7 +298,7 @@ describe('setup-go', () => { }); await main.run(); - expect(vars).toStrictEqual({}); + expect(vars).not.toHaveProperty('GOROOT'); }); it('exports GOROOT for Go versions <1.9', async () => { @@ -314,9 +314,7 @@ describe('setup-go', () => { }); await main.run(); - expect(vars).toStrictEqual({ - GOROOT: toolPath - }); + expect(vars).toHaveProperty('GOROOT', toolPath); }); it('finds a version of go already in the cache', async () => { @@ -989,4 +987,18 @@ use . } ); }); + + it('exports GOTOOLCHAIN and sets it in current process env', async () => { + inputs['go-version'] = '1.21.0'; + inSpy.mockImplementation(name => inputs[name]); + + const vars: {[key: string]: string} = {}; + exportVarSpy.mockImplementation((name: string, val: string) => { + vars[name] = val; + }); + + await main.run(); + expect(vars).toStrictEqual({GOTOOLCHAIN: 'local'}); + expect(process.env).toHaveProperty('GOTOOLCHAIN', 'local'); + }); }); diff --git a/dist/setup/index.js b/dist/setup/index.js index 11df681..41bb000 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -94777,6 +94777,7 @@ const os_1 = __importDefault(__nccwpck_require__(2037)); function run() { return __awaiter(this, void 0, void 0, function* () { try { + setToolchain(); // // versionSpec is optional. If supplied, install / use from the tool cache // If not supplied then problem matchers will still be setup. Useful for self-hosted. @@ -94890,6 +94891,21 @@ function resolveVersionInput() { } return version; } +function setToolchain() { + // docs: https://go.dev/doc/toolchain + // "local indicates the bundled Go toolchain (the one that shipped with the go command being run)" + // this is so any 'go' command is run with the selected Go version + // and doesn't trigger a toolchain download and run commands with that + // see e.g. issue #424 + // and a similar discussion: https://github.com/docker-library/golang/issues/472 + const toolchain = 'local'; + const toolchainVar = 'GOTOOLCHAIN'; + // set the value in process env so any `go` commands run as child-process + // don't cause toolchain downloads + process.env[toolchainVar] = toolchain; + // and in the runner env so e.g. a user running `go mod tidy` won't cause it + core.exportVariable(toolchainVar, toolchain); +} /***/ }), diff --git a/src/main.ts b/src/main.ts index 690d277..a52951c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,6 +11,7 @@ import os from 'os'; export async function run() { try { + setToolchain(); // // versionSpec is optional. If supplied, install / use from the tool cache // If not supplied then problem matchers will still be setup. Useful for self-hosted. @@ -160,3 +161,20 @@ function resolveVersionInput(): string { return version; } + +function setToolchain() { + // docs: https://go.dev/doc/toolchain + // "local indicates the bundled Go toolchain (the one that shipped with the go command being run)" + // this is so any 'go' command is run with the selected Go version + // and doesn't trigger a toolchain download and run commands with that + // see e.g. issue #424 + // and a similar discussion: https://github.com/docker-library/golang/issues/472 + const toolchain = 'local'; + const toolchainVar = 'GOTOOLCHAIN'; + + // set the value in process env so any `go` commands run as child-process + // don't cause toolchain downloads + process.env[toolchainVar] = toolchain; + // and in the runner env so e.g. a user running `go mod tidy` won't cause it + core.exportVariable(toolchainVar, toolchain); +} From 7252af563bd48b7eec9f0318ce4e14603edf4877 Mon Sep 17 00:00:00 2001 From: Matthew Hughes Date: Sun, 3 Mar 2024 09:48:10 +0000 Subject: [PATCH 2/4] Prefer installing version from `toolchain` directive Prefer this over the version from the `go` directive. Per the docs[1] > The toolchain line declares a suggested toolchain to use with the module or workspace It seems reasonable to use this, since running this action in a directory containing a `go.mod` (or `go.work`) suggests the user is wishing to work _with the module or workspace_. Link: https://go.dev/doc/toolchain#config [1] Issue: https://github.com/actions/setup-go/issues/457 --- README.md | 10 ++++++-- __tests__/setup-go.test.ts | 50 ++++++++++++++++++++++++++++++++++++++ dist/setup/index.js | 10 ++++++-- src/installer.ts | 11 +++++++-- 4 files changed, 75 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b23585d..d4fa6fa 100644 --- a/README.md +++ b/README.md @@ -191,9 +191,15 @@ steps: ## Getting go version from the go.mod file -The `go-version-file` input accepts a path to a `go.mod` file or a `go.work` file that contains the version of Go to be used by a project. +The `go-version-file` input accepts a path to a `go.mod` file or a `go.work` +file that contains the version of Go to be used by a project. The version taken +from thils file will be: + + - The version from the `toolchain` directive, if there is one, otherwise + - The version from the `go` directive + +The version can specify a patch version or omit it altogether (e.g., `go 1.22.0` or `go 1.22`). -The `go` directive in `go.mod` can specify a patch version or omit it altogether (e.g., `go 1.22.0` or `go 1.22`). If a patch version is specified, that specific patch version will be used. If no patch version is specified, it will search for the latest available patch version in the cache, [versions-manifest.json](https://github.com/actions/go-versions/blob/main/versions-manifest.json), and the diff --git a/__tests__/setup-go.test.ts b/__tests__/setup-go.test.ts index 1596d7b..e167ce6 100644 --- a/__tests__/setup-go.test.ts +++ b/__tests__/setup-go.test.ts @@ -988,6 +988,56 @@ use . ); }); + describe('go-version-file-toolchain', () => { + const goModContents = `module example.com/mymodule + +go 1.14 + +toolchain go1.21.0 + +require ( + example.com/othermodule v1.2.3 + example.com/thismodule v1.2.3 + example.com/thatmodule v1.2.3 +) + +replace example.com/thatmodule => ../thatmodule +exclude example.com/thismodule v1.3.0 +`; + + const goWorkContents = `go 1.19 + +toolchain go1.21.0 + +use . + +`; + + it('reads version from toolchain directive in go.mod', async () => { + inputs['go-version-file'] = 'go.mod'; + existsSpy.mockImplementation(() => true); + readFileSpy.mockImplementation(() => Buffer.from(goModContents)); + + await main.run(); + + expect(logSpy).toHaveBeenCalledWith('Setup go version spec 1.21.0'); + expect(logSpy).toHaveBeenCalledWith('Attempting to download 1.21.0...'); + expect(logSpy).toHaveBeenCalledWith('matching 1.21.0...'); + }); + + it('reads version from toolchain directive in go.work', async () => { + inputs['go-version-file'] = 'go.work'; + existsSpy.mockImplementation(() => true); + readFileSpy.mockImplementation(() => Buffer.from(goWorkContents)); + + await main.run(); + + expect(logSpy).toHaveBeenCalledWith('Setup go version spec 1.21.0'); + expect(logSpy).toHaveBeenCalledWith('Attempting to download 1.21.0...'); + expect(logSpy).toHaveBeenCalledWith('matching 1.21.0...'); + }); + }); + it('exports GOTOOLCHAIN and sets it in current process env', async () => { inputs['go-version'] = '1.21.0'; inSpy.mockImplementation(name => inputs[name]); diff --git a/dist/setup/index.js b/dist/setup/index.js index 41bb000..da47f5f 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -94663,8 +94663,14 @@ function parseGoVersionFile(versionFilePath) { const contents = fs_1.default.readFileSync(versionFilePath).toString(); if (path.basename(versionFilePath) === 'go.mod' || path.basename(versionFilePath) === 'go.work') { - const match = contents.match(/^go (\d+(\.\d+)*)/m); - return match ? match[1] : ''; + // toolchain directive: https://go.dev/ref/mod#go-mod-file-toolchain + const matchToolchain = contents.match(/^toolchain go(\d+(\.\d+)*)/m); + if (matchToolchain) { + return matchToolchain[1]; + } + // go directive: https://go.dev/ref/mod#go-mod-file-go + const matchGo = contents.match(/^go (\d+(\.\d+)*)/m); + return matchGo ? matchGo[1] : ''; } return contents.trim(); } diff --git a/src/installer.ts b/src/installer.ts index 1b5f20f..4b8204d 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -495,8 +495,15 @@ export function parseGoVersionFile(versionFilePath: string): string { path.basename(versionFilePath) === 'go.mod' || path.basename(versionFilePath) === 'go.work' ) { - const match = contents.match(/^go (\d+(\.\d+)*)/m); - return match ? match[1] : ''; + // toolchain directive: https://go.dev/ref/mod#go-mod-file-toolchain + const matchToolchain = contents.match(/^toolchain go(\d+(\.\d+)*)/m); + if (matchToolchain) { + return matchToolchain[1]; + } + + // go directive: https://go.dev/ref/mod#go-mod-file-go + const matchGo = contents.match(/^go (\d+(\.\d+)*)/m); + return matchGo ? matchGo[1] : ''; } return contents.trim(); From 277ab1eb0ed26622ce40991ce64902ca51c78d09 Mon Sep 17 00:00:00 2001 From: Matthew Hughes Date: Mon, 25 Aug 2025 20:07:45 +0100 Subject: [PATCH 3/4] squash! Configure environment to avoid toolchain installs Only modify env if `GOTOOLCHAIN` is not set --- dist/setup/index.js | 17 +++++++++-------- src/installer.ts | 2 ++ src/main.ts | 18 +++++++++--------- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/dist/setup/index.js b/dist/setup/index.js index da47f5f..ca909af 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -94312,6 +94312,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.GOTOOLCHAIN_LOCAL_VAL = exports.GOTOOLCHAIN_ENV_VAR = void 0; exports.getGo = getGo; exports.extractGoArchive = extractGoArchive; exports.getManifest = getManifest; @@ -94330,6 +94331,8 @@ const sys = __importStar(__nccwpck_require__(5632)); const fs_1 = __importDefault(__nccwpck_require__(7147)); const os_1 = __importDefault(__nccwpck_require__(2037)); const utils_1 = __nccwpck_require__(1314); +exports.GOTOOLCHAIN_ENV_VAR = 'GOTOOLCHAIN'; +exports.GOTOOLCHAIN_LOCAL_VAL = 'local'; const MANIFEST_REPO_OWNER = 'actions'; const MANIFEST_REPO_NAME = 'go-versions'; const MANIFEST_REPO_BRANCH = 'main'; @@ -94783,12 +94786,12 @@ const os_1 = __importDefault(__nccwpck_require__(2037)); function run() { return __awaiter(this, void 0, void 0, function* () { try { - setToolchain(); // // versionSpec is optional. If supplied, install / use from the tool cache // If not supplied then problem matchers will still be setup. Useful for self-hosted. // const versionSpec = resolveVersionInput(); + setGoToolchain(); const cache = core.getBooleanInput('cache'); core.info(`Setup go version spec ${versionSpec}`); let arch = core.getInput('architecture'); @@ -94897,20 +94900,18 @@ function resolveVersionInput() { } return version; } -function setToolchain() { +function setGoToolchain() { // docs: https://go.dev/doc/toolchain // "local indicates the bundled Go toolchain (the one that shipped with the go command being run)" // this is so any 'go' command is run with the selected Go version // and doesn't trigger a toolchain download and run commands with that // see e.g. issue #424 - // and a similar discussion: https://github.com/docker-library/golang/issues/472 - const toolchain = 'local'; - const toolchainVar = 'GOTOOLCHAIN'; - // set the value in process env so any `go` commands run as child-process + // and a similar discussion: https://github.com/docker-library/golang/issues/472. + // Set the value in process env so any `go` commands run as child-process // don't cause toolchain downloads - process.env[toolchainVar] = toolchain; + process.env[installer.GOTOOLCHAIN_ENV_VAR] = installer.GOTOOLCHAIN_LOCAL_VAL; // and in the runner env so e.g. a user running `go mod tidy` won't cause it - core.exportVariable(toolchainVar, toolchain); + core.exportVariable(installer.GOTOOLCHAIN_ENV_VAR, installer.GOTOOLCHAIN_LOCAL_VAL); } diff --git a/src/installer.ts b/src/installer.ts index 4b8204d..7887ba2 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -8,6 +8,8 @@ import fs from 'fs'; import os from 'os'; import {StableReleaseAlias, isSelfHosted} from './utils'; +export const GOTOOLCHAIN_ENV_VAR = 'GOTOOLCHAIN'; +export const GOTOOLCHAIN_LOCAL_VAL = 'local'; const MANIFEST_REPO_OWNER = 'actions'; const MANIFEST_REPO_NAME = 'go-versions'; const MANIFEST_REPO_BRANCH = 'main'; diff --git a/src/main.ts b/src/main.ts index a52951c..0412d13 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,12 +11,12 @@ import os from 'os'; export async function run() { try { - setToolchain(); // // versionSpec is optional. If supplied, install / use from the tool cache // If not supplied then problem matchers will still be setup. Useful for self-hosted. // const versionSpec = resolveVersionInput(); + setGoToolchain(); const cache = core.getBooleanInput('cache'); core.info(`Setup go version spec ${versionSpec}`); @@ -162,19 +162,19 @@ function resolveVersionInput(): string { return version; } -function setToolchain() { +function setGoToolchain() { // docs: https://go.dev/doc/toolchain // "local indicates the bundled Go toolchain (the one that shipped with the go command being run)" // this is so any 'go' command is run with the selected Go version // and doesn't trigger a toolchain download and run commands with that // see e.g. issue #424 - // and a similar discussion: https://github.com/docker-library/golang/issues/472 - const toolchain = 'local'; - const toolchainVar = 'GOTOOLCHAIN'; - - // set the value in process env so any `go` commands run as child-process + // and a similar discussion: https://github.com/docker-library/golang/issues/472. + // Set the value in process env so any `go` commands run as child-process // don't cause toolchain downloads - process.env[toolchainVar] = toolchain; + process.env[installer.GOTOOLCHAIN_ENV_VAR] = installer.GOTOOLCHAIN_LOCAL_VAL; // and in the runner env so e.g. a user running `go mod tidy` won't cause it - core.exportVariable(toolchainVar, toolchain); + core.exportVariable( + installer.GOTOOLCHAIN_ENV_VAR, + installer.GOTOOLCHAIN_LOCAL_VAL + ); } From b967a467e1c904489be015f76916b6bb83d60ef2 Mon Sep 17 00:00:00 2001 From: Matthew Hughes Date: Mon, 25 Aug 2025 20:08:21 +0100 Subject: [PATCH 4/4] squash! Prefer installing version from `toolchain` directive Avoid installing from `toolchain` if `GOTOOLCHAIN` is `local`, also better regex for matching toolchain directive --- __tests__/setup-go.test.ts | 89 +++++++++++++++++++++++++++----------- dist/setup/index.js | 12 +++-- src/installer.ts | 14 ++++-- 3 files changed, 82 insertions(+), 33 deletions(-) diff --git a/__tests__/setup-go.test.ts b/__tests__/setup-go.test.ts index e167ce6..b89c08f 100644 --- a/__tests__/setup-go.test.ts +++ b/__tests__/setup-go.test.ts @@ -129,6 +129,9 @@ describe('setup-go', () => { }); afterEach(() => { + // clear out env var set during 'run' + delete process.env[im.GOTOOLCHAIN_ENV_VAR]; + //jest.resetAllMocks(); jest.clearAllMocks(); //jest.restoreAllMocks(); @@ -989,11 +992,16 @@ use . }); describe('go-version-file-toolchain', () => { - const goModContents = `module example.com/mymodule + const goVersions = ['1.22.0', '1.21rc2', '1.18']; + const placeholderVersion = '1.19'; + const buildGoMod = ( + goVersion: string, + toolchainVersion: string + ) => `module example.com/mymodule -go 1.14 +go ${goVersion} -toolchain go1.21.0 +toolchain go${toolchainVersion} require ( example.com/othermodule v1.2.3 @@ -1005,36 +1013,67 @@ replace example.com/thatmodule => ../thatmodule exclude example.com/thismodule v1.3.0 `; - const goWorkContents = `go 1.19 + const buildGoWork = ( + goVersion: string, + toolchainVersion: string + ) => `go 1.19 -toolchain go1.21.0 +toolchain go${toolchainVersion} use . `; - it('reads version from toolchain directive in go.mod', async () => { - inputs['go-version-file'] = 'go.mod'; - existsSpy.mockImplementation(() => true); - readFileSpy.mockImplementation(() => Buffer.from(goModContents)); + goVersions.forEach(version => { + [ + { + goVersionfile: 'go.mod', + fileContents: Buffer.from(buildGoMod(placeholderVersion, version)), + expected_version: version, + desc: 'from toolchain directive' + }, + { + goVersionfile: 'go.work', + fileContents: Buffer.from(buildGoMod(placeholderVersion, version)), + expected_version: version, + desc: 'from toolchain directive' + }, + { + goVersionfile: 'go.mod', + fileContents: Buffer.from(buildGoMod(placeholderVersion, version)), + gotoolchain_env: 'local', + expected_version: placeholderVersion, + desc: 'from go directive when GOTOOLCHAIN is local' + }, + { + goVersionfile: 'go.work', + fileContents: Buffer.from(buildGoMod(placeholderVersion, version)), + gotoolchain_env: 'local', + expected_version: placeholderVersion, + desc: 'from go directive when GOTOOLCHAIN is local' + } + ].forEach(test => { + it(`reads version (${version}) in ${test.goVersionfile} ${test.desc}`, async () => { + inputs['go-version-file'] = test.goVersionfile; + if (test.gotoolchain_env !== undefined) { + process.env[im.GOTOOLCHAIN_ENV_VAR] = test.gotoolchain_env; + } + existsSpy.mockImplementation(() => true); + readFileSpy.mockImplementation(() => Buffer.from(test.fileContents)); - await main.run(); + await main.run(); - expect(logSpy).toHaveBeenCalledWith('Setup go version spec 1.21.0'); - expect(logSpy).toHaveBeenCalledWith('Attempting to download 1.21.0...'); - expect(logSpy).toHaveBeenCalledWith('matching 1.21.0...'); - }); - - it('reads version from toolchain directive in go.work', async () => { - inputs['go-version-file'] = 'go.work'; - existsSpy.mockImplementation(() => true); - readFileSpy.mockImplementation(() => Buffer.from(goWorkContents)); - - await main.run(); - - expect(logSpy).toHaveBeenCalledWith('Setup go version spec 1.21.0'); - expect(logSpy).toHaveBeenCalledWith('Attempting to download 1.21.0...'); - expect(logSpy).toHaveBeenCalledWith('matching 1.21.0...'); + expect(logSpy).toHaveBeenCalledWith( + `Setup go version spec ${test.expected_version}` + ); + expect(logSpy).toHaveBeenCalledWith( + `Attempting to download ${test.expected_version}...` + ); + expect(logSpy).toHaveBeenCalledWith( + `matching ${test.expected_version}...` + ); + }); + }); }); }); diff --git a/dist/setup/index.js b/dist/setup/index.js index ca909af..96f4ef9 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -94666,10 +94666,14 @@ function parseGoVersionFile(versionFilePath) { const contents = fs_1.default.readFileSync(versionFilePath).toString(); if (path.basename(versionFilePath) === 'go.mod' || path.basename(versionFilePath) === 'go.work') { - // toolchain directive: https://go.dev/ref/mod#go-mod-file-toolchain - const matchToolchain = contents.match(/^toolchain go(\d+(\.\d+)*)/m); - if (matchToolchain) { - return matchToolchain[1]; + // for backwards compatibility: use version from go directive if + // 'GOTOOLCHAIN' has been explicitly set + if (process.env[exports.GOTOOLCHAIN_ENV_VAR] !== exports.GOTOOLCHAIN_LOCAL_VAL) { + // toolchain directive: https://go.dev/ref/mod#go-mod-file-toolchain + const matchToolchain = contents.match(/^toolchain go(1\.\d+(?:\.\d+|rc\d+)?)/m); + if (matchToolchain) { + return matchToolchain[1]; + } } // go directive: https://go.dev/ref/mod#go-mod-file-go const matchGo = contents.match(/^go (\d+(\.\d+)*)/m); diff --git a/src/installer.ts b/src/installer.ts index 7887ba2..2fad1d8 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -497,10 +497,16 @@ export function parseGoVersionFile(versionFilePath: string): string { path.basename(versionFilePath) === 'go.mod' || path.basename(versionFilePath) === 'go.work' ) { - // toolchain directive: https://go.dev/ref/mod#go-mod-file-toolchain - const matchToolchain = contents.match(/^toolchain go(\d+(\.\d+)*)/m); - if (matchToolchain) { - return matchToolchain[1]; + // for backwards compatibility: use version from go directive if + // 'GOTOOLCHAIN' has been explicitly set + if (process.env[GOTOOLCHAIN_ENV_VAR] !== GOTOOLCHAIN_LOCAL_VAL) { + // toolchain directive: https://go.dev/ref/mod#go-mod-file-toolchain + const matchToolchain = contents.match( + /^toolchain go(1\.\d+(?:\.\d+|rc\d+)?)/m + ); + if (matchToolchain) { + return matchToolchain[1]; + } } // go directive: https://go.dev/ref/mod#go-mod-file-go