Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 32 additions & 4 deletions src/detect.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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,
);
}
}
}
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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),
Expand Down
8 changes: 7 additions & 1 deletion src/format.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
Expand All @@ -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.");
Expand Down
15 changes: 11 additions & 4 deletions test/detect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
);
Comment on lines 13 to 16
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 () => {
Expand Down
5 changes: 5 additions & 0 deletions test/fixtures/aligned/.github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 2 additions & 0 deletions test/fixtures/aligned/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go 1.24
toolchain go1.24.3
1 change: 1 addition & 0 deletions test/fixtures/drift/Dockerfile
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
FROM node:20-alpine
FROM golang:1.23
1 change: 1 addition & 0 deletions test/fixtures/drift/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
go 1.24