feat: add support for reading golangci-lint version from go.mod

Implements automatic version detection from go.mod files to keep
golangci-lint versions synchronized between the module and CI.

- Add new `go-mod-path` input parameter to specify custom go.mod location
- Automatically detect and use version from go.mod when no explicit version provided
- Force `goinstall` mode when using go.mod versions for compatibility
- Support both default go.mod and custom paths (e.g., tools/go.mod)

This eliminates the need to manually update versions in multiple places
and ensures CI uses the same golangci-lint version as the project.

Fixes #1268

Signed-off-by: Pedro Tanaka <pedro.tanaka@shopify.com>
This commit is contained in:
Pedro Tanaka 2025-08-21 12:54:47 +02:00
parent 9511564208
commit 17e961267d
No known key found for this signature in database
GPG key ID: D0D8389DA4EE060B
6 changed files with 183 additions and 13 deletions

View file

@ -105,6 +105,68 @@ You will also likely need to add the following `.gitattributes` file to ensure t
</details>
<details>
<summary>Using go.mod Version Example</summary>
If you have golangci-lint as a dependency in your go.mod file, you can use that version automatically:
```yaml
name: golangci-lint
on:
push:
branches:
- main
- master
pull_request:
permissions:
contents: read
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
# No version specified - will read from go.mod automatically
```
Or if you have a separate tools module:
```yaml
name: golangci-lint
on:
push:
branches:
- main
- master
pull_request:
permissions:
contents: read
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
go-mod-path: tools/go.mod
```
</details>
<details>
<summary>Go Workspace Example</summary>
@ -280,6 +342,29 @@ with:
</details>
### `go-mod-path`
(optional)
Path to a go.mod file to read the golangci-lint version from.
If specified and `version` is not set, the action will parse the go.mod file to determine the golangci-lint version.
Default is "go.mod" in the working directory.
**Note:** When a version is read from go.mod, it automatically uses `goinstall` install-mode to ensure compatibility with your module's dependencies.
<details>
<summary>Example</summary>
```yml
uses: golangci/golangci-lint-action@v8
with:
go-mod-path: tools/go.mod
# ...
```
</details>
### `install-mode`
(optional)
@ -288,6 +373,8 @@ The mode to install golangci-lint: it can be `binary`, `goinstall`, or `none`.
The default value is `binary`.
**Note:** When using `go-mod-path` to read the version from a go.mod file, the install-mode is automatically set to `goinstall`.
<details>
<summary>Example</summary>

View file

@ -11,6 +11,13 @@ inputs:
- `goinstall`: the value can be v2.3.4, `latest`, or the hash of a commit.
- `none`: the value is ignored.
required: false
go-mod-path:
description: |
Path to a go.mod file to read the golangci-lint version from.
If specified and version is not set, the action will parse the go.mod file
to determine the golangci-lint version. Default is "go.mod" in the working directory.
Note: When a version is read from go.mod, it automatically uses `goinstall` install-mode.
required: false
install-mode:
description: "The mode to install golangci-lint. It can be 'binary', 'goinstall', or 'none'."
default: "binary"

32
dist/post_run/index.js generated vendored
View file

