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
159 changes: 150 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ on:
pull_request:
branches: [main]

permissions:
contents: read
issues: write
pull-requests: write

concurrency:
group: ci-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true
Expand All @@ -14,17 +19,16 @@ jobs:
quality:
name: Quality Gates
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 22]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
node-version: 22
cache: pnpm

- run: pnpm install --frozen-lockfile
Expand All @@ -38,23 +42,160 @@ jobs:
- name: Build
run: pnpm build

- name: Test
run: pnpm test
- name: Codebase Intelligence CI
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE_REF="origin/${{ github.base_ref }}"
else
BASE_REF="${{ github.event.before }}"
if [ -z "$BASE_REF" ] || [ "$BASE_REF" = "0000000000000000000000000000000000000000" ]; then
BASE_REF="HEAD~1"
fi
fi

set +e

npx -y codebase-intelligence@canary ci . \
--base "$BASE_REF" \
--new-only \
--changed-workspaces \
--format annotations \
--summary \
--force
CBI_STATUS=$?

npx -y codebase-intelligence@canary ci . \
--base "$BASE_REF" \
--new-only \
--changed-workspaces \
--format sarif \
--output codebase-intelligence.sarif \
--summary \
--force
CBI_SARIF_STATUS=$?

npx -y codebase-intelligence@canary ci . \
--base "$BASE_REF" \
--new-only \
--changed-workspaces \
--comment markdown \
--output codebase-intelligence-pr-comment.md \
--summary \
--force
CBI_COMMENT_STATUS=$?

if [ -f codebase-intelligence-pr-comment.md ]; then
cat codebase-intelligence-pr-comment.md >> "$GITHUB_STEP_SUMMARY"
fi

if [ "$CBI_STATUS" -ne 0 ]; then
exit "$CBI_STATUS"
fi
if [ "$CBI_SARIF_STATUS" -ne 0 ]; then
exit "$CBI_SARIF_STATUS"
fi
exit "$CBI_COMMENT_STATUS"

- name: Post Codebase Intelligence PR comment
if: ${{ always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }}
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const marker = '<!-- codebase-intelligence-ci-comment -->';
const reportPath = 'codebase-intelligence-pr-comment.md';

if (!fs.existsSync(reportPath)) {
core.info('No Codebase Intelligence PR comment report found.');
return;
}

const report = fs.readFileSync(reportPath, 'utf8').trim();
if (!report) {
core.info('Codebase Intelligence PR comment report is empty.');
return;
}

const body = `${marker}\n${report}`;
const { owner, repo } = context.repo;
const issue_number = context.issue.number;
const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number,
per_page: 100,
});
const existing = comments.find((comment) =>
comment.user?.type === 'Bot' && comment.body?.includes(marker)
);

if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body,
});
return;
}

await github.rest.issues.createComment({
owner,
repo,
issue_number,
body,
});

- name: Upload Codebase Intelligence reports
if: always()
uses: actions/upload-artifact@v4
with:
name: codebase-intelligence-reports
path: |
codebase-intelligence.sarif
codebase-intelligence-pr-comment.md
retention-days: 14
if-no-files-found: warn

# Coverage is best-effort: vitest v3 + v8 provider can hang after tests
# pass on CI runners. The blocking Test step above proves correctness;
# pass on CI runners. The blocking test matrix proves correctness;
# this step is bounded so coverage cannot block the release gate.
- name: Coverage
if: matrix.node-version == 22
continue-on-error: true
timeout-minutes: 4
run: timeout 180s pnpm vitest run --coverage --pool forks --no-file-parallelism || true

- name: Upload coverage
if: matrix.node-version == 22 && always()
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
retention-days: 14
if-no-files-found: ignore

