diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9d465bf68e..ef89553b2f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -32,6 +32,15 @@
4. Build: `npm run build`
5. Run `npm run local` after any change to test it
+### Testing custom plugins
+
+To test the custom plugin support:
+
+1. Create a `.custom-gcl.yml` file in one of the sample directories (e.g., `sample-go-mod/.custom-gcl.yml`)
+2. Add a plugin configuration following the [golangci-lint plugin documentation](https://golangci-lint.run/plugins/module-plugins/)
+3. Update the `.golangci.yml` file to enable the custom linter
+4. Run the action and verify that it builds and uses the custom binary
+
### Releases
```bash
diff --git a/README.md b/README.md
index 3d0cdff30b..ca53fd2c0e 100644
--- a/README.md
+++ b/README.md
@@ -243,6 +243,75 @@ You will also likely need to add the following `.gitattributes` file to ensure t
+## Custom Plugins Support
+
+This action supports [golangci-lint's module plugin system](https://golangci-lint.run/plugins/module-plugins/).
+
+To use custom plugins:
+
+1. Create a `.custom-gcl.yml` (or `.custom-gcl.yaml` or `.custom-gcl.json`) file in your repository root or working directory
+2. Define your plugins in this file following the [golangci-lint plugin documentation](https://golangci-lint.run/plugins/module-plugins/)
+3. Configure your plugins in your `.golangci.yml` configuration file
+4. The action will automatically:
+ - Detect the custom plugin configuration file
+ - Build a custom golangci-lint binary with `golangci-lint custom`
+ - Use the custom binary for linting
+ - Cache the custom binary for faster subsequent runs
+
+
+Example with nilaway plugin
+
+Create a `.custom-gcl.yml` file:
+
+```yaml
+version: v2.5.0
+name: custom-gcl
+plugins:
+ - module: 'github.com/uber-go/nilaway'
+ import: 'github.com/uber-go/nilaway/golangci-lint-plugin'
+ version: v0.1.0
+```
+
+Then configure it in your `.golangci.yml`:
+
+```yaml
+version: "2"
+
+linters-settings:
+ custom:
+ nilaway:
+ path: github.com/uber-go/nilaway/golangci-lint-plugin.New
+ description: Nilaway is a static analysis tool that detects nil panics
+ original-url: github.com/uber-go/nilaway
+
+linters:
+ enable:
+ - nilaway
+```
+
+Your workflow file remains unchanged:
+
+```yaml
+- name: golangci-lint
+ uses: golangci/golangci-lint-action@v8
+ with:
+ version: v2.5
+```
+
+
+
+### Caching with Custom Plugins
+
+The action automatically handles caching for custom plugins:
+- The cache key includes the hash of your `.custom-gcl.yml` file
+- The custom binary is cached alongside the golangci-lint cache
+- The cache is invalidated when:
+ - The `.custom-gcl.yml` file changes
+ - The golangci-lint version changes
+ - The cache invalidation interval expires (default: 7 days)
+
+**Important**: Always use specific versions for your plugins (e.g., `v0.1.0`) instead of `latest` to ensure proper cache invalidation and reproducible builds.
+
## Compatibility
* `v8.0.0` works with `golangci-lint` version >= `v2.1.0`
diff --git a/dist/post_run/index.js b/dist/post_run/index.js
index 99223c1fe2..a7549b75f6 100644
--- a/dist/post_run/index.js
+++ b/dist/post_run/index.js
@@ -93588,6 +93588,7 @@ const crypto = __importStar(__nccwpck_require__(6982));
const fs = __importStar(__nccwpck_require__(9896));
const path_1 = __importDefault(__nccwpck_require__(6928));
const constants_1 = __nccwpck_require__(7242);
+const custom_1 = __nccwpck_require__(8000);
const utils = __importStar(__nccwpck_require__(8270));
function checksumFile(hashName, path) {
return new Promise((resolve, reject) => {
@@ -93625,6 +93626,16 @@ async function buildCacheKeys() {
const invalidationIntervalDays = parseInt(core.getInput(`cache-invalidation-interval`, { required: true }).trim());
cacheKey += `${getIntervalKey(invalidationIntervalDays)}-`;
keys.push(cacheKey);
+ // Check for custom golangci-lint plugin configuration
+ const workDir = workingDirectory ? path_1.default.resolve(workingDirectory) : process.cwd();
+ const customConfigPath = (0, custom_1.findCustomConfigFile)(workDir);
+ if (customConfigPath) {
+ core.info(`Custom plugin configuration detected for cache: ${customConfigPath}`);
+ // Add hash of custom config file to cache key
+ const customConfigHash = (0, custom_1.hashCustomConfigFile)(customConfigPath);
+ cacheKey += `custom-${customConfigHash}-`;
+ keys.push(cacheKey);
+ }
// create path to go.mod prepending the workingDirectory if it exists
const goModPath = path_1.default.join(workingDirectory, `go.mod`);
core.info(`Checking for go.mod: ${goModPath}`);
@@ -93656,8 +93667,19 @@ async function restoreCache() {
return;
}
core.saveState(constants_1.State.CachePrimaryKey, primaryKey);
+ // Prepare cache paths
+ const cachePaths = [getLintCacheDir()];
+ // If custom plugin config exists, also cache the custom binary
+ const workingDirectory = core.getInput(`working-directory`);
+ const workDir = workingDirectory ? path_1.default.resolve(workingDirectory) : process.cwd();
+ const customConfigPath = (0, custom_1.findCustomConfigFile)(workDir);
+ if (customConfigPath) {
+ core.info(`Will cache custom golangci-lint binary from: ${workDir}`);
+ // Cache the working directory to include the custom binary
+ cachePaths.push(workDir);
+ }
try {
- const cacheKey = await cache.restoreCache([getLintCacheDir()], primaryKey, restoreKeys);
+ const cacheKey = await cache.restoreCache(cachePaths, primaryKey, restoreKeys);
if (!cacheKey) {
core.info(`Cache not found for input keys: ${[primaryKey, ...restoreKeys].join(", ")}`);
return;
@@ -93687,6 +93709,14 @@ async function saveCache() {
}
const startedAt = Date.now();
const cacheDirs = [getLintCacheDir()];
+ // If custom plugin config exists, also cache the custom binary
+ const workingDirectory = core.getInput(`working-directory`);
+ const workDir = workingDirectory ? path_1.default.resolve(workingDirectory) : process.cwd();
+ const customConfigPath = (0, custom_1.findCustomConfigFile)(workDir);
+ if (customConfigPath) {
+ core.info(`Will save custom golangci-lint binary from: ${workDir}`);
+ cacheDirs.push(workDir);
+ }
const primaryKey = core.getState(constants_1.State.CachePrimaryKey);
if (!primaryKey) {
utils.logWarning(`Error retrieving key from state.`);
@@ -93744,6 +93774,181 @@ var Events;
exports.RefKey = "GITHUB_REF";
+/***/ }),
+
+/***/ 8000:
+/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
+
+"use strict";
+
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ var desc = Object.getOwnPropertyDescriptor(m, k);
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+ desc = { enumerable: true, get: function() { return m[k]; } };
+ }
+ Object.defineProperty(o, k2, desc);
+}) : (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+ o["default"] = v;
+});
+var __importStar = (this && this.__importStar) || (function () {
+ var ownKeys = function(o) {
+ ownKeys = Object.getOwnPropertyNames || function (o) {
+ var ar = [];
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
+ return ar;
+ };
+ return ownKeys(o);
+ };
+ return function (mod) {
+ if (mod && mod.__esModule) return mod;
+ var result = {};
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
+ __setModuleDefault(result, mod);
+ return result;
+ };
+})();
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.findCustomConfigFile = findCustomConfigFile;
+exports.getCustomBinaryInfo = getCustomBinaryInfo;
+exports.buildCustomBinary = buildCustomBinary;
+exports.hashCustomConfigFile = hashCustomConfigFile;
+const core = __importStar(__nccwpck_require__(7484));
+const crypto = __importStar(__nccwpck_require__(6982));
+const child_process_1 = __nccwpck_require__(5317);
+const fs = __importStar(__nccwpck_require__(9896));
+const path = __importStar(__nccwpck_require__(6928));
+const util_1 = __nccwpck_require__(9023);
+const execShellCommand = (0, util_1.promisify)(child_process_1.exec);
+const printOutput = (res) => {
+ if (res.stdout) {
+ core.info(res.stdout);
+ }
+ if (res.stderr) {
+ core.info(res.stderr);
+ }
+};
+/**
+ * Check if a custom golangci-lint config file exists.
+ * The file can be .custom-gcl.yml, .custom-gcl.yaml, or .custom-gcl.json.
+ *
+ * @param workingDirectory the working directory to search in
+ * @returns the path to the custom config file, or null if not found
+ */
+function findCustomConfigFile(workingDirectory) {
+ const possibleFiles = [".custom-gcl.yml", ".custom-gcl.yaml", ".custom-gcl.json"];
+ for (const file of possibleFiles) {
+ const filePath = path.join(workingDirectory, file);
+ if (fs.existsSync(filePath)) {
+ core.info(`Found custom golangci-lint config: ${filePath}`);
+ return filePath;
+ }
+ }
+ return null;
+}
+/**
+ * Parse the custom config file to extract the binary name and destination.
+ *
+ * @param configPath path to the custom config file
+ * @returns object with name and destination (relative path)
+ */
+function getCustomBinaryInfo(configPath) {
+ try {
+ const content = fs.readFileSync(configPath, "utf-8");
+ let name = "custom-gcl";
+ let destination = ".";
+ // Try to parse as YAML/JSON
+ // Look for "name:" field in the config
+ const nameMatch = content.match(/^name:\s*["']?([^"'\s]+)["']?/m);
+ if (nameMatch && nameMatch[1]) {
+ name = nameMatch[1];
+ }
+ // Look for "destination:" field in the config
+ const destMatch = content.match(/^destination:\s*["']?([^"'\s]+)["']?/m);
+ if (destMatch && destMatch[1]) {
+ destination = destMatch[1];
+ }
+ // For JSON format
+ if (configPath.endsWith(".json")) {
+ try {
+ const json = JSON.parse(content);
+ if (json.name) {
+ name = json.name;
+ }
+ if (json.destination) {
+ destination = json.destination;
+ }
+ }
+ catch {
+ // Fallback to default
+ }
+ }
+ return { name, destination };
+ }
+ catch (err) {
+ core.warning(`Failed to parse custom config file: ${err}`);
+ }
+ // Default values
+ return { name: "custom-gcl", destination: "." };
+}
+/**
+ * Build a custom golangci-lint binary using `golangci-lint custom`.
+ *
+ * @param binPath path to the golangci-lint binary
+ * @param workingDirectory the working directory
+ * @param customConfigPath path to the custom config file
+ * @returns path to the built custom binary
+ */
+async function buildCustomBinary(binPath, workingDirectory, customConfigPath) {
+ core.info(`Building custom golangci-lint binary from ${customConfigPath}...`);
+ const startedAt = Date.now();
+ const cmdArgs = {
+ cwd: workingDirectory,
+ };
+ const binaryInfo = getCustomBinaryInfo(customConfigPath);
+ const destinationDir = path.join(workingDirectory, binaryInfo.destination);
+ const customBinaryPath = path.join(destinationDir, binaryInfo.name);
+ // Ensure destination directory exists
+ if (!fs.existsSync(destinationDir)) {
+ core.info(`Creating destination directory: ${destinationDir}`);
+ fs.mkdirSync(destinationDir, { recursive: true });
+ }
+ // Check if the binary already exists (from cache)
+ if (fs.existsSync(customBinaryPath)) {
+ core.info(`Custom binary already exists at ${customBinaryPath}`);
+ return customBinaryPath;
+ }
+ const cmd = `${binPath} custom`;
+ core.info(`Running [${cmd}] in [${workingDirectory}] ...`);
+ try {
+ const res = await execShellCommand(cmd, cmdArgs);
+ printOutput(res);
+ core.info(`Built custom golangci-lint binary in ${Date.now() - startedAt}ms`);
+ return customBinaryPath;
+ }
+ catch (exc) {
+ core.error(`Failed to build custom golangci-lint binary: ${exc}`);
+ throw new Error(`Failed to build custom golangci-lint binary: ${exc.message}`);
+ }
+}
+/**
+ * Calculate the hash of the custom config file.
+ *
+ * @param configPath path to the custom config file
+ * @returns SHA256 hash of the file content
+ */
+function hashCustomConfigFile(configPath) {
+ const content = fs.readFileSync(configPath, "utf-8");
+ return crypto.createHash("sha256").update(content).digest("hex");
+}
+
+
/***/ }),
/***/ 232:
@@ -94135,6 +94340,7 @@ const fs = __importStar(__nccwpck_require__(9896));
const path = __importStar(__nccwpck_require__(6928));
const util_1 = __nccwpck_require__(9023);
const cache_1 = __nccwpck_require__(7377);
+const custom_1 = __nccwpck_require__(8000);
const install_1 = __nccwpck_require__(232);
const patch_1 = __nccwpck_require__(7161);
const execShellCommand = (0, util_1.promisify)(child_process_1.exec);
@@ -94156,9 +94362,20 @@ const printOutput = (res) => {
}
};
async function runLint(binPath, patchPath) {
+ // Check for custom golangci-lint plugin configuration
+ const workingDirectory = core.getInput(`working-directory`);
+ const workDir = workingDirectory ? path.resolve(workingDirectory) : process.cwd();
+ const customConfigPath = (0, custom_1.findCustomConfigFile)(workDir);
+ let effectiveBinPath = binPath;
+ // If a custom config file exists, build the custom binary
+ if (customConfigPath) {
+ core.info(`Custom plugin configuration detected: ${customConfigPath}`);
+ effectiveBinPath = await (0, custom_1.buildCustomBinary)(binPath, workDir, customConfigPath);
+ core.info(`Using custom golangci-lint binary: ${effectiveBinPath}`);
+ }
const debug = core.getInput(`debug`);
if (debug.split(`,`).includes(`cache`)) {
- const res = await execShellCommand(`${binPath} cache status`);
+ const res = await execShellCommand(`${effectiveBinPath} cache status`);
printOutput(res);
}
const userArgs = core.getInput(`args`);
@@ -94214,7 +94431,6 @@ async function runLint(binPath, patchPath) {
}
}
const cmdArgs = {};
- const workingDirectory = core.getInput(`working-directory`);
if (workingDirectory) {
if (!fs.existsSync(workingDirectory) || !fs.lstatSync(workingDirectory).isDirectory()) {
throw new Error(`working-directory (${workingDirectory}) was not a path`);
@@ -94224,8 +94440,8 @@ async function runLint(binPath, patchPath) {
}
cmdArgs.cwd = path.resolve(workingDirectory);
}
- await runVerify(binPath, userArgsMap, cmdArgs);
- const cmd = `${binPath} run ${addedArgs.join(` `)} ${userArgs}`.trimEnd();
+ await runVerify(effectiveBinPath, userArgsMap, cmdArgs);
+ const cmd = `${effectiveBinPath} run ${addedArgs.join(` `)} ${userArgs}`.trimEnd();
core.info(`Running [${cmd}] in [${cmdArgs.cwd || process.cwd()}] ...`);
const startedAt = Date.now();
try {
diff --git a/dist/run/index.js b/dist/run/index.js
index c304e958d6..16036686cf 100644
--- a/dist/run/index.js
+++ b/dist/run/index.js
@@ -93588,6 +93588,7 @@ const crypto = __importStar(__nccwpck_require__(6982));
const fs = __importStar(__nccwpck_require__(9896));
const path_1 = __importDefault(__nccwpck_require__(6928));
const constants_1 = __nccwpck_require__(7242);
+const custom_1 = __nccwpck_require__(8000);
const utils = __importStar(__nccwpck_require__(8270));
function checksumFile(hashName, path) {
return new Promise((resolve, reject) => {
@@ -93625,6 +93626,16 @@ async function buildCacheKeys() {
const invalidationIntervalDays = parseInt(core.getInput(`cache-invalidation-interval`, { required: true }).trim());
cacheKey += `${getIntervalKey(invalidationIntervalDays)}-`;
keys.push(cacheKey);
+ // Check for custom golangci-lint plugin configuration
+ const workDir = workingDirectory ? path_1.default.resolve(workingDirectory) : process.cwd();
+ const customConfigPath = (0, custom_1.findCustomConfigFile)(workDir);
+ if (customConfigPath) {
+ core.info(`Custom plugin configuration detected for cache: ${customConfigPath}`);
+ // Add hash of custom config file to cache key
+ const customConfigHash = (0, custom_1.hashCustomConfigFile)(customConfigPath);
+ cacheKey += `custom-${customConfigHash}-`;
+ keys.push(cacheKey);
+ }
// create path to go.mod prepending the workingDirectory if it exists
const goModPath = path_1.default.join(workingDirectory, `go.mod`);
core.info(`Checking for go.mod: ${goModPath}`);
@@ -93656,8 +93667,19 @@ async function restoreCache() {
return;
}
core.saveState(constants_1.State.CachePrimaryKey, primaryKey);
+ // Prepare cache paths
+ const cachePaths = [getLintCacheDir()];
+ // If custom plugin config exists, also cache the custom binary
+ const workingDirectory = core.getInput(`working-directory`);
+ const workDir = workingDirectory ? path_1.default.resolve(workingDirectory) : process.cwd();
+ const customConfigPath = (0, custom_1.findCustomConfigFile)(workDir);
+ if (customConfigPath) {
+ core.info(`Will cache custom golangci-lint binary from: ${workDir}`);
+ // Cache the working directory to include the custom binary
+ cachePaths.push(workDir);
+ }
try {
- const cacheKey = await cache.restoreCache([getLintCacheDir()], primaryKey, restoreKeys);
+ const cacheKey = await cache.restoreCache(cachePaths, primaryKey, restoreKeys);
if (!cacheKey) {
core.info(`Cache not found for input keys: ${[primaryKey, ...restoreKeys].join(", ")}`);
return;
@@ -93687,6 +93709,14 @@ async function saveCache() {
}
const startedAt = Date.now();
const cacheDirs = [getLintCacheDir()];
+ // If custom plugin config exists, also cache the custom binary
+ const workingDirectory = core.getInput(`working-directory`);
+ const workDir = workingDirectory ? path_1.default.resolve(workingDirectory) : process.cwd();
+ const customConfigPath = (0, custom_1.findCustomConfigFile)(workDir);
+ if (customConfigPath) {
+ core.info(`Will save custom golangci-lint binary from: ${workDir}`);
+ cacheDirs.push(workDir);
+ }
const primaryKey = core.getState(constants_1.State.CachePrimaryKey);
if (!primaryKey) {
utils.logWarning(`Error retrieving key from state.`);
@@ -93744,6 +93774,181 @@ var Events;
exports.RefKey = "GITHUB_REF";
+/***/ }),
+
+/***/ 8000:
+/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
+
+"use strict";
+
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ var desc = Object.getOwnPropertyDescriptor(m, k);
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+ desc = { enumerable: true, get: function() { return m[k]; } };
+ }
+ Object.defineProperty(o, k2, desc);
+}) : (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+ o["default"] = v;
+});
+var __importStar = (this && this.__importStar) || (function () {
+ var ownKeys = function(o) {
+ ownKeys = Object.getOwnPropertyNames || function (o) {
+ var ar = [];
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
+ return ar;
+ };
+ return ownKeys(o);
+ };
+ return function (mod) {
+ if (mod && mod.__esModule) return mod;
+ var result = {};
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
+ __setModuleDefault(result, mod);
+ return result;
+ };
+})();
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.findCustomConfigFile = findCustomConfigFile;
+exports.getCustomBinaryInfo = getCustomBinaryInfo;
+exports.buildCustomBinary = buildCustomBinary;
+exports.hashCustomConfigFile = hashCustomConfigFile;
+const core = __importStar(__nccwpck_require__(7484));
+const crypto = __importStar(__nccwpck_require__(6982));
+const child_process_1 = __nccwpck_require__(5317);
+const fs = __importStar(__nccwpck_require__(9896));
+const path = __importStar(__nccwpck_require__(6928));
+const util_1 = __nccwpck_require__(9023);
+const execShellCommand = (0, util_1.promisify)(child_process_1.exec);
+const printOutput = (res) => {
+ if (res.stdout) {
+ core.info(res.stdout);
+ }
+ if (res.stderr) {
+ core.info(res.stderr);
+ }
+};
+/**
+ * Check if a custom golangci-lint config file exists.
+ * The file can be .custom-gcl.yml, .custom-gcl.yaml, or .custom-gcl.json.
+ *
+ * @param workingDirectory the working directory to search in
+ * @returns the path to the custom config file, or null if not found
+ */
+function findCustomConfigFile(workingDirectory) {
+ const possibleFiles = [".custom-gcl.yml", ".custom-gcl.yaml", ".custom-gcl.json"];
+ for (const file of possibleFiles) {
+ const filePath = path.join(workingDirectory, file);
+ if (fs.existsSync(filePath)) {
+ core.info(`Found custom golangci-lint config: ${filePath}`);
+ return filePath;
+ }
+ }
+ return null;
+}
+/**
+ * Parse the custom config file to extract the binary name and destination.
+ *
+ * @param configPath path to the custom config file
+ * @returns object with name and destination (relative path)
+ */
+function getCustomBinaryInfo(configPath) {
+ try {
+ const content = fs.readFileSync(configPath, "utf-8");
+ let name = "custom-gcl";
+ let destination = ".";
+ // Try to parse as YAML/JSON
+ // Look for "name:" field in the config
+ const nameMatch = content.match(/^name:\s*["']?([^"'\s]+)["']?/m);
+ if (nameMatch && nameMatch[1]) {
+ name = nameMatch[1];
+ }
+ // Look for "destination:" field in the config
+ const destMatch = content.match(/^destination:\s*["']?([^"'\s]+)["']?/m);
+ if (destMatch && destMatch[1]) {
+ destination = destMatch[1];
+ }
+ // For JSON format
+ if (configPath.endsWith(".json")) {
+ try {
+ const json = JSON.parse(content);
+ if (json.name) {
+ name = json.name;
+ }
+ if (json.destination) {
+ destination = json.destination;
+ }
+ }
+ catch {
+ // Fallback to default
+ }
+ }
+ return { name, destination };
+ }
+ catch (err) {
+ core.warning(`Failed to parse custom config file: ${err}`);
+ }
+ // Default values
+ return { name: "custom-gcl", destination: "." };
+}
+/**
+ * Build a custom golangci-lint binary using `golangci-lint custom`.
+ *
+ * @param binPath path to the golangci-lint binary
+ * @param workingDirectory the working directory
+ * @param customConfigPath path to the custom config file
+ * @returns path to the built custom binary
+ */
+async function buildCustomBinary(binPath, workingDirectory, customConfigPath) {
+ core.info(`Building custom golangci-lint binary from ${customConfigPath}...`);
+ const startedAt = Date.now();
+ const cmdArgs = {
+ cwd: workingDirectory,
+ };
+ const binaryInfo = getCustomBinaryInfo(customConfigPath);
+ const destinationDir = path.join(workingDirectory, binaryInfo.destination);
+ const customBinaryPath = path.join(destinationDir, binaryInfo.name);
+ // Ensure destination directory exists
+ if (!fs.existsSync(destinationDir)) {
+ core.info(`Creating destination directory: ${destinationDir}`);
+ fs.mkdirSync(destinationDir, { recursive: true });
+ }
+ // Check if the binary already exists (from cache)
+ if (fs.existsSync(customBinaryPath)) {
+ core.info(`Custom binary already exists at ${customBinaryPath}`);
+ return customBinaryPath;
+ }
+ const cmd = `${binPath} custom`;
+ core.info(`Running [${cmd}] in [${workingDirectory}] ...`);
+ try {
+ const res = await execShellCommand(cmd, cmdArgs);
+ printOutput(res);
+ core.info(`Built custom golangci-lint binary in ${Date.now() - startedAt}ms`);
+ return customBinaryPath;
+ }
+ catch (exc) {
+ core.error(`Failed to build custom golangci-lint binary: ${exc}`);
+ throw new Error(`Failed to build custom golangci-lint binary: ${exc.message}`);
+ }
+}
+/**
+ * Calculate the hash of the custom config file.
+ *
+ * @param configPath path to the custom config file
+ * @returns SHA256 hash of the file content
+ */
+function hashCustomConfigFile(configPath) {
+ const content = fs.readFileSync(configPath, "utf-8");
+ return crypto.createHash("sha256").update(content).digest("hex");
+}
+
+
/***/ }),
/***/ 232:
@@ -94135,6 +94340,7 @@ const fs = __importStar(__nccwpck_require__(9896));
const path = __importStar(__nccwpck_require__(6928));
const util_1 = __nccwpck_require__(9023);
const cache_1 = __nccwpck_require__(7377);
+const custom_1 = __nccwpck_require__(8000);
const install_1 = __nccwpck_require__(232);
const patch_1 = __nccwpck_require__(7161);
const execShellCommand = (0, util_1.promisify)(child_process_1.exec);
@@ -94156,9 +94362,20 @@ const printOutput = (res) => {
}
};
async function runLint(binPath, patchPath) {
+ // Check for custom golangci-lint plugin configuration
+ const workingDirectory = core.getInput(`working-directory`);
+ const workDir = workingDirectory ? path.resolve(workingDirectory) : process.cwd();
+ const customConfigPath = (0, custom_1.findCustomConfigFile)(workDir);
+ let effectiveBinPath = binPath;
+ // If a custom config file exists, build the custom binary
+ if (customConfigPath) {
+ core.info(`Custom plugin configuration detected: ${customConfigPath}`);
+ effectiveBinPath = await (0, custom_1.buildCustomBinary)(binPath, workDir, customConfigPath);
+ core.info(`Using custom golangci-lint binary: ${effectiveBinPath}`);
+ }
const debug = core.getInput(`debug`);
if (debug.split(`,`).includes(`cache`)) {
- const res = await execShellCommand(`${binPath} cache status`);
+ const res = await execShellCommand(`${effectiveBinPath} cache status`);
printOutput(res);
}
const userArgs = core.getInput(`args`);
@@ -94214,7 +94431,6 @@ async function runLint(binPath, patchPath) {
}
}
const cmdArgs = {};
- const workingDirectory = core.getInput(`working-directory`);
if (workingDirectory) {
if (!fs.existsSync(workingDirectory) || !fs.lstatSync(workingDirectory).isDirectory()) {
throw new Error(`working-directory (${workingDirectory}) was not a path`);
@@ -94224,8 +94440,8 @@ async function runLint(binPath, patchPath) {
}
cmdArgs.cwd = path.resolve(workingDirectory);
}
- await runVerify(binPath, userArgsMap, cmdArgs);
- const cmd = `${binPath} run ${addedArgs.join(` `)} ${userArgs}`.trimEnd();
+ await runVerify(effectiveBinPath, userArgsMap, cmdArgs);
+ const cmd = `${effectiveBinPath} run ${addedArgs.join(` `)} ${userArgs}`.trimEnd();
core.info(`Running [${cmd}] in [${cmdArgs.cwd || process.cwd()}] ...`);
const startedAt = Date.now();
try {
diff --git a/src/cache.ts b/src/cache.ts
index 57b817e9f6..0119ef44e5 100644
--- a/src/cache.ts
+++ b/src/cache.ts
@@ -5,6 +5,7 @@ import * as fs from "fs"
import path from "path"
import { Events, State } from "./constants"
+import { findCustomConfigFile, hashCustomConfigFile } from "./custom"
import * as utils from "./utils/actionUtils"
function checksumFile(hashName: string, path: string): Promise {
@@ -56,6 +57,18 @@ async function buildCacheKeys(): Promise {
keys.push(cacheKey)
+ // Check for custom golangci-lint plugin configuration
+ const workDir = workingDirectory ? path.resolve(workingDirectory) : process.cwd()
+ const customConfigPath = findCustomConfigFile(workDir)
+
+ if (customConfigPath) {
+ core.info(`Custom plugin configuration detected for cache: ${customConfigPath}`)
+ // Add hash of custom config file to cache key
+ const customConfigHash = hashCustomConfigFile(customConfigPath)
+ cacheKey += `custom-${customConfigHash}-`
+ keys.push(cacheKey)
+ }
+
// create path to go.mod prepending the workingDirectory if it exists
const goModPath = path.join(workingDirectory, `go.mod`)
@@ -97,8 +110,23 @@ export async function restoreCache(): Promise {
return
}
core.saveState(State.CachePrimaryKey, primaryKey)
+
+ // Prepare cache paths
+ const cachePaths = [getLintCacheDir()]
+
+ // If custom plugin config exists, also cache the custom binary
+ const workingDirectory = core.getInput(`working-directory`)
+ const workDir = workingDirectory ? path.resolve(workingDirectory) : process.cwd()
+ const customConfigPath = findCustomConfigFile(workDir)
+
+ if (customConfigPath) {
+ core.info(`Will cache custom golangci-lint binary from: ${workDir}`)
+ // Cache the working directory to include the custom binary
+ cachePaths.push(workDir)
+ }
+
try {
- const cacheKey = await cache.restoreCache([getLintCacheDir()], primaryKey, restoreKeys)
+ const cacheKey = await cache.restoreCache(cachePaths, primaryKey, restoreKeys)
if (!cacheKey) {
core.info(`Cache not found for input keys: ${[primaryKey, ...restoreKeys].join(", ")}`)
return
@@ -130,6 +158,17 @@ export async function saveCache(): Promise {
const startedAt = Date.now()
const cacheDirs = [getLintCacheDir()]
+
+ // If custom plugin config exists, also cache the custom binary
+ const workingDirectory = core.getInput(`working-directory`)
+ const workDir = workingDirectory ? path.resolve(workingDirectory) : process.cwd()
+ const customConfigPath = findCustomConfigFile(workDir)
+
+ if (customConfigPath) {
+ core.info(`Will save custom golangci-lint binary from: ${workDir}`)
+ cacheDirs.push(workDir)
+ }
+
const primaryKey = core.getState(State.CachePrimaryKey)
if (!primaryKey) {
utils.logWarning(`Error retrieving key from state.`)
diff --git a/src/custom.ts b/src/custom.ts
new file mode 100644
index 0000000000..7cc8181d3a
--- /dev/null
+++ b/src/custom.ts
@@ -0,0 +1,154 @@
+import * as core from "@actions/core"
+import * as crypto from "crypto"
+import { exec, ExecOptionsWithStringEncoding } from "child_process"
+import * as fs from "fs"
+import * as path from "path"
+import { promisify } from "util"
+
+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)
+ }
+}
+
+/**
+ * Check if a custom golangci-lint config file exists.
+ * The file can be .custom-gcl.yml, .custom-gcl.yaml, or .custom-gcl.json.
+ *
+ * @param workingDirectory the working directory to search in
+ * @returns the path to the custom config file, or null if not found
+ */
+export function findCustomConfigFile(workingDirectory: string): string | null {
+ const possibleFiles = [".custom-gcl.yml", ".custom-gcl.yaml", ".custom-gcl.json"]
+
+ for (const file of possibleFiles) {
+ const filePath = path.join(workingDirectory, file)
+ if (fs.existsSync(filePath)) {
+ core.info(`Found custom golangci-lint config: ${filePath}`)
+ return filePath
+ }
+ }
+
+ return null
+}
+
+/**
+ * Parse the custom config file to extract the binary name and destination.
+ *
+ * @param configPath path to the custom config file
+ * @returns object with name and destination (relative path)
+ */
+export function getCustomBinaryInfo(configPath: string): { name: string; destination: string } {
+ try {
+ const content = fs.readFileSync(configPath, "utf-8")
+
+ let name = "custom-gcl"
+ let destination = "."
+
+ // Try to parse as YAML/JSON
+ // Look for "name:" field in the config
+ const nameMatch = content.match(/^name:\s*["']?([^"'\s]+)["']?/m)
+ if (nameMatch && nameMatch[1]) {
+ name = nameMatch[1]
+ }
+
+ // Look for "destination:" field in the config
+ const destMatch = content.match(/^destination:\s*["']?([^"'\s]+)["']?/m)
+ if (destMatch && destMatch[1]) {
+ destination = destMatch[1]
+ }
+
+ // For JSON format
+ if (configPath.endsWith(".json")) {
+ try {
+ const json = JSON.parse(content)
+ if (json.name) {
+ name = json.name
+ }
+ if (json.destination) {
+ destination = json.destination
+ }
+ } catch {
+ // Fallback to default
+ }
+ }
+
+ return { name, destination }
+ } catch (err) {
+ core.warning(`Failed to parse custom config file: ${err}`)
+ }
+
+ // Default values
+ return { name: "custom-gcl", destination: "." }
+}
+
+/**
+ * Build a custom golangci-lint binary using `golangci-lint custom`.
+ *
+ * @param binPath path to the golangci-lint binary
+ * @param workingDirectory the working directory
+ * @param customConfigPath path to the custom config file
+ * @returns path to the built custom binary
+ */
+export async function buildCustomBinary(binPath: string, workingDirectory: string, customConfigPath: string): Promise {
+ core.info(`Building custom golangci-lint binary from ${customConfigPath}...`)
+
+ const startedAt = Date.now()
+
+ const cmdArgs: ExecOptionsWithStringEncoding = {
+ cwd: workingDirectory,
+ }
+
+ const binaryInfo = getCustomBinaryInfo(customConfigPath)
+ const destinationDir = path.join(workingDirectory, binaryInfo.destination)
+ const customBinaryPath = path.join(destinationDir, binaryInfo.name)
+
+ // Ensure destination directory exists
+ if (!fs.existsSync(destinationDir)) {
+ core.info(`Creating destination directory: ${destinationDir}`)
+ fs.mkdirSync(destinationDir, { recursive: true })
+ }
+
+ // Check if the binary already exists (from cache)
+ if (fs.existsSync(customBinaryPath)) {
+ core.info(`Custom binary already exists at ${customBinaryPath}`)
+ return customBinaryPath
+ }
+
+ const cmd = `${binPath} custom`
+
+ core.info(`Running [${cmd}] in [${workingDirectory}] ...`)
+
+ try {
+ const res = await execShellCommand(cmd, cmdArgs)
+ printOutput(res)
+
+ core.info(`Built custom golangci-lint binary in ${Date.now() - startedAt}ms`)
+
+ return customBinaryPath
+ } catch (exc) {
+ core.error(`Failed to build custom golangci-lint binary: ${exc}`)
+ throw new Error(`Failed to build custom golangci-lint binary: ${exc.message}`)
+ }
+}
+
+/**
+ * Calculate the hash of the custom config file.
+ *
+ * @param configPath path to the custom config file
+ * @returns SHA256 hash of the file content
+ */
+export function hashCustomConfigFile(configPath: string): string {
+ const content = fs.readFileSync(configPath, "utf-8")
+ return crypto.createHash("sha256").update(content).digest("hex")
+}
diff --git a/src/run.ts b/src/run.ts
index 8f79b998d6..18b6b5f165 100644
--- a/src/run.ts
+++ b/src/run.ts
@@ -6,6 +6,7 @@ import * as path from "path"
import { promisify } from "util"
import { restoreCache, saveCache } from "./cache"
+import { buildCustomBinary, findCustomConfigFile } from "./custom"
import { install } from "./install"
import { fetchPatch, isOnlyNewIssues } from "./patch"
@@ -45,9 +46,23 @@ const printOutput = (res: ExecRes): void => {
}
async function runLint(binPath: string, patchPath: string): Promise {
+ // Check for custom golangci-lint plugin configuration
+ const workingDirectory = core.getInput(`working-directory`)
+ const workDir = workingDirectory ? path.resolve(workingDirectory) : process.cwd()
+ const customConfigPath = findCustomConfigFile(workDir)
+
+ let effectiveBinPath = binPath
+
+ // If a custom config file exists, build the custom binary
+ if (customConfigPath) {
+ core.info(`Custom plugin configuration detected: ${customConfigPath}`)
+ effectiveBinPath = await buildCustomBinary(binPath, workDir, customConfigPath)
+ core.info(`Using custom golangci-lint binary: ${effectiveBinPath}`)
+ }
+
const debug = core.getInput(`debug`)
if (debug.split(`,`).includes(`cache`)) {
- const res = await execShellCommand(`${binPath} cache status`)
+ const res = await execShellCommand(`${effectiveBinPath} cache status`)
printOutput(res)
}
@@ -118,7 +133,6 @@ async function runLint(binPath: string, patchPath: string): Promise {
const cmdArgs: ExecOptionsWithStringEncoding = {}
- const workingDirectory = core.getInput(`working-directory`)
if (workingDirectory) {
if (!fs.existsSync(workingDirectory) || !fs.lstatSync(workingDirectory).isDirectory()) {
throw new Error(`working-directory (${workingDirectory}) was not a path`)
@@ -131,9 +145,9 @@ async function runLint(binPath: string, patchPath: string): Promise {
cmdArgs.cwd = path.resolve(workingDirectory)
}
- await runVerify(binPath, userArgsMap, cmdArgs)
+ await runVerify(effectiveBinPath, userArgsMap, cmdArgs)
- const cmd = `${binPath} run ${addedArgs.join(` `)} ${userArgs}`.trimEnd()
+ const cmd = `${effectiveBinPath} run ${addedArgs.join(` `)} ${userArgs}`.trimEnd()
core.info(`Running [${cmd}] in [${cmdArgs.cwd || process.cwd()}] ...`)