feat: support Module Plugin System (#1306)

This commit is contained in:
Ludovic Fernandez 2025-11-08 00:39:57 +01:00 committed by GitHub
parent a66d26a465
commit 043b1b8d1c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 20734 additions and 3090 deletions

View file

@ -114,3 +114,35 @@ jobs:
with: with:
working-directory: ${{ matrix.wd }} working-directory: ${{ matrix.wd }}
args: --timeout=5m --issues-exit-code=0 ./... args: --timeout=5m --issues-exit-code=0 ./...
test-plugins: # make sure the action works on a clean machine with plugins
needs: [ build ]
strategy:
matrix:
os:
- ubuntu-latest
- ubuntu-22.04-arm
- macos-latest
- windows-latest
version:
- ""
- "latest"
- "v2.5"
- "v2.5.0"
runs-on: ${{ matrix.os }}
permissions:
contents: read
pull-requests: read
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v6
with:
node-version: 24.x
- uses: actions/setup-go@v6
with:
go-version: oldstable
- uses: ./
with:
version: ${{ matrix.version }}
working-directory: sample-plugins
args: --timeout=5m --issues-exit-code=0 ./...

View file

@ -548,6 +548,13 @@ permissions:
For annotations to work, use the default format output (`text`) and either use [`actions/setup-go`](https://github.com/actions/setup-go) in the job or enable the internal [problem matchers](#problem-matchers). For annotations to work, use the default format output (`text`) and either use [`actions/setup-go`](https://github.com/actions/setup-go) in the job or enable the internal [problem matchers](#problem-matchers).
## Module Plugin System
The action will automatically detect the custom build configuration file `.custom-gcl.yml`,
build and run the custom version of golangci-lint.
For more information, see [module plugin system](https://golangci-lint.run/docs/plugins/module-plugins/).
## Performance ## Performance
The action was implemented with performance in mind: The action was implemented with performance in mind:

11803
dist/post_run/index.js generated vendored

File diff suppressed because one or more lines are too long

11803
dist/run/index.js generated vendored

File diff suppressed because one or more lines are too long

15
package-lock.json generated
View file

@ -21,7 +21,8 @@
"@types/tmp": "^0.2.6", "@types/tmp": "^0.2.6",
"@types/which": "^3.0.4", "@types/which": "^3.0.4",
"tmp": "^0.2.5", "tmp": "^0.2.5",
"which": "^5.0.0" "which": "^5.0.0",
"yaml": "^2.8.1"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.46.2", "@typescript-eslint/eslint-plugin": "^8.46.2",
@ -4408,6 +4409,18 @@
"node": ">=4.0" "node": ">=4.0"
} }
}, },
"node_modules/yaml": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
},
"node_modules/yocto-queue": { "node_modules/yocto-queue": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View file

@ -39,7 +39,8 @@
"@types/tmp": "^0.2.6", "@types/tmp": "^0.2.6",
"@types/which": "^3.0.4", "@types/which": "^3.0.4",
"tmp": "^0.2.5", "tmp": "^0.2.5",
"which": "^5.0.0" "which": "^5.0.0",
"yaml": "^2.8.1"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.46.2", "@typescript-eslint/eslint-plugin": "^8.46.2",

View file

@ -0,0 +1,7 @@
version: v2.6.1
name: custom-golangci-lint
#destination: ./zzz/path/
plugins:
- module: 'github.com/golangci/example-plugin-module-linter'
version: v0.1.0

1
sample-plugins/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/custom-golangci-lint

View file

@ -0,0 +1,25 @@
version: "2"
linters:
default: none
enable:
- example
settings:
custom:
example:
type: module
# Description is optional
description: The description of the linter. This is optional, but shows up when running `golangci-lint linters`.
# Original-url is optional, and is only used for documentation purposes.
original-url: github.com/golangci/example-plugin-module-linter
settings:
one: Foo
two:
- name: Bar
three:
name: Bar
issues:
max-issues-per-linter: 0
max-same-issues: 0

3
sample-plugins/go.mod Normal file
View file

@ -0,0 +1,3 @@
module github.com/golangci/sample
go 1.24.0

22
sample-plugins/sample.go Normal file
View file

@ -0,0 +1,22 @@
// Package sample is used as test input for golangci action.
package sample
// comment without a to do
func SomeFunc1() {
_ = 1 + 1
}
// TODO: do something // want "TODO comment has no author"
func SomeFunc2() {
_ = 1 + 2
}
// TODO(): do something // want "TODO comment has no author"
func SomeFunc3() {
_ = 1 + 3
}
// TODO(dbraley): Do something with the value
func SomeFunc4() {
_ = 1 + 4
}

90
src/plugins.ts Normal file
View file

@ -0,0 +1,90 @@
import * as core from "@actions/core"
import { exec, ExecOptionsWithStringEncoding } from "child_process"
import * as fs from "fs"
import * as path from "path"
import { promisify } from "util"
import YAML from "yaml"
const execShellCommand = promisify(exec)
type ExecRes = {
stdout: string
stderr: string
}
const printOutput = (res: ExecRes): void => {
if (res.stdout) {
core.info(res.stdout)
}
if (res.stderr) {
core.info(res.stderr)
}
}
export async function install(binPath: string): Promise<string> {
let rootDir = core.getInput(`working-directory`)
if (rootDir) {
if (!fs.existsSync(rootDir) || !fs.lstatSync(rootDir).isDirectory()) {
throw new Error(`working-directory (${rootDir}) was not a path`)
}
rootDir = path.resolve(rootDir)
} else {
rootDir = process.cwd()
}
const configFile = [".custom-gcl.yml", ".custom-gcl.yaml", ".custom-gcl.json"]
.map((v) => path.join(rootDir, v))
.find((filePath) => fs.existsSync(filePath))
if (!configFile || configFile === "") {
return ""
}
core.info(`Found configuration for the plugin module system : ${configFile}`)
core.info(`Building and installing custom golangci-lint binary...`)
const startedAt = Date.now()
const config = YAML.parse(fs.readFileSync(configFile, "utf-8"))
const v: string = core.getInput(`version`)
if (v !== "" && config.version !== v) {
core.warning(
`The golangci-lint version (${config.version}) defined inside in ${configFile} does not match the version defined in the action (${v})`
)
}
if (!config.destination) {
config.destination = "."
}
if (!config.name) {
config.name = "custom-gcl"
}
if (!fs.existsSync(config.destination)) {
core.info(`Creating destination directory: ${config.destination}`)
fs.mkdirSync(config.destination, { recursive: true })
}
const cmd = `${binPath} custom`
core.info(`Running [${cmd}] in [${rootDir}] ...`)
try {
const options: ExecOptionsWithStringEncoding = {
cwd: rootDir,
}
const res = await execShellCommand(cmd, options)
printOutput(res)
core.info(`Built custom golangci-lint binary in ${Date.now() - startedAt}ms`)
return path.join(rootDir, config.destination, config.name)
} catch (exc) {
throw new Error(`Failed to build custom golangci-lint binary: ${exc.message}`)
}
}

View file

@ -8,6 +8,7 @@ import { promisify } from "util"
import { restoreCache, saveCache } from "./cache" import { restoreCache, saveCache } from "./cache"
import { install } from "./install" import { install } from "./install"
import { fetchPatch, isOnlyNewIssues } from "./patch" import { fetchPatch, isOnlyNewIssues } from "./patch"
import * as plugins from "./plugins"
const execShellCommand = promisify(exec) const execShellCommand = promisify(exec)
@ -22,7 +23,13 @@ async function prepareEnv(installOnly: boolean): Promise<Env> {
// Prepare cache, lint and go in parallel. // Prepare cache, lint and go in parallel.
await restoreCache() await restoreCache()
const binPath = await install() let binPath = await install()
// Build custom golangci-lint if needed.
const customBinPath = await plugins.install(binPath)
if (customBinPath !== "") {
binPath = customBinPath
}
if (installOnly) { if (installOnly) {
return { binPath, patchPath: `` } return { binPath, patchPath: `` }
@ -203,9 +210,7 @@ export async function run(): Promise<void> {
try { try {
const installOnly = core.getBooleanInput(`install-only`, { required: true }) const installOnly = core.getBooleanInput(`install-only`, { required: true })
const { binPath, patchPath } = await core.group(`prepare environment`, () => { const { binPath, patchPath } = await core.group(`prepare environment`, () => prepareEnv(installOnly))
return prepareEnv(installOnly)
})
core.addPath(path.dirname(binPath)) core.addPath(path.dirname(binPath))