diff --git a/src/detect.js b/src/detect.js index 2f5f8fb..3d64f98 100644 --- a/src/detect.js +++ b/src/detect.js @@ -61,6 +61,24 @@ async function detectPyproject(root, output) { if (match) add(output, "python", "pyproject.toml", match[1], index + 1); } } +async function detectGoMod(root, output) { + const content = await read(root, "go.mod"); + if (!content) return; + + const lines = content.split(/\r?\n/); + + for (const [index, line] of lines.entries()) { + const go = line.match(/^\s*go\s+(\S+)/); + if (go) { + add(output, "go", "go.mod", go[1], index + 1); + } + + const toolchain = line.match(/^\s*toolchain\s+go(\S+)/); + if (toolchain) { + add(output, "go", "go.mod", toolchain[1], index + 1); + } + } +} async function detectToolVersions(root, output) { const content = await read(root, ".tool-versions"); @@ -96,8 +114,17 @@ async function detectDockerfiles(root, output) { if (!entry.isFile() || !/^Dockerfile(?:\..+)?$/i.test(entry.name)) continue; const content = await read(root, entry.name); for (const [index, line] of content.split(/\r?\n/).entries()) { - const match = line.match(/^\s*FROM\s+(?:[^/\s]+\/)?(node|python):([^\s@]+)/i); - if (match) add(output, match[1].toLowerCase(), entry.name, match[2], index + 1); + const match = line.match( + /^\s*FROM\s+(?:[^/\s]+\/)?(node|python|golang):([^\s@]+)/i, + ); + if (match) + add( + output, + match[1].toLowerCase() === "golang" ? "go" : match[1].toLowerCase(), + entry.name, + match[2], + index + 1, + ); } } } @@ -124,7 +151,7 @@ async function detectGithubActions(root, output) { let remaining = 0; for (const [index, line] of lines.entries()) { - const action = line.match(/uses:\s*actions\/setup-(node|python)@/i); + const action = line.match(/uses:\s*actions\/setup-(node|python|go)@/i); if (action) { runtime = action[1].toLowerCase(); remaining = 12; @@ -135,7 +162,7 @@ async function detectGithubActions(root, output) { continue; } remaining -= 1; - const declared = line.match(/(?:node-version|python-version):\s*["']?([^"'#\s]+)/i); + const declared = line.match(/(?:node-version|python-version|go-version):\s*["']?([^"'#\s]+)/i); if (runtime && declared && !declared[1].startsWith("${{")) { add(output, runtime, file, declared[1], index + 1); runtime = null; @@ -150,6 +177,7 @@ export async function detectDeclarations(root) { detectVersionFiles(root, output), detectPackageJson(root, output), detectPyproject(root, output), + detectGoMod(root, output), detectToolVersions(root, output), detectMise(root, output), detectDockerfiles(root, output), diff --git a/src/format.js b/src/format.js index 2bf565e..70a9273 100644 --- a/src/format.js +++ b/src/format.js @@ -2,6 +2,12 @@ function location(declaration) { return declaration.line ? `${declaration.file}:${declaration.line}` : declaration.file; } +const LABELS = { + go: "Go", + node: "Node.js", + python: "Python", +}; + export function formatHuman(result) { if (result.runtimes.length === 0) { return "No supported runtime declarations found."; @@ -14,7 +20,7 @@ export function formatHuman(result) { }); if (!runtime.ok) { - const label = runtime.runtime === "node" ? "Node.js" : "Python"; + const label = LABELS[runtime.runtime] ?? runtime.runtime; rows.push("", ` No ${label} version satisfies every declaration.`); } else if (runtime.declarations.some((declaration) => !declaration.parsed)) { rows.push("", " Some declarations could not be parsed."); diff --git a/test/detect.test.js b/test/detect.test.js index 7dec27d..ec7ab70 100644 --- a/test/detect.test.js +++ b/test/detect.test.js @@ -12,17 +12,24 @@ test("finds aligned Node.js and Python declarations", async () => { assert.equal(result.ok, true); assert.deepEqual( result.runtimes.map((runtime) => runtime.runtime), - ["node", "python"], + ["go", "node", "python"], ); - assert.equal(result.runtimes[0].declarations.length, 5); + assert.equal(result.runtimes[0].declarations.length, 3); assert.equal(result.runtimes[1].declarations.length, 5); + assert.equal(result.runtimes[2].declarations.length, 5); }); test("finds Docker and project-file drift", async () => { const result = await inspect(path.join(here, "fixtures", "drift")); - assert.equal(result.ok, false); - assert.equal(result.runtimes[0].runtime, "node"); + assert.deepEqual( + result.runtimes.map((runtime) => runtime.runtime), + ["go", "node"], + ); + const go = result.runtimes.find((runtime) => runtime.runtime === "go"); + + assert.ok(go); + assert.equal(go.ok, false); }); test("returns an empty success for a directory with no declarations", async () => { diff --git a/test/fixtures/aligned/.github/workflows/ci.yml b/test/fixtures/aligned/.github/workflows/ci.yml index a33365f..d932b85 100644 --- a/test/fixtures/aligned/.github/workflows/ci.yml +++ b/test/fixtures/aligned/.github/workflows/ci.yml @@ -9,3 +9,8 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.12" + go: + steps: + - uses: actions/setup-go@v5 + with: + go-version: "1.24" diff --git a/test/fixtures/aligned/go.mod b/test/fixtures/aligned/go.mod new file mode 100644 index 0000000..85cfba5 --- /dev/null +++ b/test/fixtures/aligned/go.mod @@ -0,0 +1,2 @@ +go 1.24 +toolchain go1.24.3 diff --git a/test/fixtures/drift/Dockerfile b/test/fixtures/drift/Dockerfile index f0e004a..67a885b 100644 --- a/test/fixtures/drift/Dockerfile +++ b/test/fixtures/drift/Dockerfile @@ -1 +1,2 @@ FROM node:20-alpine +FROM golang:1.23 diff --git a/test/fixtures/drift/go.mod b/test/fixtures/drift/go.mod new file mode 100644 index 0000000..8e3bc02 --- /dev/null +++ b/test/fixtures/drift/go.mod @@ -0,0 +1 @@ +go 1.24