UNPKG

pi-lens

Version:

Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo

111 lines (110 loc) • 4.08 kB
import path from "node:path"; import { safeSpawnAsync } from "../../safe-spawn.js"; import { PRIORITY } from "../priorities.js"; import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js"; const actionlint = createAvailabilityChecker("actionlint", ".exe"); export function isGitHubWorkflowFile(filePath) { const normalized = filePath.replace(/\\/g, "/"); return /(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/i.test(normalized); } function toDiagnostic(issue, filePath) { const line = issue.line && issue.line > 0 ? issue.line : 1; const column = issue.column && issue.column > 0 ? issue.column : 1; const rule = issue.kind || "actionlint"; const message = issue.message || "GitHub Actions workflow issue"; const idMessage = message.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 80); return { id: `actionlint-${rule}-${line}-${column}-${idMessage}`, message, filePath, line, column, severity: "error", semantic: "blocking", tool: "actionlint", rule, defectClass: "correctness", matchedText: issue.snippet, }; } export function parseActionlintJson(raw, filePath) { const trimmed = raw.trim(); if (!trimmed) return []; try { const parsed = JSON.parse(trimmed); const issues = Array.isArray(parsed) ? parsed : [parsed]; return issues.map((issue) => toDiagnostic(issue, filePath)); } catch { // Some actionlint versions or wrappers may emit one JSON object per line. const diagnostics = []; for (const line of trimmed.split(/\r?\n/)) { if (!line.trim()) continue; try { const parsed = JSON.parse(line); diagnostics.push(toDiagnostic(parsed, filePath)); } catch { // Ignore non-JSON chatter; the caller will synthesize a generic diagnostic // if actionlint failed and no structured diagnostics were parsed. } } return diagnostics; } } const actionlintRunner = { id: "actionlint", appliesTo: ["yaml"], priority: PRIORITY.YAML_LINT + 1, enabledByDefault: true, skipTestFiles: false, when(ctx) { return isGitHubWorkflowFile(ctx.filePath); }, async run(ctx) { const cwd = ctx.cwd || process.cwd(); let cmd = null; if (await (actionlint.isAvailableAsync(cwd))) { cmd = actionlint.getCommand(cwd); } else { cmd = await resolveToolCommandWithInstallFallback(cwd, "actionlint"); } if (!cmd) return { status: "skipped", diagnostics: [], semantic: "none" }; const relativeFilePath = path.relative(cwd, ctx.filePath) || ctx.filePath; const result = await safeSpawnAsync(cmd, ["-format", "{{json .}}", relativeFilePath], { cwd, timeout: 15000, }); const raw = `${result.stdout ?? ""}${result.stderr ?? ""}`; let diagnostics = parseActionlintJson(raw, ctx.filePath); if (diagnostics.length === 0 && result.status !== 0 && raw.trim()) { diagnostics = [ { id: `actionlint-${path.basename(ctx.filePath)}-failure`, message: raw.trim().split(/\r?\n/)[0] || "actionlint failed", filePath: ctx.filePath, line: 1, column: 1, severity: "error", semantic: "blocking", tool: "actionlint", rule: "actionlint", defectClass: "correctness", }, ]; } if (diagnostics.length === 0) { return { status: "succeeded", diagnostics: [], semantic: "none" }; } return { status: "failed", diagnostics, semantic: "blocking", }; }, }; export default actionlintRunner;