From 71b6f8926f920b5fb0d95ed94db07f8aa28c16c2 Mon Sep 17 00:00:00 2001 From: John Maguire Date: Tue, 18 Aug 2026 11:35:13 -0400 Subject: [PATCH] Add go-version-file-behavior input By default an exact version read from go-version-file is used as written, so a "go 1.22.0" directive pins CI to the oldest patch release of the minor even when newer patches with security fixes exist. Dependencies can force such an exact version into go.mod. Setting go-version-file-behavior to latest-patch widens an exact major.minor.patch version into a ~X.Y.Z range, resolving the newest available patch release of the same minor while keeping the file's version as a floor. Bare minors and prereleases pass through unchanged. The default behavior (exact) is unchanged. --- README.md | 5 +++ __tests__/setup-go.test.ts | 92 ++++++++++++++++++++++++++++++++++++++ action.yml | 3 ++ dist/setup/index.js | 22 +++++++++ docs/advanced-usage.md | 19 ++++++++ src/installer.ts | 12 +++++ src/main.ts | 15 +++++++ 7 files changed, 168 insertions(+) diff --git a/README.md b/README.md index ecee210..2331926 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/__tests__/setup-go.test.ts b/__tests__/setup-go.test.ts index 3884a38..dccde7f 100644 --- a/__tests__/setup-go.test.ts +++ b/__tests__/setup-go.test.ts @@ -1089,6 +1089,98 @@ 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( + `Acquiring 1.12.17 from ${expectedUrl}` + ); + }); + + 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('Setup go version spec 1.14'); + }); + + 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.each([ + ['1.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'; diff --git a/action.yml b/action.yml index 74a4e7e..f5b85e6 100644 --- a/action.yml +++ b/action.yml @@ -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). 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 diff --git a/dist/setup/index.js b/dist/setup/index.js index 07ab08e..fd9ee75 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -43581,6 +43581,17 @@ function parseGoVersionFile(versionFilePath) { } return contents.trim(); } +// 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: 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) { + if (!/^\d+\.\d+\.\d+$/.test(version)) { + return version; + } + return `~${version}`; +} async function resolveStableVersionDist(versionSpec, arch) { const archFilter = getArch(arch); const platFilter = getPlatform(); @@ -100360,6 +100371,17 @@ function resolveVersionInput() { throw new Error(`The specified go version file at: ${versionFilePath} does not exist`); } version = parseGoVersionFile(versionFilePath); + const behavior = getInput('go-version-file-behavior') || 'exact'; + if (behavior === 'latest-patch') { + const spec = latestPatchSpec(version); + if (spec !== version) { + core_info(`Using latest patch release satisfying ${spec} (version file specifies ${version})`); + version = spec; + } + } + else if (behavior !== 'exact') { + throw new Error(`Invalid go-version-file-behavior: '${behavior}'. Supported values: 'exact', 'latest-patch'`); + } } return version; } diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index e0932f3..268ac58 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -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) @@ -212,6 +213,24 @@ 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 +``` + +Versions without a patch component (e.g., `go 1.22`) already resolve to the latest available patch release, and prerelease versions (e.g., `go1.22rc1` from a `toolchain` directive) are always used as written, so `latest-patch` leaves both unchanged. As with any version range, the resolved patch release depends on what is available in the runner's tool cache and the versions manifest. + ## Check latest version The `check-latest` flag defaults to `false`. Use the default or set `check-latest` to `false` if you prefer stability diff --git a/src/installer.ts b/src/installer.ts index 1354d7b..92ea632 100644 --- a/src/installer.ts +++ b/src/installer.ts @@ -672,6 +672,18 @@ export function parseGoVersionFile(versionFilePath: string): string { return contents.trim(); } +// 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: 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 { + if (!/^\d+\.\d+\.\d+$/.test(version)) { + return version; + } + return `~${version}`; +} + async function resolveStableVersionDist( versionSpec: string, arch: Architecture diff --git a/src/main.ts b/src/main.ts index 98aee1c..f68d057 100644 --- a/src/main.ts +++ b/src/main.ts @@ -173,6 +173,21 @@ function resolveVersionInput(): string { ); } version = installer.parseGoVersionFile(versionFilePath); + + const behavior = core.getInput('go-version-file-behavior') || 'exact'; + if (behavior === 'latest-patch') { + const spec = installer.latestPatchSpec(version); + if (spec !== version) { + core.info( + `Using latest patch release satisfying ${spec} (version file specifies ${version})` + ); + version = spec; + } + } else if (behavior !== 'exact') { + throw new Error( + `Invalid go-version-file-behavior: '${behavior}'. Supported values: 'exact', 'latest-patch'` + ); + } } return version;