@ -92671,7 +92671,7 @@ const printOutput = (res) => {
* @returns path to installed binary of golangci-lint.
*/
async function install() {
const mode = core.getInput("install-mode").toLowerCase();
let mode = core.getInput("install-mode").toLowerCase();
if (mode === InstallMode.None) {
const binPath = await (0, which_1.default)("golangci-lint", { nothrow: true });
if (!binPath) {
@ -92679,6 +92679,11 @@ async function install() {
}
return binPath;
}
// If version is being read from go.mod, force goinstall mode
if ((0, version_1.isVersionFromGoMod)()) {
core.info(`Version detected from go.mod file, using goinstall mode`);
mode = InstallMode.GoInstall;
}
const versionInfo = await (0, version_1.getVersion)(mode);
return await installBinary(versionInfo, mode);
}
@ -93358,6 +93363,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.stringifyVersion = void 0;
exports.isVersionFromGoMod = isVersionFromGoMod;
exports.getVersion = getVersion;
const core = __importStar(__nccwpck_require__(7484));
const httpm = __importStar(__nccwpck_require__(4844));
@ -93412,8 +93418,9 @@ const isLessVersion = (a, b) => {
const getRequestedVersion = () => {
let requestedVersion = core.getInput(`version`);
const workingDirectory = core.getInput(`working-directory`);
let goMod = "go.mod";
if (workingDirectory) {
const goModPath = core.getInput(`go-mod-path`);
let goMod = goModPath || "go.mod";
if (!path_1.default.isAbsolute(goMod) && workingDirectory) {
goMod = path_1.default.join(workingDirectory, goMod);
}
if (requestedVersion == "" && fs.existsSync(goMod)) {
@ -93451,10 +93458,27 @@ const fetchVersionMapping = async () => {
throw new Error(`failed to get action config: ${exc.message}`);
}
};
function isVersionFromGoMod() {
const requestedVersion = core.getInput(`version`);
const goModPath = core.getInput(`go-mod-path`);
const workingDirectory = core.getInput(`working-directory`);
let goMod = goModPath || "go.mod";
if (!path_1.default.isAbsolute(goMod) && workingDirectory) {
goMod = path_1.default.join(workingDirectory, goMod);
}
return requestedVersion == "" && fs.existsSync(goMod);
}
async function getVersion(mode) {
core.info(`Finding needed golangci-lint version...`);
if (mode == install_1.InstallMode.GoInstall) {
const v = core.getInput(`version`);
let v = core.getInput(`version`);
// If no explicit version is provided, check go.mod
if (!v && isVersionFromGoMod()) {
const reqVersion = getRequestedVersion();
if (reqVersion) {
v = (0, exports.stringifyVersion)(reqVersion);
}
}
return { TargetVersion: v ? v : "latest" };
}
const reqVersion = getRequestedVersion();

32
dist/run/index.js generated vendored
View file

@ -92671,7 +92671,7 @@ const printOutput = (res) => {
* @returns path to installed binary of golangci-lint.
*/
async function install() {
const mode = core.getInput("install-mode").toLowerCase();
let mode = core.getInput("install-mode").toLowerCase();
if (mode === InstallMode.None) {
const binPath = await (0, which_1.default)("golangci-lint", { nothrow: true });
if (!binPath) {
@ -92679,6 +92679,11 @@ async function install() {
}
return binPath;
}
// If version is being read from go.mod, force goinstall mode
if ((0, version_1.isVersionFromGoMod)()) {
core.info(`Version detected from go.mod file, using goinstall mode`);
mode = InstallMode.GoInstall;
}
const versionInfo = await (0, version_1.getVersion)(mode);
return await installBinary(versionInfo, mode);
}
@ -93358,6 +93363,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.stringifyVersion = void 0;
exports.isVersionFromGoMod = isVersionFromGoMod;
exports.getVersion = getVersion;
const core = __importStar(__nccwpck_require__(7484));
const httpm = __importStar(__nccwpck_require__(4844));
@ -93412,8 +93418,9 @@ const isLessVersion = (a, b) => {
const getRequestedVersion = () => {
let requestedVersion = core.getInput(`version`);
const workingDirectory = core.getInput(`working-directory`);
let goMod = "go.mod";
if (workingDirectory) {
const goModPath = core.getInput(`go-mod-path`);
let goMod = goModPath || "go.mod";
if (!path_1.default.isAbsolute(goMod) && workingDirectory) {
goMod = path_1.default.join(workingDirectory, goMod);
}
if (requestedVersion == "" && fs.existsSync(goMod)) {
@ -93451,10 +93458,27 @@ const fetchVersionMapping = async () => {
throw new Error(`failed to get action config: ${exc.message}`);
}
};
function isVersionFromGoMod() {
const requestedVersion = core.getInput(`version`);
const goModPath = core.getInput(`go-mod-path`);
const workingDirectory = core.getInput(`working-directory`);
let goMod = goModPath || "go.mod";
if (!path_1.default.isAbsolute(goMod) && workingDirectory) {
goMod = path_1.default.join(workingDirectory, goMod);
}
return requestedVersion == "" && fs.existsSync(goMod);
}
async function getVersion(mode) {
core.info(`Finding needed golangci-lint version...`);
if (mode == install_1.InstallMode.GoInstall) {
const v = core.getInput(`version`);
let v = core.getInput(`version`);
// If no explicit version is provided, check go.mod
if (!v && isVersionFromGoMod()) {
const reqVersion = getRequestedVersion();
if (reqVersion) {
v = (0, exports.stringifyVersion)(reqVersion);
}
}
return { TargetVersion: v ? v : "latest" };
}
const reqVersion = getRequestedVersion();

View file

@ -6,7 +6,7 @@ import path from "path"
import { promisify } from "util"
import which from "which"
import { getVersion, VersionInfo } from "./version"
import { getVersion, isVersionFromGoMod, VersionInfo } from "./version"
const execShellCommand = promisify(exec)
@ -36,7 +36,7 @@ const printOutput = (res: ExecRes): void => {
* @returns path to installed binary of golangci-lint.
*/
export async function install(): Promise<string> {
const mode = core.getInput("install-mode").toLowerCase()
let mode = core.getInput("install-mode").toLowerCase()
if (mode === InstallMode.None) {
const binPath = await which("golangci-lint", { nothrow: true })
@ -46,6 +46,12 @@ export async function install(): Promise<string> {
return binPath
}
// If version is being read from go.mod, force goinstall mode
if (isVersionFromGoMod()) {
core.info(`Version detected from go.mod file, using goinstall mode`)
mode = InstallMode.GoInstall
}
const versionInfo = await getVersion(<InstallMode>mode)
return await installBinary(versionInfo, <InstallMode>mode)

View file

@ -68,9 +68,10 @@ const isLessVersion = (a: Version, b: Version): boolean => {
const getRequestedVersion = (): Version => {
let requestedVersion = core.getInput(`version`)
const workingDirectory = core.getInput(`working-directory`)
const goModPath = core.getInput(`go-mod-path`)
let goMod = "go.mod"
if (workingDirectory) {
let goMod = goModPath || "go.mod"
if (!path.isAbsolute(goMod) && workingDirectory) {
goMod = path.join(workingDirectory, goMod)
}
@ -129,11 +130,32 @@ const fetchVersionMapping = async (): Promise<VersionMapping> => {
}
}
export function isVersionFromGoMod(): boolean {
const requestedVersion = core.getInput(`version`)
const goModPath = core.getInput(`go-mod-path`)
const workingDirectory = core.getInput(`working-directory`)
let goMod = goModPath || "go.mod"
if (!path.isAbsolute(goMod) && workingDirectory) {
goMod = path.join(workingDirectory, goMod)
}
return requestedVersion == "" && fs.existsSync(goMod)
}
export async function getVersion(mode: InstallMode): Promise<VersionInfo> {
core.info(`Finding needed golangci-lint version...`)
if (mode == InstallMode.GoInstall) {
const v: string = core.getInput(`version`)
let v: string = core.getInput(`version`)
// If no explicit version is provided, check go.mod
if (!v && isVersionFromGoMod()) {
const reqVersion = getRequestedVersion()
if (reqVersion) {
v = stringifyVersion(reqVersion)
}
}
return { TargetVersion: v ? v : "latest" }
}