From 99895923b96003d66f851cce1e8caa285db89e24 Mon Sep 17 00:00:00 2001 From: ahmdkaml Date: Tue, 28 Jul 2026 02:23:54 +0300 Subject: [PATCH 1/4] Add Go toolchain detection --- src/detect.js | 36 ++++++++++++++++--- test/detect.test.js | 11 ++++-- .../fixtures/aligned/.github/workflows/ci.yml | 5 +++ test/fixtures/aligned/go.mod | 2 ++ test/fixtures/drift/Dockerfile | 1 + test/fixtures/drift/go.mod | 1 + 6 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 test/fixtures/aligned/go.mod create mode 100644 test/fixtures/drift/go.mod 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/test/detect.test.js b/test/detect.test.js index 7dec27d..ae848a0 100644 --- a/test/detect.test.js +++ b/test/detect.test.js @@ -11,18 +11,23 @@ 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"], +); }); 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..15e7e7e 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" \ No newline at end of file diff --git a/test/fixtures/aligned/go.mod b/test/fixtures/aligned/go.mod new file mode 100644 index 0000000..d3f9de9 --- /dev/null +++ b/test/fixtures/aligned/go.mod @@ -0,0 +1,2 @@ +go 1.24 +toolchain go1.24.3 \ No newline at end of file diff --git a/test/fixtures/drift/Dockerfile b/test/fixtures/drift/Dockerfile index f0e004a..58abd4a 100644 --- a/test/fixtures/drift/Dockerfile +++ b/test/fixtures/drift/Dockerfile @@ -1 +1,2 @@ FROM node:20-alpine +FROM golang:1.23 \ No newline at end of file diff --git a/test/fixtures/drift/go.mod b/test/fixtures/drift/go.mod new file mode 100644 index 0000000..b506908 --- /dev/null +++ b/test/fixtures/drift/go.mod @@ -0,0 +1 @@ +go 1.24 \ No newline at end of file From d90be13ef57c5d4e2ba36eb930c87f0665f076c7 Mon Sep 17 00:00:00 2001 From: ahmdkaml Date: Tue, 28 Jul 2026 18:31:39 +0300 Subject: [PATCH 2/4] Strengthen Go drift test --- test/detect.test.js | 13 +++++++------ test/fixtures/aligned/.github/workflows/ci.yml | 2 +- test/fixtures/aligned/go.mod | 2 +- test/fixtures/drift/Dockerfile | 2 +- test/fixtures/drift/go.mod | 2 +- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/test/detect.test.js b/test/detect.test.js index ae848a0..ac50477 100644 --- a/test/detect.test.js +++ b/test/detect.test.js @@ -11,9 +11,8 @@ test("finds aligned Node.js and Python declarations", async () => { assert.equal(result.ok, true); assert.deepEqual( - result.runtimes.map((runtime) => runtime.runtime), - ["go","node", "python"], + ["go", "node", "python"], ); assert.equal(result.runtimes[0].declarations.length, 3); assert.equal(result.runtimes[1].declarations.length, 5); @@ -22,12 +21,14 @@ test("finds aligned Node.js and Python declarations", async () => { test("finds Docker and project-file drift", async () => { const result = await inspect(path.join(here, "fixtures", "drift")); - assert.equal(result.ok, false); assert.deepEqual( - result.runtimes.map((runtime) => runtime.runtime), - ["go", "node"], -); + result.runtimes.map((runtime) => runtime.runtime), + ["go", "node"], + ); + const go = result.runtimes.find((runtime) => runtime.runtime === "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 15e7e7e..d932b85 100644 --- a/test/fixtures/aligned/.github/workflows/ci.yml +++ b/test/fixtures/aligned/.github/workflows/ci.yml @@ -13,4 +13,4 @@ jobs: steps: - uses: actions/setup-go@v5 with: - go-version: "1.24" \ No newline at end of file + go-version: "1.24" diff --git a/test/fixtures/aligned/go.mod b/test/fixtures/aligned/go.mod index d3f9de9..85cfba5 100644 --- a/test/fixtures/aligned/go.mod +++ b/test/fixtures/aligned/go.mod @@ -1,2 +1,2 @@ go 1.24 -toolchain go1.24.3 \ No newline at end of file +toolchain go1.24.3 diff --git a/test/fixtures/drift/Dockerfile b/test/fixtures/drift/Dockerfile index 58abd4a..67a885b 100644 --- a/test/fixtures/drift/Dockerfile +++ b/test/fixtures/drift/Dockerfile @@ -1,2 +1,2 @@ FROM node:20-alpine -FROM golang:1.23 \ No newline at end of file +FROM golang:1.23 diff --git a/test/fixtures/drift/go.mod b/test/fixtures/drift/go.mod index b506908..8e3bc02 100644 --- a/test/fixtures/drift/go.mod +++ b/test/fixtures/drift/go.mod @@ -1 +1 @@ -go 1.24 \ No newline at end of file +go 1.24 From 8ef18732af60c85238c4dd05ab18b38407ca7d9c Mon Sep 17 00:00:00 2001 From: ahmdkaml Date: Tue, 28 Jul 2026 18:40:45 +0300 Subject: [PATCH 3/4] Strengthen Go drift test --- src/format.js | 8 +++++++- test/detect.test.js | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) 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 ac50477..ec7ab70 100644 --- a/test/detect.test.js +++ b/test/detect.test.js @@ -28,6 +28,7 @@ test("finds Docker and project-file drift", async () => { ); const go = result.runtimes.find((runtime) => runtime.runtime === "go"); + assert.ok(go); assert.equal(go.ok, false); }); From 5d5ac186e12b7aac66497628104ddc7ba521182a Mon Sep 17 00:00:00 2001 From: ahmdkaml Date: Thu, 30 Jul 2026 15:52:29 +0300 Subject: [PATCH 4/4] Support Rust runtime detection --- src/detect.js | 41 +++++++++++++++++-- src/format.js | 1 + test/detect.test.js | 15 +++++-- .../fixtures/aligned/.github/workflows/ci.yml | 5 +++ test/fixtures/aligned/Cargo.toml | 5 +++ test/fixtures/aligned/Dockerfile | 1 + test/fixtures/aligned/rust-toolchain.toml | 2 + test/fixtures/drift/Cargo.toml | 5 +++ test/fixtures/drift/Dockerfile | 1 + test/fixtures/drift/rust-toolchain.toml | 2 + 10 files changed, 71 insertions(+), 7 deletions(-) create mode 100644 test/fixtures/aligned/Cargo.toml create mode 100644 test/fixtures/aligned/rust-toolchain.toml create mode 100644 test/fixtures/drift/Cargo.toml create mode 100644 test/fixtures/drift/rust-toolchain.toml diff --git a/src/detect.js b/src/detect.js index 3d64f98..9ebea74 100644 --- a/src/detect.js +++ b/src/detect.js @@ -61,6 +61,33 @@ async function detectPyproject(root, output) { if (match) add(output, "python", "pyproject.toml", match[1], index + 1); } } + +async function detectCargoToml(root, output) { + const content = await read(root, "Cargo.toml"); + if (!content) return; + const lines = content.split(/\r?\n/); + + for (const [index, line] of lines.entries()) { + const match = line.match(/^\s*rust-version\s*=\s*["']([^"']+)["']/); + if (match) { + add(output, "rust", "Cargo.toml", match[1], index + 1); + } + } +} + +async function detectRustToolchain(root, output) { + const content = await read(root, "rust-toolchain.toml"); + if (!content) return; + const lines = content.split(/\r?\n/); + + for (const [index, line] of lines.entries()) { + const match = line.match(/^\s*channel\s*=\s*["']([^"']+)["']/); + if (match) { + add(output, "rust", "rust-toolchain.toml", match[1], index + 1); + } + } +} + async function detectGoMod(root, output) { const content = await read(root, "go.mod"); if (!content) return; @@ -115,7 +142,7 @@ async function detectDockerfiles(root, output) { 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|golang):([^\s@]+)/i, + /^\s*FROM\s+(?:[^/\s]+\/)?(node|python|golang|rust):([^\s@]+)/i, ); if (match) add( @@ -151,9 +178,11 @@ 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|go)@/i); + const action = line.match( + /uses:\s*(?:actions\/setup-(node|python|go)|dtolnay\/rust-toolchain)@/i, + ); if (action) { - runtime = action[1].toLowerCase(); + runtime = action[1] ? action[1].toLowerCase() : "rust"; remaining = 12; continue; } @@ -162,7 +191,9 @@ async function detectGithubActions(root, output) { continue; } remaining -= 1; - const declared = line.match(/(?:node-version|python-version|go-version):\s*["']?([^"'#\s]+)/i); + const declared = line.match( + /(?:node-version|python-version|go-version|toolchain):\s*["']?([^"'#\s]+)/i, + ); if (runtime && declared && !declared[1].startsWith("${{")) { add(output, runtime, file, declared[1], index + 1); runtime = null; @@ -178,6 +209,8 @@ export async function detectDeclarations(root) { detectPackageJson(root, output), detectPyproject(root, output), detectGoMod(root, output), + detectCargoToml(root, output), + detectRustToolchain(root, output), detectToolVersions(root, output), detectMise(root, output), detectDockerfiles(root, output), diff --git a/src/format.js b/src/format.js index 70a9273..bb5f3cf 100644 --- a/src/format.js +++ b/src/format.js @@ -6,6 +6,7 @@ const LABELS = { go: "Go", node: "Node.js", python: "Python", + rust: "Rust", }; export function formatHuman(result) { diff --git a/test/detect.test.js b/test/detect.test.js index ec7ab70..d15073d 100644 --- a/test/detect.test.js +++ b/test/detect.test.js @@ -6,17 +6,18 @@ import { inspect } from "../src/index.js"; const here = path.dirname(fileURLToPath(import.meta.url)); -test("finds aligned Node.js and Python declarations", async () => { +test("finds aligned Node.js, golang, Rust, and Python declarations", async () => { const result = await inspect(path.join(here, "fixtures", "aligned")); assert.equal(result.ok, true); assert.deepEqual( result.runtimes.map((runtime) => runtime.runtime), - ["go", "node", "python"], + ["go", "node", "python", "rust"], ); assert.equal(result.runtimes[0].declarations.length, 3); assert.equal(result.runtimes[1].declarations.length, 5); assert.equal(result.runtimes[2].declarations.length, 5); + assert.equal(result.runtimes[3].declarations.length, 4); }); test("finds Docker and project-file drift", async () => { @@ -24,12 +25,20 @@ test("finds Docker and project-file drift", async () => { assert.equal(result.ok, false); assert.deepEqual( result.runtimes.map((runtime) => runtime.runtime), - ["go", "node"], + ["go", "node", "rust"], ); + // Node.js, golang, and Rust are all misaligned + const node = result.runtimes.find((runtime) => runtime.runtime === "node"); + assert.ok(node); + assert.equal(node.ok, false); const go = result.runtimes.find((runtime) => runtime.runtime === "go"); assert.ok(go); assert.equal(go.ok, false); + + const rust = result.runtimes.find((runtime) => runtime.runtime === "rust"); + assert.ok(rust); + assert.equal(rust.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 d932b85..80d14d9 100644 --- a/test/fixtures/aligned/.github/workflows/ci.yml +++ b/test/fixtures/aligned/.github/workflows/ci.yml @@ -14,3 +14,8 @@ jobs: - uses: actions/setup-go@v5 with: go-version: "1.24" + rust: + steps: + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: "1.81.0" diff --git a/test/fixtures/aligned/Cargo.toml b/test/fixtures/aligned/Cargo.toml new file mode 100644 index 0000000..0331b24 --- /dev/null +++ b/test/fixtures/aligned/Cargo.toml @@ -0,0 +1,5 @@ +[package] +name = "example" +version = "0.1.0" +edition = "2021" +rust-version = "1.81" diff --git a/test/fixtures/aligned/Dockerfile b/test/fixtures/aligned/Dockerfile index 82caf45..837dd2e 100644 --- a/test/fixtures/aligned/Dockerfile +++ b/test/fixtures/aligned/Dockerfile @@ -1,2 +1,3 @@ FROM node:22-alpine AS web FROM python:3.12-slim AS worker +FROM rust:1.81.0 diff --git a/test/fixtures/aligned/rust-toolchain.toml b/test/fixtures/aligned/rust-toolchain.toml new file mode 100644 index 0000000..1de01fa --- /dev/null +++ b/test/fixtures/aligned/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.81.0" diff --git a/test/fixtures/drift/Cargo.toml b/test/fixtures/drift/Cargo.toml new file mode 100644 index 0000000..0331b24 --- /dev/null +++ b/test/fixtures/drift/Cargo.toml @@ -0,0 +1,5 @@ +[package] +name = "example" +version = "0.1.0" +edition = "2021" +rust-version = "1.81" diff --git a/test/fixtures/drift/Dockerfile b/test/fixtures/drift/Dockerfile index 67a885b..373bd7c 100644 --- a/test/fixtures/drift/Dockerfile +++ b/test/fixtures/drift/Dockerfile @@ -1,2 +1,3 @@ FROM node:20-alpine FROM golang:1.23 +FROM rust:1.82 diff --git a/test/fixtures/drift/rust-toolchain.toml b/test/fixtures/drift/rust-toolchain.toml new file mode 100644 index 0000000..1de01fa --- /dev/null +++ b/test/fixtures/drift/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.81.0"