This commit is contained in:
John Maguire 2026-08-19 17:10:41 +02:00 committed by GitHub
commit 80d79fad81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 387 additions and 24 deletions

View file

@ -44,6 +44,11 @@ See [action.yml](action.yml).
# Note: if both go-version and go-version-file are provided, go-version takes precedence. # Note: if both go-version and go-version-file are provided, go-version takes precedence.
go-version-file: 'go.mod' go-version-file: 'go.mod'
# How to interpret an exact version read from go-version-file.
# Set to latest-patch to use the newest patch release of the same minor version.
# Default: exact
go-version-file-behavior: 'exact'
# Set this option if you want the action to check for the latest available version # Set this option if you want the action to check for the latest available version
# Default: false # Default: false
check-latest: false check-latest: false

View file

@ -1089,6 +1089,186 @@ use .
); );
}); });
describe('go-version-file-behavior', () => {
const buildGoMod = (goVersion: string) => `module example.com/mymodule
go ${goVersion}
`;
it('resolves the latest patch of the minor with latest-patch', async () => {
os.platform = 'linux';
os.arch = 'x64';
inputs['go-version-file'] = 'go.mod';
inputs['go-version-file-behavior'] = 'latest-patch';
inputs['token'] = 'faketoken';
existsSpy.mockImplementation(() => true);
readFileSpy.mockImplementation(() => Buffer.from(buildGoMod('1.12.16')));
const expectedUrl =
'https://github.com/actions/go-versions/releases/download/1.12.17-20200616.21/go-1.12.17-linux-x64.tar.gz';
// ... but not in the local cache
findSpy.mockImplementation(() => '');
dlSpy.mockImplementation(async () => '/some/temp/path');
const toolPath = path.normalize('/cache/go/1.12.17/x64');
extractTarSpy.mockImplementation(async () => '/some/other/temp/path');
cacheSpy.mockImplementation(async () => toolPath);
await main.run();
expect(logSpy).toHaveBeenCalledWith(
'Using latest patch release satisfying ~1.12.16 (version file specifies 1.12.16)'
);
expect(logSpy).toHaveBeenCalledWith('Setup go version spec ~1.12.16');
expect(logSpy).toHaveBeenCalledWith(
'go-version-file-behavior: latest-patch implies check-latest'
);
expect(logSpy).toHaveBeenCalledWith(
'Attempting to resolve the latest version from the manifest...'
);
expect(logSpy).toHaveBeenCalledWith(
`Acquiring 1.12.17 from ${expectedUrl}`
);
});
it('warns and falls back when the manifest cannot be resolved', async () => {
os.platform = 'linux';
os.arch = 'x64';
inputs['go-version-file'] = 'go.mod';
inputs['go-version-file-behavior'] = 'latest-patch';
inputs['token'] = 'faketoken';
existsSpy.mockImplementation(() => true);
readFileSpy.mockImplementation(() => Buffer.from(buildGoMod('1.12.16')));
getManifestSpy.mockImplementation(() => {
throw new Error('Unable to download manifest');
});
(httpmGetJsonSpy as jest.Mock<any>).mockRejectedValue(
new Error('Unable to download manifest from raw URL')
);
// ... and not in the local cache, so the dist fallback downloads
findSpy.mockImplementation(() => '');
dlSpy.mockImplementation(async () => '/some/temp/path');
const toolPath = path.normalize('/cache/go/1.12.17/x64');
extractTarSpy.mockImplementation(async () => '/some/other/temp/path');
cacheSpy.mockImplementation(async () => toolPath);
await main.run();
expect(cnSpy).toHaveBeenCalledWith(
`::warning::go-version-file-behavior: latest-patch could not be honored: unable to resolve ~1.12.16 from the versions manifest. Falling back to the version spec, which may resolve to an older patch release from the runner's tool cache.${osm.EOL}`
);
expect(dlSpy).toHaveBeenCalled();
});
it('leaves a bare minor version unchanged with latest-patch', async () => {
inputs['go-version-file'] = 'go.mod';
inputs['go-version-file-behavior'] = 'latest-patch';
existsSpy.mockImplementation(() => true);
readFileSpy.mockImplementation(() => Buffer.from(buildGoMod('1.14')));
await main.run();
expect(logSpy).toHaveBeenCalledWith(
'Using version 1.14 as written (latest-patch only widens exact major.minor.patch versions)'
);
expect(logSpy).toHaveBeenCalledWith('Setup go version spec 1.14');
});
it('does not widen an explicit toolchain directive pin', async () => {
inputs['go-version-file'] = 'go.mod';
inputs['go-version-file-behavior'] = 'latest-patch';
existsSpy.mockImplementation(() => true);
readFileSpy.mockImplementation(() =>
Buffer.from(`module example.com/mymodule
go 1.21
toolchain go1.22.3
`)
);
await main.run();
expect(logSpy).toHaveBeenCalledWith(
'Using toolchain directive version 1.22.3 as written (latest-patch does not widen an explicit toolchain pin)'
);
expect(logSpy).toHaveBeenCalledWith('Setup go version spec 1.22.3');
});
it('fails when combined with a custom download base URL', async () => {
inputs['go-version-file'] = 'go.mod';
inputs['go-version-file-behavior'] = 'latest-patch';
inputs['go-download-base-url'] = 'https://internal.example.com/go';
existsSpy.mockImplementation(() => true);
readFileSpy.mockImplementation(() => Buffer.from(buildGoMod('1.22.0')));
await main.run();
expect(cnSpy).toHaveBeenCalledWith(
`::error::go-version-file-behavior: 'latest-patch' is not supported with a custom download base URL because version ranges cannot be resolved against it. Use the default 'exact' behavior.${osm.EOL}`
);
});
it('uses the exact version by default', async () => {
inputs['go-version-file'] = 'go.mod';
existsSpy.mockImplementation(() => true);
readFileSpy.mockImplementation(() => Buffer.from(buildGoMod('1.12.16')));
await main.run();
expect(logSpy).toHaveBeenCalledWith('Setup go version spec 1.12.16');
});
it('does not apply to the go-version input', async () => {
inputs['go-version'] = '1.12.16';
inputs['go-version-file-behavior'] = 'latest-patch';
await main.run();
expect(logSpy).toHaveBeenCalledWith('Setup go version spec 1.12.16');
});
it('fails on an unsupported value', async () => {
inputs['go-version-file'] = 'go.mod';
inputs['go-version-file-behavior'] = 'newest';
existsSpy.mockImplementation(() => true);
readFileSpy.mockImplementation(() => Buffer.from(buildGoMod('1.12.16')));
await main.run();
expect(cnSpy).toHaveBeenCalledWith(
`::error::Invalid go-version-file-behavior: 'newest'. Supported values: 'exact', 'latest-patch'${osm.EOL}`
);
});
it('fails on an unsupported value even when go-version is used', async () => {
inputs['go-version'] = '1.12.16';
inputs['go-version-file-behavior'] = 'newest';
await main.run();
expect(cnSpy).toHaveBeenCalledWith(
`::error::Invalid go-version-file-behavior: 'newest'. Supported values: 'exact', 'latest-patch'${osm.EOL}`
);
});
it.each([
['1.22.0', '~1.22.0'],
['v1.22.0', '~1.22.0'],
['1.22', '1.22'],
['1.21rc2', '1.21rc2'],
['1.22.x', '1.22.x'],
['>=1.22.0', '>=1.22.0']
])('latestPatchSpec(%s) == %s', (version, expected) => {
expect(im.latestPatchSpec(version)).toBe(expected);
});
});
describe('go-version-file-toolchain', () => { describe('go-version-file-toolchain', () => {
const goVersions = ['1.22.0', '1.21rc2', '1.18']; const goVersions = ['1.22.0', '1.21rc2', '1.18'];
const placeholderVersion = '1.19'; const placeholderVersion = '1.19';

View file

@ -6,6 +6,9 @@ inputs:
description: 'The Go version to download (if necessary) and use. Supports semver spec and ranges. Be sure to enclose this option in single quotation marks.' description: 'The Go version to download (if necessary) and use. Supports semver spec and ranges. Be sure to enclose this option in single quotation marks.'
go-version-file: go-version-file:
description: 'Path to the go.mod, go.work, .go-version, or .tool-versions file.' description: 'Path to the go.mod, go.work, .go-version, or .tool-versions file.'
go-version-file-behavior:
description: 'How to interpret an exact version read from go-version-file. Use "latest-patch" to resolve the newest available patch release of the same minor version (e.g. "1.22.0" in go.mod resolves to the newest 1.22.x); this implies check-latest and is not supported with go-download-base-url. Defaults to "exact", which uses the version as written.'
default: exact
check-latest: check-latest:
description: 'Set this option to true if you want the action to always check for the latest available version that satisfies the version spec' description: 'Set this option to true if you want the action to always check for the latest available version that satisfies the version spec'
default: false default: false

76
dist/setup/index.js vendored
View file

@ -43462,7 +43462,7 @@ const GOLANG_DOWNLOAD_URL = 'https://go.dev/dl/?mode=json&include=all';
// For these URLs we skip the getInfoFromDist() call entirely and construct // For these URLs we skip the getInfoFromDist() call entirely and construct
// the download URL directly, avoiding a guaranteed-404 HTTP request. // the download URL directly, avoiding a guaranteed-404 HTTP request.
const NO_VERSION_LISTING_BASE_URLS = ['https://aka.ms/golang/release/latest']; const NO_VERSION_LISTING_BASE_URLS = ['https://aka.ms/golang/release/latest'];
async function getGo(versionSpec, checkLatest, auth, arch = external_os_default().arch(), goDownloadBaseUrl) { async function getGo(versionSpec, checkLatest, auth, arch = external_os_default().arch(), goDownloadBaseUrl, latestPatchApplied = false) {
let manifest; let manifest;
const osPlat = external_os_default().platform(); const osPlat = external_os_default().platform();
const customBaseUrl = goDownloadBaseUrl?.replace(/\/+$/, ''); const customBaseUrl = goDownloadBaseUrl?.replace(/\/+$/, '');
@ -43493,6 +43493,11 @@ async function getGo(versionSpec, checkLatest, auth, arch = external_os_default(
versionSpec = resolvedVersion; versionSpec = resolvedVersion;
core_info(`Resolved as '${versionSpec}'`); core_info(`Resolved as '${versionSpec}'`);
} }
else if (latestPatchApplied) {
// latest-patch depends on the manifest to see patches newer than
// the runner's tool cache, so a silent info line is not enough here
warning(`go-version-file-behavior: latest-patch could not be honored: unable to resolve ${versionSpec} from the versions manifest. Falling back to the version spec, which may resolve to an older patch release from the runner's tool cache.`);
}
else { else {
core_info(`Failed to resolve version ${versionSpec} from manifest`); core_info(`Failed to resolve version ${versionSpec} from manifest`);
} }
@ -43865,18 +43870,37 @@ function parseGoVersionFile(versionFilePath) {
// toolchain directive: https://go.dev/ref/mod#go-mod-file-toolchain // toolchain directive: https://go.dev/ref/mod#go-mod-file-toolchain
const matchToolchain = contents.match(/^toolchain go(1\.\d+(?:\.\d+|rc\d+)?)/m); const matchToolchain = contents.match(/^toolchain go(1\.\d+(?:\.\d+|rc\d+)?)/m);
if (matchToolchain) { if (matchToolchain) {
return matchToolchain[1]; return { version: matchToolchain[1], fromToolchainDirective: true };
} }
} }
// go directive: https://go.dev/ref/mod#go-mod-file-go // go directive: https://go.dev/ref/mod#go-mod-file-go
const matchGo = contents.match(/^go (\d+(\.\d+)*)/m); const matchGo = contents.match(/^go (\d+(\.\d+)*)/m);
return matchGo ? matchGo[1] : ''; return {
version: matchGo ? matchGo[1] : '',
fromToolchainDirective: false
};
} }
else if (external_path_.basename(versionFilePath) === '.tool-versions') { else if (external_path_.basename(versionFilePath) === '.tool-versions') {
const match = contents.match(/^golang\s+([^\n#]+)/m); const match = contents.match(/^golang\s+([^\n#]+)/m);
return match ? match[1].trim() : ''; return {
version: match ? match[1].trim() : '',
fromToolchainDirective: false
};
} }
return contents.trim(); return { version: contents.trim(), fromToolchainDirective: false };
}
// Widen an exact version from a version file into a semver range matching
// the newest patch release of the same minor (go-version-file-behavior:
// latest-patch). Only exact major.minor.patch versions are widened,
// optionally with a leading 'v' as found in some .go-version files: bare
// minors like '1.22' already resolve to the newest patch, and prereleases
// like '1.21rc2' have no patch series to float within.
function latestPatchSpec(version) {
const match = version.match(/^v?(\d+\.\d+\.\d+)$/);
if (!match) {
return version;
}
return `~${match[1]}`;
} }
async function resolveStableVersionDist(versionSpec, arch) { async function resolveStableVersionDist(versionSpec, arch) {
const archFilter = getArch(arch); const archFilter = getArch(arch);
@ -100573,7 +100597,7 @@ async function run() {
// versionSpec is optional. If supplied, install / use from the tool cache // 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. // If not supplied then problem matchers will still be setup. Useful for self-hosted.
// //
const versionSpec = resolveVersionInput(); const { version: versionSpec, latestPatchApplied } = resolveVersionInput();
setGoToolchain(); setGoToolchain();
const cache = getBooleanInput('cache'); const cache = getBooleanInput('cache');
core_info(`Setup go version spec ${versionSpec}`); core_info(`Setup go version spec ${versionSpec}`);
@ -100584,14 +100608,20 @@ async function run() {
if (versionSpec) { if (versionSpec) {
const token = getInput('token'); const token = getInput('token');
const auth = !token ? undefined : `token ${token}`; const auth = !token ? undefined : `token ${token}`;
const checkLatest = getBooleanInput('check-latest'); let checkLatest = getBooleanInput('check-latest');
if (latestPatchApplied && !checkLatest) {
// the runner's tool cache may only hold a stale patch release; the
// newest one has to come from the versions manifest
core_info('go-version-file-behavior: latest-patch implies check-latest');
checkLatest = true;
}
const goDownloadBaseUrl = getInput('go-download-base-url') || const goDownloadBaseUrl = getInput('go-download-base-url') ||
process.env['GO_DOWNLOAD_BASE_URL'] || process.env['GO_DOWNLOAD_BASE_URL'] ||
undefined; undefined;
if (goDownloadBaseUrl) { if (goDownloadBaseUrl) {
core_info(`Using custom Go download base URL: ${goDownloadBaseUrl}`); core_info(`Using custom Go download base URL: ${goDownloadBaseUrl}`);
} }
const installDir = await getGo(versionSpec, checkLatest, auth, arch, goDownloadBaseUrl); const installDir = await getGo(versionSpec, checkLatest, auth, arch, goDownloadBaseUrl, latestPatchApplied);
const installDirVersion = external_path_default().basename(external_path_default().dirname(installDir)); const installDirVersion = external_path_default().basename(external_path_default().dirname(installDir));
addPath(external_path_default().join(installDir, 'bin')); addPath(external_path_default().join(installDir, 'bin'));
core_info('Added go to the path'); core_info('Added go to the path');
@ -100672,19 +100702,43 @@ function parseGoVersion(versionString) {
function resolveVersionInput() { function resolveVersionInput() {
let version = getInput('go-version'); let version = getInput('go-version');
const versionFilePath = getInput('go-version-file'); const versionFilePath = getInput('go-version-file');
const behavior = getInput('go-version-file-behavior') || 'exact';
if (behavior !== 'exact' && behavior !== 'latest-patch') {
throw new Error(`Invalid go-version-file-behavior: '${behavior}'. Supported values: 'exact', 'latest-patch'`);
}
if (version && versionFilePath) { if (version && versionFilePath) {
warning('Both go-version and go-version-file inputs are specified, only go-version will be used'); warning('Both go-version and go-version-file inputs are specified, only go-version will be used');
} }
if (version) { if (version) {
return version; return { version, latestPatchApplied: false };
} }
if (versionFilePath) { if (versionFilePath) {
if (!external_fs_default().existsSync(versionFilePath)) { if (!external_fs_default().existsSync(versionFilePath)) {
throw new Error(`The specified go version file at: ${versionFilePath} does not exist`); throw new Error(`The specified go version file at: ${versionFilePath} does not exist`);
} }
version = parseGoVersionFile(versionFilePath); const versionFile = parseGoVersionFile(versionFilePath);
version = versionFile.version;
if (behavior === 'latest-patch' && version) {
if (versionFile.fromToolchainDirective) {
core_info(`Using toolchain directive version ${version} as written (latest-patch does not widen an explicit toolchain pin)`);
}
else {
const spec = latestPatchSpec(version);
if (spec === version) {
core_info(`Using version ${version} as written (latest-patch only widens exact major.minor.patch versions)`);
}
else {
if (getInput('go-download-base-url') ||
process.env['GO_DOWNLOAD_BASE_URL']) {
throw new Error(`go-version-file-behavior: 'latest-patch' is not supported with a custom download base URL because version ranges cannot be resolved against it. Use the default 'exact' behavior.`);
}
core_info(`Using latest patch release satisfying ${spec} (version file specifies ${version})`);
return { version: spec, latestPatchApplied: true };
}
}
}
} }
return version; return { version, latestPatchApplied: false };
} }
function setGoToolchain() { function setGoToolchain() {
// docs: https://go.dev/doc/toolchain // docs: https://go.dev/doc/toolchain

View file

@ -3,6 +3,7 @@
- [Specifying a go version](advanced-usage.md#specifying-a-go-version) - [Specifying a go version](advanced-usage.md#specifying-a-go-version)
- [Matrix testing](advanced-usage.md#matrix-testing) - [Matrix testing](advanced-usage.md#matrix-testing)
- [Using the go-version-file input](advanced-usage.md#using-the-go-version-file-input) - [Using the go-version-file input](advanced-usage.md#using-the-go-version-file-input)
- [Using the latest patch release](advanced-usage.md#using-the-latest-patch-release)
- [Check latest version](advanced-usage.md#check-latest-version) - [Check latest version](advanced-usage.md#check-latest-version)
- [Caching](advanced-usage.md#caching) - [Caching](advanced-usage.md#caching)
- [Caching in monorepos](advanced-usage.md#caching-in-monorepos) - [Caching in monorepos](advanced-usage.md#caching-in-monorepos)
@ -212,6 +213,37 @@ steps:
- run: go version - run: go version
``` ```
### Using the latest patch release
By default, an exact version read from the version file is used as written: a `go 1.22.0` directive installs exactly Go 1.22.0, even if newer 1.22.x patch releases with security fixes are available.
Set `go-version-file-behavior` to `latest-patch` to instead resolve the newest available patch release of the same minor version that is at least the version in the file (e.g., `go 1.22.0` resolves to the newest 1.22.x):
```yaml
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: 'go.mod'
go-version-file-behavior: 'latest-patch'
- run: go version
```
Because the newest patch release is often not yet present in the runner's tool cache, `latest-patch` implies `check-latest`: the newest matching patch is resolved from the versions manifest rather than from whatever the cache happens to hold.
Two operational effects to be aware of:
- The dependency cache key includes the installed Go version, so with `cache: true` each new Go patch release changes the key: the first run after a patch release rebuilds the module and build caches from scratch.
- If the versions manifest cannot be reached (for example on GitHub Enterprise Server or other runners without github.com access), the action emits a warning and falls back to resolving the version range locally, which may install an older patch release from the runner's tool cache.
Some versions are always used as written and are not affected by `latest-patch`:
- Versions without a patch component (e.g., `go 1.22`), which already resolve to the latest available patch release.
- Prerelease versions (e.g., `go1.22rc1`), which have no patch series to float within.
- An exact version pinned by a go.mod or go.work `toolchain` directive (e.g., `toolchain go1.22.3`): the pin is deliberate and is never widened.
`latest-patch` is not supported together with `go-download-base-url`, which requires an exact version.
## Check latest version ## Check latest version
The `check-latest` flag defaults to `false`. Use the default or set `check-latest` to `false` if you prefer stability The `check-latest` flag defaults to `false`. Use the default or set `check-latest` to `false` if you prefer stability

View file

@ -54,7 +54,8 @@ export async function getGo(
checkLatest: boolean, checkLatest: boolean,
auth: string | undefined, auth: string | undefined,
arch: Architecture = os.arch() as Architecture, arch: Architecture = os.arch() as Architecture,
goDownloadBaseUrl?: string goDownloadBaseUrl?: string,
latestPatchApplied = false
) { ) {
let manifest: tc.IToolRelease[] | undefined; let manifest: tc.IToolRelease[] | undefined;
const osPlat: string = os.platform(); const osPlat: string = os.platform();
@ -111,6 +112,12 @@ export async function getGo(
if (resolvedVersion) { if (resolvedVersion) {
versionSpec = resolvedVersion; versionSpec = resolvedVersion;
core.info(`Resolved as '${versionSpec}'`); core.info(`Resolved as '${versionSpec}'`);
} else if (latestPatchApplied) {
// latest-patch depends on the manifest to see patches newer than
// the runner's tool cache, so a silent info line is not enough here
core.warning(
`go-version-file-behavior: latest-patch could not be honored: unable to resolve ${versionSpec} from the versions manifest. Falling back to the version spec, which may resolve to an older patch release from the runner's tool cache.`
);
} else { } else {
core.info(`Failed to resolve version ${versionSpec} from manifest`); core.info(`Failed to resolve version ${versionSpec} from manifest`);
} }
@ -642,7 +649,16 @@ export function makeSemver(version: string): string {
return fullVersion; return fullVersion;
} }
export function parseGoVersionFile(versionFilePath: string): string { export interface GoVersionFileResult {
version: string;
// true when the version came from a go.mod/go.work toolchain directive,
// an explicit pin that must be respected as written
fromToolchainDirective: boolean;
}
export function parseGoVersionFile(
versionFilePath: string
): GoVersionFileResult {
const contents = fs.readFileSync(versionFilePath).toString(); const contents = fs.readFileSync(versionFilePath).toString();
if ( if (
@ -657,19 +673,39 @@ export function parseGoVersionFile(versionFilePath: string): string {
/^toolchain go(1\.\d+(?:\.\d+|rc\d+)?)/m /^toolchain go(1\.\d+(?:\.\d+|rc\d+)?)/m
); );
if (matchToolchain) { if (matchToolchain) {
return matchToolchain[1]; return {version: matchToolchain[1], fromToolchainDirective: true};
} }
} }
// go directive: https://go.dev/ref/mod#go-mod-file-go // go directive: https://go.dev/ref/mod#go-mod-file-go
const matchGo = contents.match(/^go (\d+(\.\d+)*)/m); const matchGo = contents.match(/^go (\d+(\.\d+)*)/m);
return matchGo ? matchGo[1] : ''; return {
version: matchGo ? matchGo[1] : '',
fromToolchainDirective: false
};
} else if (path.basename(versionFilePath) === '.tool-versions') { } else if (path.basename(versionFilePath) === '.tool-versions') {
const match = contents.match(/^golang\s+([^\n#]+)/m); const match = contents.match(/^golang\s+([^\n#]+)/m);
return match ? match[1].trim() : ''; return {
version: match ? match[1].trim() : '',
fromToolchainDirective: false
};
} }
return contents.trim(); return {version: contents.trim(), fromToolchainDirective: false};
}
// Widen an exact version from a version file into a semver range matching
// the newest patch release of the same minor (go-version-file-behavior:
// latest-patch). Only exact major.minor.patch versions are widened,
// optionally with a leading 'v' as found in some .go-version files: bare
// minors like '1.22' already resolve to the newest patch, and prereleases
// like '1.21rc2' have no patch series to float within.
export function latestPatchSpec(version: string): string {
const match = version.match(/^v?(\d+\.\d+\.\d+)$/);
if (!match) {
return version;
}
return `~${match[1]}`;
} }
async function resolveStableVersionDist( async function resolveStableVersionDist(

View file

@ -17,7 +17,7 @@ export async function run() {
// versionSpec is optional. If supplied, install / use from the tool cache // 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. // If not supplied then problem matchers will still be setup. Useful for self-hosted.
// //
const versionSpec = resolveVersionInput(); const {version: versionSpec, latestPatchApplied} = resolveVersionInput();
setGoToolchain(); setGoToolchain();
const cache = core.getBooleanInput('cache'); const cache = core.getBooleanInput('cache');
@ -33,7 +33,15 @@ export async function run() {
const token = core.getInput('token'); const token = core.getInput('token');
const auth = !token ? undefined : `token ${token}`; const auth = !token ? undefined : `token ${token}`;
const checkLatest = core.getBooleanInput('check-latest'); let checkLatest = core.getBooleanInput('check-latest');
if (latestPatchApplied && !checkLatest) {
// the runner's tool cache may only hold a stale patch release; the
// newest one has to come from the versions manifest
core.info(
'go-version-file-behavior: latest-patch implies check-latest'
);
checkLatest = true;
}
const goDownloadBaseUrl = const goDownloadBaseUrl =
core.getInput('go-download-base-url') || core.getInput('go-download-base-url') ||
@ -49,7 +57,8 @@ export async function run() {
checkLatest, checkLatest,
auth, auth,
arch, arch,
goDownloadBaseUrl goDownloadBaseUrl,
latestPatchApplied
); );
const installDirVersion = path.basename(path.dirname(installDir)); const installDirVersion = path.basename(path.dirname(installDir));
@ -152,10 +161,25 @@ export function parseGoVersion(versionString: string): string {
return versionString.split(' ')[2].slice('go'.length); return versionString.split(' ')[2].slice('go'.length);
} }
function resolveVersionInput(): string { interface ResolvedVersionInput {
version: string;
// true when latest-patch widened the version into a range; the newest
// matching patch must then be resolved from the versions manifest, not
// just the runner's local tool cache
latestPatchApplied: boolean;
}
function resolveVersionInput(): ResolvedVersionInput {
let version = core.getInput('go-version'); let version = core.getInput('go-version');
const versionFilePath = core.getInput('go-version-file'); const versionFilePath = core.getInput('go-version-file');
const behavior = core.getInput('go-version-file-behavior') || 'exact';
if (behavior !== 'exact' && behavior !== 'latest-patch') {
throw new Error(
`Invalid go-version-file-behavior: '${behavior}'. Supported values: 'exact', 'latest-patch'`
);
}
if (version && versionFilePath) { if (version && versionFilePath) {
core.warning( core.warning(
'Both go-version and go-version-file inputs are specified, only go-version will be used' 'Both go-version and go-version-file inputs are specified, only go-version will be used'
@ -163,7 +187,7 @@ function resolveVersionInput(): string {
} }
if (version) { if (version) {
return version; return {version, latestPatchApplied: false};
} }
if (versionFilePath) { if (versionFilePath) {
@ -172,10 +196,39 @@ function resolveVersionInput(): string {
`The specified go version file at: ${versionFilePath} does not exist` `The specified go version file at: ${versionFilePath} does not exist`
); );
} }
version = installer.parseGoVersionFile(versionFilePath); const versionFile = installer.parseGoVersionFile(versionFilePath);
version = versionFile.version;
if (behavior === 'latest-patch' && version) {
if (versionFile.fromToolchainDirective) {
core.info(
`Using toolchain directive version ${version} as written (latest-patch does not widen an explicit toolchain pin)`
);
} else {
const spec = installer.latestPatchSpec(version);
if (spec === version) {
core.info(
`Using version ${version} as written (latest-patch only widens exact major.minor.patch versions)`
);
} else {
if (
core.getInput('go-download-base-url') ||
process.env['GO_DOWNLOAD_BASE_URL']
) {
throw new Error(
`go-version-file-behavior: 'latest-patch' is not supported with a custom download base URL because version ranges cannot be resolved against it. Use the default 'exact' behavior.`
);
}
core.info(
`Using latest patch release satisfying ${spec} (version file specifies ${version})`
);
return {version: spec, latestPatchApplied: true};
}
}
}
} }
return version; return {version, latestPatchApplied: false};
} }
function setGoToolchain() { function setGoToolchain() {