Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ See [action.yml](action.yml).
# Note: if both go-version and go-version-file are provided, go-version takes precedence.
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
# Default: false
check-latest: false
Expand Down
180 changes: 180 additions & 0 deletions __tests__/setup-go.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
const goVersions = ['1.22.0', '1.21rc2', '1.18'];
const placeholderVersion = '1.19';
Expand Down
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
go-version-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:
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
Expand Down
76 changes: 65 additions & 11 deletions dist/setup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
// the download URL directly, avoiding a guaranteed-404 HTTP request.
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;
const osPlat = external_os_default().platform();
const customBaseUrl = goDownloadBaseUrl?.replace(/\/+$/, '');
Expand Down Expand Up @@ -43493,6 +43493,11 @@ async function getGo(versionSpec, checkLatest, auth, arch = external_os_default(
versionSpec = resolvedVersion;
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 {
core_info(`Failed to resolve version ${versionSpec} from manifest`);
}
Expand Down Expand Up @@ -43865,18 +43870,37 @@ function parseGoVersionFile(versionFilePath) {
// 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];
return { version: matchToolchain[1], fromToolchainDirective: true };
}
}
// go directive: https://go.dev/ref/mod#go-mod-file-go
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') {
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) {
const archFilter = getArch(arch);
Expand Down Expand Up @@ -100573,7 +100597,7 @@ async function run() {
// 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();
const { version: versionSpec, latestPatchApplied } = resolveVersionInput();
setGoToolchain();
const cache = getBooleanInput('cache');
let arch = getInput('architecture');
Expand All @@ -100586,14 +100610,20 @@ async function run() {
core_info(`Setup go version spec ${versionSpec}`);
const token = getInput('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') ||
process.env['GO_DOWNLOAD_BASE_URL'] ||
undefined;
if (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));
addPath(external_path_default().join(installDir, 'bin'));
core_info('Added go to the path');
Expand Down Expand Up @@ -100678,19 +100708,43 @@ function parseGoVersion(versionString) {
function resolveVersionInput() {
let version = getInput('go-version');
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) {
warning('Both go-version and go-version-file inputs are specified, only go-version will be used');
}
if (version) {
return version;
return { version, latestPatchApplied: false };
}
if (versionFilePath) {
if (!external_fs_default().existsSync(versionFilePath)) {
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() {
// docs: https://go.dev/doc/toolchain
Expand Down
32 changes: 32 additions & 0 deletions docs/advanced-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- [Specifying a go version](advanced-usage.md#specifying-a-go-version)
- [Matrix testing](advanced-usage.md#matrix-testing)
- [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)
- [Caching](advanced-usage.md#caching)
- [Caching in monorepos](advanced-usage.md#caching-in-monorepos)
Expand Down Expand Up @@ -212,6 +213,37 @@ steps:
- 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

The `check-latest` flag defaults to `false`. Use the default or set `check-latest` to `false` if you prefer stability
Expand Down
Loading