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>
<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> <details>
<summary>Go Workspace Example</summary> <summary>Go Workspace Example</summary>
@ -280,6 +342,29 @@ with:
</details> </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` ### `install-mode`
(optional) (optional)
@ -288,6 +373,8 @@ The mode to install golangci-lint: it can be `binary`, `goinstall`, or `none`.
The default value is `binary`. 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> <details>
<summary>Example</summary> <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. - `goinstall`: the value can be v2.3.4, `latest`, or the hash of a commit.
- `none`: the value is ignored. - `none`: the value is ignored.
required: false 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: install-mode:
description: "The mode to install golangci-lint. It can be 'binary', 'goinstall', or 'none'." description: "The mode to install golangci-lint. It can be 'binary', 'goinstall', or 'none'."
default: "binary" 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. * @returns path to installed binary of golangci-lint.
*/ */
async function install() { async function install() {
const mode = core.getInput("install-mode").toLowerCase(); let mode = core.getInput("install-mode").toLowerCase();
if (mode === InstallMode.None) { if (mode === InstallMode.None) {
const binPath = await (0, which_1.default)("golangci-lint", { nothrow: true }); const binPath = await (0, which_1.default)("golangci-lint", { nothrow: true });
if (!binPath) { if (!binPath) {
@ -92679,6 +92679,11 @@ async function install() {
} }
return binPath; 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); const versionInfo = await (0, version_1.getVersion)(mode);
return await installBinary(versionInfo, mode); return await installBinary(versionInfo, mode);
} }
@ -93358,6 +93363,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.stringifyVersion = void 0; exports.stringifyVersion = void 0;
exports.isVersionFromGoMod = isVersionFromGoMod;
exports.getVersion = getVersion; exports.getVersion = getVersion;
const core = __importStar(__nccwpck_require__(7484)); const core = __importStar(__nccwpck_require__(7484));
const httpm = __importStar(__nccwpck_require__(4844)); const httpm = __importStar(__nccwpck_require__(4844));
@ -93412,8 +93418,9 @@ const isLessVersion = (a, b) => {
const getRequestedVersion = () => { const getRequestedVersion = () => {
let requestedVersion = core.getInput(`version`); let requestedVersion = core.getInput(`version`);
const workingDirectory = core.getInput(`working-directory`); const workingDirectory = core.getInput(`working-directory`);
let goMod = "go.mod"; const goModPath = core.getInput(`go-mod-path`);
if (workingDirectory) { let goMod = goModPath || "go.mod";
if (!path_1.default.isAbsolute(goMod) && workingDirectory) {
goMod = path_1.default.join(workingDirectory, goMod); goMod = path_1.default.join(workingDirectory, goMod);
} }
if (requestedVersion == "" && fs.existsSync(goMod)) { if (requestedVersion == "" && fs.existsSync(goMod)) {
@ -93451,10 +93458,27 @@ const fetchVersionMapping = async () => {
throw new Error(`failed to get action config: ${exc.message}`); 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) { async function getVersion(mode) {
core.info(`Finding needed golangci-lint version...`); core.info(`Finding needed golangci-lint version...`);
if (mode == install_1.InstallMode.GoInstall) { 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" }; return { TargetVersion: v ? v : "latest" };
} }
const reqVersion = getRequestedVersion(); 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. * @returns path to installed binary of golangci-lint.
*/ */
async function install() { async function install() {
const mode = core.getInput("install-mode").toLowerCase(); let mode = core.getInput("install-mode").toLowerCase();
if (mode === InstallMode.None) { if (mode === InstallMode.None) {
const binPath = await (0, which_1.default)("golangci-lint", { nothrow: true }); const binPath = await (0, which_1.default)("golangci-lint", { nothrow: true });
if (!binPath) { if (!binPath) {
@ -92679,6 +92679,11 @@ async function install() {
} }
return binPath; 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); const versionInfo = await (0, version_1.getVersion)(mode);
return await installBinary(versionInfo, mode); return await installBinary(versionInfo, mode);
} }
@ -93358,6 +93363,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
}; };
Object.defineProperty(exports, "__esModule", ({ value: true })); Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.stringifyVersion = void 0; exports.stringifyVersion = void 0;
exports.isVersionFromGoMod = isVersionFromGoMod;
exports.getVersion = getVersion; exports.getVersion = getVersion;
const core = __importStar(__nccwpck_require__(7484)); const core = __importStar(__nccwpck_require__(7484));
const httpm = __importStar(__nccwpck_require__(4844)); const httpm = __importStar(__nccwpck_require__(4844));
@ -93412,8 +93418,9 @@ const isLessVersion = (a, b) => {
const getRequestedVersion = () => { const getRequestedVersion = () => {
let requestedVersion = core.getInput(`version`); let requestedVersion = core.getInput(`version`);
const workingDirectory = core.getInput(`working-directory`); const workingDirectory = core.getInput(`working-directory`);
let goMod = "go.mod"; const goModPath = core.getInput(`go-mod-path`);
if (workingDirectory) { let goMod = goModPath || "go.mod";
if (!path_1.default.isAbsolute(goMod) && workingDirectory) {
goMod = path_1.default.join(workingDirectory, goMod); goMod = path_1.default.join(workingDirectory, goMod);
} }
if (requestedVersion == "" && fs.existsSync(goMod)) { if (requestedVersion == "" && fs.existsSync(goMod)) {
@ -93451,10 +93458,27 @@ const fetchVersionMapping = async () => {
throw new Error(`failed to get action config: ${exc.message}`); 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) { async function getVersion(mode) {
core.info(`Finding needed golangci-lint version...`); core.info(`Finding needed golangci-lint version...`);
if (mode == install_1.InstallMode.GoInstall) { 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" }; return { TargetVersion: v ? v : "latest" };
} }
const reqVersion = getRequestedVersion(); const reqVersion = getRequestedVersion();

View file

@ -6,7 +6,7 @@ import path from "path"
import { promisify } from "util" import { promisify } from "util"
import which from "which" import which from "which"
import { getVersion, VersionInfo } from "./version" import { getVersion, isVersionFromGoMod, VersionInfo } from "./version"
const execShellCommand = promisify(exec) const execShellCommand = promisify(exec)
@ -36,7 +36,7 @@ const printOutput = (res: ExecRes): void => {
* @returns path to installed binary of golangci-lint. * @returns path to installed binary of golangci-lint.
*/ */
export async function install(): Promise<string> { export async function install(): Promise<string> {
const mode = core.getInput("install-mode").toLowerCase() let mode = core.getInput("install-mode").toLowerCase()
if (mode === InstallMode.None) { if (mode === InstallMode.None) {
const binPath = await which("golangci-lint", { nothrow: true }) const binPath = await which("golangci-lint", { nothrow: true })
@ -46,6 +46,12 @@ export async function install(): Promise<string> {
return binPath 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) const versionInfo = await getVersion(<InstallMode>mode)
return await installBinary(versionInfo, <InstallMode>mode) return await installBinary(versionInfo, <InstallMode>mode)

View file

@ -68,9 +68,10 @@ const isLessVersion = (a: Version, b: Version): boolean => {
const getRequestedVersion = (): Version => { const getRequestedVersion = (): Version => {
let requestedVersion = core.getInput(`version`) let requestedVersion = core.getInput(`version`)
const workingDirectory = core.getInput(`working-directory`) const workingDirectory = core.getInput(`working-directory`)
const goModPath = core.getInput(`go-mod-path`)
let goMod = "go.mod" let goMod = goModPath || "go.mod"
if (workingDirectory) { if (!path.isAbsolute(goMod) && workingDirectory) {
goMod = path.join(workingDirectory, goMod) 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> { export async function getVersion(mode: InstallMode): Promise<VersionInfo> {
core.info(`Finding needed golangci-lint version...`) core.info(`Finding needed golangci-lint version...`)
if (mode == InstallMode.GoInstall) { 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" } return { TargetVersion: v ? v : "latest" }
} }