Skip to content
Open
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
9 changes: 9 additions & 0 deletions src/generator.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,15 @@ function commandLines(commands) {
.join("\n");
}

function formatCi(ci) {
const files = [
...(ci?.githubActions || []),
...(ci?.gitlabCi || []),
...(ci?.circleCi || []),
];
return files.length ? files.join(", ") : "No CI workflows detected";
}

function verificationChecklist(profile) {
const ordered = ["test", "lint", "build", "format"];
const lines = ordered
Expand Down
12 changes: 12 additions & 0 deletions src/reporter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export function renderScanSummary(profile) {
`Monorepo: ${formatMonorepo(profile.monorepo)}`,
`Package manager: ${profile.packageManager}`,
`Commands: ${Object.entries(profile.commands).filter(([, value]) => value).map(([key, value]) => `${key}=${value}`).join("; ") || "none"}`,
`CI: ${formatCi(profile.ci)}`,
`Agent docs: ${profile.agentDocs.map((doc) => doc.file).join(", ") || "none"}`,
].join("\n");
}
Expand Down Expand Up @@ -425,6 +426,7 @@ export function renderMarkdownReport(profile, findings, score) {
- Frameworks: ${profile.frameworks.join(", ") || "none"}
- Monorepo: ${formatMonorepo(profile.monorepo)}
- Package manager: ${profile.packageManager}
- CI: ${formatCi(profile.ci)}

${score.summary}

Expand Down Expand Up @@ -469,6 +471,7 @@ export function renderSnapshot(snapshot) {
- Primary language: ${profile.primaryLanguage}
- Package manager: ${profile.packageManager}
- Monorepo: ${formatMonorepo(profile.monorepo)}
- CI: ${formatCi(profile.ci)}

${score.summary}

Expand Down Expand Up @@ -539,6 +542,15 @@ function formatMonorepo(monorepo) {
return `${tools}${workspaces}`;
}

function formatCi(ci) {
const files = [
...(ci?.githubActions || []),
...(ci?.gitlabCi || []),
...(ci?.circleCi || []),
];
return files.length ? files.join(", ") : "none";
}

function badgeColor(score) {
if (score >= 90) return "brightgreen";
if (score >= 75) return "green";
Expand Down
20 changes: 10 additions & 10 deletions src/scanner.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -501,21 +501,21 @@ async function detectCi(root) {
.filter((entry) => entry.isFile() && /\.(ya?ml)$/.test(entry.name))
.map((entry) => `.github/workflows/${entry.name}`)
.sort();
const gitlabCi = [];
for (const file of [".gitlab-ci.yml", ".gitlab-ci.yaml"]) {
if (await pathExists(path.join(root, file))) gitlabCi.push(file);
}
const circleCi = [];
for (const file of [".circleci/config.yml", ".circleci/config.yaml"]) {
if (await pathExists(path.join(root, file))) circleCi.push(file);
}
return {
githubActions: workflows,
gitlabCi,
circleCi,
gitlabCi: await detectExistingFiles(root, [".gitlab-ci.yml", ".gitlab-ci.yaml"]),
circleCi: await detectExistingFiles(root, [".circleci/config.yml", ".circleci/config.yaml"]),
};
}

async function detectExistingFiles(root, candidates) {
const results = [];
for (const candidate of candidates) {
if (await pathExists(path.join(root, candidate))) results.push(candidate);
}
return results;
}

function parseTomlValue(text, key) {
if (!text) return "";
const match = text.match(new RegExp(`^\\s*${escapeRegExp(key)}\\s*=\\s*["']([^"']+)["']`, "m"));
Expand Down
9 changes: 9 additions & 0 deletions test/fixtures/multi-ci-app/.circleci/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version: 2.1

jobs:
lint:
docker:
- image: cimg/node:20.0
steps:
- checkout
- run: npm run lint
9 changes: 9 additions & 0 deletions test/fixtures/multi-ci-app/.circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version: 2.1

jobs:
test:
docker:
- image: cimg/node:20.0
steps:
- checkout
- run: npm test
12 changes: 12 additions & 0 deletions test/fixtures/multi-ci-app/.github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: CI

on:
push:
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
3 changes: 3 additions & 0 deletions test/fixtures/multi-ci-app/.gitlab-ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
lint:
script:
- npm run lint
3 changes: 3 additions & 0 deletions test/fixtures/multi-ci-app/.gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
test:
script:
- npm test
7 changes: 7 additions & 0 deletions test/fixtures/multi-ci-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "fixture-multi-ci-app",
"type": "module",
"scripts": {
"test": "node --test"
}
}
27 changes: 16 additions & 11 deletions test/scanner.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { improveRepo } from "../src/improver.mjs";
import { lintRepo, scoreRepo } from "../src/linter.mjs";
import { buildAgentMatrix } from "../src/matrix.mjs";
import { resolvePreset } from "../src/presets.mjs";
import { renderAgentMatrix, renderAnnotations, renderBenchmarkReport, renderComparison, renderDoctor, renderExamplesCatalog, renderExplanation, renderImprovement, renderImprovementIssue, renderLeaderboard, renderMarkdownReport, renderRoadmap, renderShareComment } from "../src/reporter.mjs";
import { renderAgentMatrix, renderAnnotations, renderBenchmarkReport, renderComparison, renderDoctor, renderExamplesCatalog, renderExplanation, renderImprovement, renderImprovementIssue, renderLeaderboard, renderMarkdownReport, renderRoadmap, renderShareComment, renderSnapshot } from "../src/reporter.mjs";
import { scanRepo } from "../src/scanner.mjs";
import { snapshotRepo } from "../src/snapshot.mjs";
import { renderCiWorkflow, writeCiWorkflow } from "../src/workflow.mjs";
Expand All @@ -35,16 +35,16 @@ test("scan detects a Node app profile", async () => {
assert.deepEqual(profile.ci.githubActions, [".github/workflows/ci.yml"]);
});

test("scan detects GitLab CI and CircleCI workflow files", async () => {
const profile = await scanRepo(fixture("alternate-ci-app"));
test("scan detects GitHub Actions, GitLab CI, and CircleCI files", async () => {
const profile = await scanRepo(fixture("multi-ci-app"));
const findings = await lintRepo(profile);
const agentsMd = buildAgentsMd(profile);

assert.deepEqual(profile.ci.githubActions, []);
assert.deepEqual(profile.ci.gitlabCi, [".gitlab-ci.yml"]);
assert.deepEqual(profile.ci.circleCi, [".circleci/config.yml"]);
assert.deepEqual(profile.ci.githubActions, [".github/workflows/ci.yml"]);
assert.deepEqual(profile.ci.gitlabCi, [".gitlab-ci.yml", ".gitlab-ci.yaml"]);
assert.deepEqual(profile.ci.circleCi, [".circleci/config.yml", ".circleci/config.yaml"]);
assert.equal(findings.some((finding) => finding.ruleId === "missing-ci"), false);
assert.match(agentsMd, /CI: \.gitlab-ci\.yml, \.circleci\/config\.yml/);
assert.match(agentsMd, /CI: \.github\/workflows\/ci\.yml, \.gitlab-ci\.yml, \.gitlab-ci\.yaml, \.circleci\/config\.yml, \.circleci\/config\.yaml/);
});

test("scan detects Python, Rust, and Go repositories", async () => {
Expand Down Expand Up @@ -364,23 +364,28 @@ test("score is explainable and bounded", async () => {
});

test("markdown report includes commands, docs, and findings", async () => {
const profile = await scanRepo(fixture("node-app"));
const profile = await scanRepo(fixture("multi-ci-app"));
const findings = await lintRepo(profile);
const score = scoreRepo(profile, findings);
const report = renderMarkdownReport(profile, findings, score);

assert.match(report, /Agent Readiness Report/);
assert.match(report, /fixture-node-app/);
assert.match(report, /fixture-multi-ci-app/);
assert.match(report, /npm run test/);
assert.match(report, /\.gitlab-ci\.yml/);
assert.match(report, /\.circleci\/config\.yml/);
});

test("snapshot summarizes score, compatibility, and findings", async () => {
const snapshot = await snapshotRepo(fixture("node-app"));
const snapshot = await snapshotRepo(fixture("multi-ci-app"));
const report = renderSnapshot(snapshot);

assert.equal(snapshot.repository.name, "fixture-node-app");
assert.equal(snapshot.repository.name, "fixture-multi-ci-app");
assert.equal(snapshot.matrix.summary.total, 5);
assert.equal(typeof snapshot.score.score, "number");
assert.equal(snapshot.summary.findings, snapshot.findings.length);
assert.match(report, /\.gitlab-ci\.yml/);
assert.match(report, /\.circleci\/config\.yml/);
});

test("examples catalog links copy-ready sample files", () => {
Expand Down