test:
name: Tests (${{ matrix.node-version }}, shard ${{ matrix.shard }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: [18, 22]
shard: ["1/4", "2/4", "3/4", "4/4"]
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: pnpm

- run: pnpm install --frozen-lockfile

- name: Build
run: pnpm build

- name: Test shard
run: pnpm test:shard --shard=${{ matrix.shard }}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
"build": "tsc -p tsconfig.build.json",
"dev": "tsx src/cli.ts",
"start": "node dist/cli.js",
"test": "pnpm build && vitest run --no-cache --pool threads --no-file-parallelism --maxWorkers=1",
"test": "pnpm build && pnpm test:run",
"test:run": "vitest run --no-cache --pool threads --maxWorkers=50%",
"test:shard": "vitest run --no-cache --pool threads --maxWorkers=2",
"test:coverage": "pnpm build && vitest run --coverage --pool threads --no-file-parallelism --exclude 'tests/*.e2e.test.ts'",
"verify:cli-real": "pnpm build && node tools/verify-cli-real-codebases.mjs",
"verify:cli-real:heavy": "pnpm build && CBI_REAL_PROFILE=heavy node tools/verify-cli-real-codebases.mjs",
"test:watch": "vitest",

Check warning on line 21 in package.json

View workflow job for this annotation

GitHub Actions / Quality Gates

Runtime import is declared in devDependencies%3A vitest (no-unused-deps)
"lint": "eslint src/",
"typecheck": "tsc --noEmit",
"clean": "rm -rf dist",
Expand Down Expand Up @@ -48,8 +50,8 @@
"graphology": "^0.25.4",
"graphology-communities-louvain": "^2.0.2",
"graphology-metrics": "^2.3.0",
"graphology-shortest-path": "^2.1.0",

Check warning on line 53 in package.json

View workflow job for this annotation

GitHub Actions / Quality Gates

Unused dependency%3A graphology-shortest-path (no-unused-deps)
"picocolors": "^1.1.1",

Check warning on line 54 in package.json

View workflow job for this annotation

GitHub Actions / Quality Gates

Unused dependency%3A picocolors (no-unused-deps)
"typescript": "^5.7.0",
"zod": "^3.23.0"
},
Expand Down
2 changes: 1 addition & 1 deletion tests/boundaries.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface RunResult {
type BoundaryPayload = BoundariesResult & { cache?: unknown; nextSteps?: unknown };

beforeAll(() => {
execSync("pnpm build", { cwd: repoRoot, stdio: "inherit" });
if (!fs.existsSync(cli)) execSync("pnpm build", { cwd: repoRoot, stdio: "inherit" });
}, 120_000);

function isRecord(value: unknown): value is Record<string, unknown> {
Expand Down
2 changes: 1 addition & 1 deletion tests/cli-mcp-stdio.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const GIT_ENV = {
};

beforeAll(() => {
execSync("pnpm build", { cwd: repoRoot, stdio: "inherit" });
if (!fs.existsSync(cli)) execSync("pnpm build", { cwd: repoRoot, stdio: "inherit" });
}, 120_000);

function isRecord(value: unknown): value is Record<string, unknown> {
Expand Down
2 changes: 1 addition & 1 deletion tests/drift.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ interface DriftPayload {
}

beforeAll(() => {
execSync("pnpm build", { cwd: repoRoot, stdio: "inherit" });
if (!fs.existsSync(cli)) execSync("pnpm build", { cwd: repoRoot, stdio: "inherit" });
}, 120_000);

function isRecord(value: unknown): value is Record<string, unknown> {
Expand Down
2 changes: 1 addition & 1 deletion tests/health.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ interface HealthPayload {
}

beforeAll(() => {
execSync("pnpm build", { cwd: repoRoot, stdio: "inherit" });
if (!fs.existsSync(cli)) execSync("pnpm build", { cwd: repoRoot, stdio: "inherit" });
}, 120_000);

function isRecord(value: unknown): value is Record<string, unknown> {
Expand Down
2 changes: 1 addition & 1 deletion tests/highways.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ interface HighwaysPayload {
const created: string[] = [];

beforeAll(() => {
execSync("pnpm build", { cwd: repoRoot, stdio: "inherit" });
if (!fs.existsSync(cli)) execSync("pnpm build", { cwd: repoRoot, stdio: "inherit" });
}, 120_000);

afterEach(() => {
Expand Down
3 changes: 2 additions & 1 deletion tests/map.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { execFile, execSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
Expand Down Expand Up @@ -66,7 +67,7 @@ interface CodebaseMapPayload {
}

beforeAll(() => {
execSync("pnpm build", { cwd: repoRoot, stdio: "inherit" });
if (!fs.existsSync(cli)) execSync("pnpm build", { cwd: repoRoot, stdio: "inherit" });
}, 120_000);

function isRecord(value: unknown): value is Record<string, unknown> {
Expand Down
2 changes: 1 addition & 1 deletion tests/operation-registry.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ function expectCliTextMatchesFormatter<TInput extends object, TResult>(
}

beforeAll(() => {
execSync("pnpm build", { cwd: repoRoot, stdio: "inherit" });
if (!fs.existsSync(cli)) execSync("pnpm build", { cwd: repoRoot, stdio: "inherit" });
}, 120_000);

describe("operation registry chained parity", () => {
Expand Down
2 changes: 1 addition & 1 deletion tests/roadmap-2-5-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ interface RunResult {
const created: string[] = [];

beforeAll(() => {
execSync("pnpm build", { cwd: repoRoot, stdio: "inherit" });
if (!fs.existsSync(cli)) execSync("pnpm build", { cwd: repoRoot, stdio: "inherit" });
}, 120_000);

afterEach(() => {
Expand Down
Loading