Skip to content

Commit 1d76b95

Browse files
Improve toolchain handling (#460)
* 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] docker-library/golang#472 * 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: #457 * squash! Configure environment to avoid toolchain installs Only modify env if `GOTOOLCHAIN` is not set * squash! Prefer installing version from `toolchain` directive Avoid installing from `toolchain` if `GOTOOLCHAIN` is `local`, also better regex for matching toolchain directive
1 parent e75c3e8 commit 1d76b95

File tree

5 files changed

+178
-11
lines changed

5 files changed

+178
-11
lines changed

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,9 +191,15 @@ steps:
191191

192192
## Getting go version from the go.mod file
193193

194-
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.
194+
The `go-version-file` input accepts a path to a `go.mod` file or a `go.work`
195+
file that contains the version of Go to be used by a project. The version taken
196+
from thils file will be:
197+
198+
- The version from the `toolchain` directive, if there is one, otherwise
199+
- The version from the `go` directive
200+
201+
The version can specify a patch version or omit it altogether (e.g., `go 1.22.0` or `go 1.22`).
195202

196-
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`).
197203
If a patch version is specified, that specific patch version will be used.
198204
If no patch version is specified, it will search for the latest available patch version in the cache,
199205
[versions-manifest.json](https://github.com/actions/go-versions/blob/main/versions-manifest.json), and the

__tests__/setup-go.test.ts

Lines changed: 106 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ describe('setup-go', () => {
129129
});
130130

131131
afterEach(() => {
132+
// clear out env var set during 'run'
133+
delete process.env[im.GOTOOLCHAIN_ENV_VAR];
134+
132135
//jest.resetAllMocks();
133136
jest.clearAllMocks();
134137
//jest.restoreAllMocks();
@@ -285,7 +288,7 @@ describe('setup-go', () => {
285288
expect(logSpy).toHaveBeenCalledWith(`Setup go version spec 1.13.0`);
286289
});
287290

288-
it('does not export any variables for Go versions >=1.9', async () => {
291+
it('does not export GOROOT for Go versions >=1.9', async () => {
289292
inputs['go-version'] = '1.13.0';
290293
inSpy.mockImplementation(name => inputs[name]);
291294

@@ -298,7 +301,7 @@ describe('setup-go', () => {
298301
});
299302

300303
await main.run();
301-
expect(vars).toStrictEqual({});
304+
expect(vars).not.toHaveProperty('GOROOT');
302305
});
303306

304307
it('exports GOROOT for Go versions <1.9', async () => {
@@ -314,9 +317,7 @@ describe('setup-go', () => {
314317
});
315318

316319
await main.run();
317-
expect(vars).toStrictEqual({
318-
GOROOT: toolPath
319-
});
320+
expect(vars).toHaveProperty('GOROOT', toolPath);
320321
});
321322

322323
it('finds a version of go already in the cache', async () => {
@@ -989,4 +990,104 @@ use .
989990
}
990991
);
991992
});
993+
994+
describe('go-version-file-toolchain', () => {
995+
const goVersions = ['1.22.0', '1.21rc2', '1.18'];
996+
const placeholderVersion = '1.19';
997+
const buildGoMod = (
998+
goVersion: string,
999+
toolchainVersion: string
1000+
) => `module example.com/mymodule
1001+
1002+
go ${goVersion}
1003+
1004+
toolchain go${toolchainVersion}
1005+
1006+
require (
1007+
example.com/othermodule v1.2.3
1008+
example.com/thismodule v1.2.3
1009+
example.com/thatmodule v1.2.3
1010+
)
1011+
1012+
replace example.com/thatmodule => ../thatmodule
1013+
exclude example.com/thismodule v1.3.0
1014+
`;
1015+
1016+
const buildGoWork = (
1017+
goVersion: string,
1018+
toolchainVersion: string
1019+
) => `go 1.19
1020+
1021+
toolchain go${toolchainVersion}
1022+
1023+
use .
1024+
1025+
`;
1026+
1027+
goVersions.forEach(version => {
1028+
[
1029+
{
1030+
goVersionfile: 'go.mod',
1031+
fileContents: Buffer.from(buildGoMod(placeholderVersion, version)),
1032+
expected_version: version,
1033+
desc: 'from toolchain directive'
1034+
},
1035+
{
1036+
goVersionfile: 'go.work',
1037+
fileContents: Buffer.from(buildGoMod(placeholderVersion, version)),
1038+
expected_version: version,
1039+
desc: 'from toolchain directive'
1040+
},
1041+
{
1042+
goVersionfile: 'go.mod',
1043+
fileContents: Buffer.from(buildGoMod(placeholderVersion, version)),
1044+
gotoolchain_env: 'local',
1045+
expected_version: placeholderVersion,
1046+
desc: 'from go directive when GOTOOLCHAIN is local'
1047+
},
1048+
{
1049+
goVersionfile: 'go.work',
1050+
fileContents: Buffer.from(buildGoMod(placeholderVersion, version)),
1051+
gotoolchain_env: 'local',
1052+
expected_version: placeholderVersion,
1053+
desc: 'from go directive when GOTOOLCHAIN is local'
1054+
}
1055+
].forEach(test => {
1056+
it(`reads version (${version}) in ${test.goVersionfile} ${test.desc}`, async () => {
1057+
inputs['go-version-file'] = test.goVersionfile;
1058+
if (test.gotoolchain_env !== undefined) {
1059+
process.env[im.GOTOOLCHAIN_ENV_VAR] = test.gotoolchain_env;
1060+
}
1061+
existsSpy.mockImplementation(() => true);
1062+
readFileSpy.mockImplementation(() => Buffer.from(test.fileContents));
1063+
1064+
await main.run();
1065+
1066+
expect(logSpy).toHaveBeenCalledWith(
1067+
`Setup go version spec ${test.expected_version}`
1068+
);
1069+
expect(logSpy).toHaveBeenCalledWith(
1070+
`Attempting to download ${test.expected_version}...`
1071+
);
1072+
expect(logSpy).toHaveBeenCalledWith(
1073+
`matching ${test.expected_version}...`
1074+
);
1075+
});
1076+
});
1077+
});
1078+
});
1079+
1080+
it('exports GOTOOLCHAIN and sets it in current process env', async () => {
1081+
inputs['go-version'] = '1.21.0';
1082+
inSpy.mockImplementation(name => inputs[name]);
1083+
1084+
const vars: {[key: string]: string} = {};
1085+
exportVarSpy.mockImplementation((name: string, val: string) => {
1086+
vars[name] = val;
1087+
});
1088+
1089+
await main.run();
1090+
expect(vars).toStrictEqual({GOTOOLCHAIN: 'local'});
1091+
expect(process.env).toHaveProperty('GOTOOLCHAIN', 'local');
1092+
});
9921093
});

dist/setup/index.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94312,6 +94312,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
9431294312
return (mod && mod.__esModule) ? mod : { "default": mod };
9431394313
};
9431494314
Object.defineProperty(exports, "__esModule", ({ value: true }));
94315+
exports.GOTOOLCHAIN_LOCAL_VAL = exports.GOTOOLCHAIN_ENV_VAR = void 0;
9431594316
exports.getGo = getGo;
9431694317
exports.extractGoArchive = extractGoArchive;
9431794318
exports.getManifest = getManifest;
@@ -94330,6 +94331,8 @@ const sys = __importStar(__nccwpck_require__(5632));
9433094331
const fs_1 = __importDefault(__nccwpck_require__(7147));
9433194332
const os_1 = __importDefault(__nccwpck_require__(2037));
9433294333
const utils_1 = __nccwpck_require__(1314);
94334+
exports.GOTOOLCHAIN_ENV_VAR = 'GOTOOLCHAIN';
94335+
exports.GOTOOLCHAIN_LOCAL_VAL = 'local';
9433394336
const MANIFEST_REPO_OWNER = 'actions';
9433494337
const MANIFEST_REPO_NAME = 'go-versions';
9433594338
const MANIFEST_REPO_BRANCH = 'main';
@@ -94663,8 +94666,18 @@ function parseGoVersionFile(versionFilePath) {
9466394666
const contents = fs_1.default.readFileSync(versionFilePath).toString();
9466494667
if (path.basename(versionFilePath) === 'go.mod' ||
9466594668
path.basename(versionFilePath) === 'go.work') {
94666-
const match = contents.match(/^go (\d+(\.\d+)*)/m);
94667-
return match ? match[1] : '';
94669+
// for backwards compatibility: use version from go directive if
94670+
// 'GOTOOLCHAIN' has been explicitly set
94671+
if (process.env[exports.GOTOOLCHAIN_ENV_VAR] !== exports.GOTOOLCHAIN_LOCAL_VAL) {
94672+
// toolchain directive: https://go.dev/ref/mod#go-mod-file-toolchain
94673+
const matchToolchain = contents.match(/^toolchain go(1\.\d+(?:\.\d+|rc\d+)?)/m);
94674+
if (matchToolchain) {
94675+
return matchToolchain[1];
94676+
}
94677+
}
94678+
// go directive: https://go.dev/ref/mod#go-mod-file-go
94679+
const matchGo = contents.match(/^go (\d+(\.\d+)*)/m);
94680+
return matchGo ? matchGo[1] : '';
9466894681
}
9466994682
return contents.trim();
9467094683
}
@@ -94782,6 +94795,7 @@ function run() {
9478294795
// If not supplied then problem matchers will still be setup. Useful for self-hosted.
9478394796
//
9478494797
const versionSpec = resolveVersionInput();
94798+
setGoToolchain();
9478594799
const cache = core.getBooleanInput('cache');
9478694800
core.info(`Setup go version spec ${versionSpec}`);
9478794801
let arch = core.getInput('architecture');
@@ -94890,6 +94904,19 @@ function resolveVersionInput() {
9489094904
}
9489194905
return version;
9489294906
}
94907+
function setGoToolchain() {
94908+
// docs: https://go.dev/doc/toolchain
94909+
// "local indicates the bundled Go toolchain (the one that shipped with the go command being run)"
94910+
// this is so any 'go' command is run with the selected Go version
94911+
// and doesn't trigger a toolchain download and run commands with that
94912+
// see e.g. issue #424
94913+
// and a similar discussion: https://github.com/docker-library/golang/issues/472.
94914+
// Set the value in process env so any `go` commands run as child-process
94915+
// don't cause toolchain downloads
94916+
process.env[installer.GOTOOLCHAIN_ENV_VAR] = installer.GOTOOLCHAIN_LOCAL_VAL;
94917+
// and in the runner env so e.g. a user running `go mod tidy` won't cause it
94918+
core.exportVariable(installer.GOTOOLCHAIN_ENV_VAR, installer.GOTOOLCHAIN_LOCAL_VAL);
94919+
}
9489394920

9489494921

9489594922
/***/ }),

src/installer.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import fs from 'fs';
88
import os from 'os';
99
import {StableReleaseAlias, isSelfHosted} from './utils';
1010

11+
export const GOTOOLCHAIN_ENV_VAR = 'GOTOOLCHAIN';
12+
export const GOTOOLCHAIN_LOCAL_VAL = 'local';
1113
const MANIFEST_REPO_OWNER = 'actions';
1214
const MANIFEST_REPO_NAME = 'go-versions';
1315
const MANIFEST_REPO_BRANCH = 'main';
@@ -495,8 +497,21 @@ export function parseGoVersionFile(versionFilePath: string): string {
495497
path.basename(versionFilePath) === 'go.mod' ||
496498
path.basename(versionFilePath) === 'go.work'
497499
) {
498-
const match = contents.match(/^go (\d+(\.\d+)*)/m);
499-
return match ? match[1] : '';
500+
// for backwards compatibility: use version from go directive if
501+
// 'GOTOOLCHAIN' has been explicitly set
502+
if (process.env[GOTOOLCHAIN_ENV_VAR] !== GOTOOLCHAIN_LOCAL_VAL) {
503+
// toolchain directive: https://go.dev/ref/mod#go-mod-file-toolchain
504+
const matchToolchain = contents.match(
505+
/^toolchain go(1\.\d+(?:\.\d+|rc\d+)?)/m
506+
);
507+
if (matchToolchain) {
508+
return matchToolchain[1];
509+
}
510+
}
511+
512+
// go directive: https://go.dev/ref/mod#go-mod-file-go
513+
const matchGo = contents.match(/^go (\d+(\.\d+)*)/m);
514+
return matchGo ? matchGo[1] : '';
500515
}
501516

502517
return contents.trim();

src/main.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export async function run() {
1616
// If not supplied then problem matchers will still be setup. Useful for self-hosted.
1717
//
1818
const versionSpec = resolveVersionInput();
19+
setGoToolchain();
1920

2021
const cache = core.getBooleanInput('cache');
2122
core.info(`Setup go version spec ${versionSpec}`);
@@ -160,3 +161,20 @@ function resolveVersionInput(): string {
160161

161162
return version;
162163
}
164+
165+
function setGoToolchain() {
166+
// docs: https://go.dev/doc/toolchain
167+
// "local indicates the bundled Go toolchain (the one that shipped with the go command being run)"
168+
// this is so any 'go' command is run with the selected Go version
169+
// and doesn't trigger a toolchain download and run commands with that
170+
// see e.g. issue #424
171+
// and a similar discussion: https://github.com/docker-library/golang/issues/472.
172+
// Set the value in process env so any `go` commands run as child-process
173+
// don't cause toolchain downloads
174+
process.env[installer.GOTOOLCHAIN_ENV_VAR] = installer.GOTOOLCHAIN_LOCAL_VAL;
175+
// and in the runner env so e.g. a user running `go mod tidy` won't cause it
176+
core.exportVariable(
177+
installer.GOTOOLCHAIN_ENV_VAR,
178+
installer.GOTOOLCHAIN_LOCAL_VAL
179+
);
180+
}

0 commit comments

Comments
 (0)