From 4822769694a488a6199e9e59e8c0c4b5f59de196 Mon Sep 17 00:00:00 2001 From: Will-thom <116388885+Will-thom@users.noreply.github.com> Date: Fri, 5 Jun 2026 23:16:52 -0300 Subject: [PATCH] feat(scanner): detect GitLab CI and CircleCI files --- src/generator.mjs | 9 +++++++ src/reporter.mjs | 12 +++++++++ src/scanner.mjs | 20 +++++++------- .../multi-ci-app/.circleci/config.yaml | 9 +++++++ .../multi-ci-app/.circleci/config.yml | 9 +++++++ .../multi-ci-app/.github/workflows/ci.yml | 12 +++++++++ test/fixtures/multi-ci-app/.gitlab-ci.yaml | 3 +++ test/fixtures/multi-ci-app/.gitlab-ci.yml | 3 +++ test/fixtures/multi-ci-app/package.json | 7 +++++ test/scanner.test.mjs | 27 +++++++++++-------- 10 files changed, 90 insertions(+), 21 deletions(-) create mode 100644 test/fixtures/multi-ci-app/.circleci/config.yaml create mode 100644 test/fixtures/multi-ci-app/.circleci/config.yml create mode 100644 test/fixtures/multi-ci-app/.github/workflows/ci.yml create mode 100644 test/fixtures/multi-ci-app/.gitlab-ci.yaml create mode 100644 test/fixtures/multi-ci-app/.gitlab-ci.yml create mode 100644 test/fixtures/multi-ci-app/package.json diff --git a/src/generator.mjs b/src/generator.mjs index ab06e57..df90b3b 100644 --- a/src/generator.mjs +++ b/src/generator.mjs @@ -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 diff --git a/src/reporter.mjs b/src/reporter.mjs index 36191ff..5d31140 100644 --- a/src/reporter.mjs +++ b/src/reporter.mjs @@ -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"); } @@ -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} @@ -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} @@ -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"; diff --git a/src/scanner.mjs b/src/scanner.mjs index bbbf796..f629632 100644 --- a/src/scanner.mjs +++ b/src/scanner.mjs @@ -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")); diff --git a/test/fixtures/multi-ci-app/.circleci/config.yaml b/test/fixtures/multi-ci-app/.circleci/config.yaml new file mode 100644 index 0000000..0b8274d --- /dev/null +++ b/test/fixtures/multi-ci-app/.circleci/config.yaml @@ -0,0 +1,9 @@ +version: 2.1 + +jobs: + lint: + docker: + - image: cimg/node:20.0 + steps: + - checkout + - run: npm run lint diff --git a/test/fixtures/multi-ci-app/.circleci/config.yml b/test/fixtures/multi-ci-app/.circleci/config.yml new file mode 100644 index 0000000..164e1ca --- /dev/null +++ b/test/fixtures/multi-ci-app/.circleci/config.yml @@ -0,0 +1,9 @@ +version: 2.1 + +jobs: + test: + docker: + - image: cimg/node:20.0 + steps: + - checkout + - run: npm test diff --git a/test/fixtures/multi-ci-app/.github/workflows/ci.yml b/test/fixtures/multi-ci-app/.github/workflows/ci.yml new file mode 100644 index 0000000..13f5f9c --- /dev/null +++ b/test/fixtures/multi-ci-app/.github/workflows/ci.yml @@ -0,0 +1,12 @@ +name: CI + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: npm test diff --git a/test/fixtures/multi-ci-app/.gitlab-ci.yaml b/test/fixtures/multi-ci-app/.gitlab-ci.yaml new file mode 100644 index 0000000..e18615c --- /dev/null +++ b/test/fixtures/multi-ci-app/.gitlab-ci.yaml @@ -0,0 +1,3 @@ +lint: + script: + - npm run lint diff --git a/test/fixtures/multi-ci-app/.gitlab-ci.yml b/test/fixtures/multi-ci-app/.gitlab-ci.yml new file mode 100644 index 0000000..be707dc --- /dev/null +++ b/test/fixtures/multi-ci-app/.gitlab-ci.yml @@ -0,0 +1,3 @@ +test: + script: + - npm test diff --git a/test/fixtures/multi-ci-app/package.json b/test/fixtures/multi-ci-app/package.json new file mode 100644 index 0000000..728729c --- /dev/null +++ b/test/fixtures/multi-ci-app/package.json @@ -0,0 +1,7 @@ +{ + "name": "fixture-multi-ci-app", + "type": "module", + "scripts": { + "test": "node --test" + } +} diff --git a/test/scanner.test.mjs b/test/scanner.test.mjs index a3649e4..2838bce 100644 --- a/test/scanner.test.mjs +++ b/test/scanner.test.mjs @@ -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"; @@ -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 () => { @@ -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", () => {