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()}] ...`)