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
46 changes: 46 additions & 0 deletions .github/workflows/code-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,49 @@ jobs:

- name: Validate docs (AGENTS.md/README commands + links)
run: pnpm validate-docs

build:
runs-on: ubuntu-latest
env:
# Public, non-secret placeholders so the production build is deterministic
# and does not depend on repository secrets (the app reads Supabase at runtime).
NEXT_PUBLIC_SUPABASE_URL: http://127.0.0.1:54321
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: build-placeholder-key
NEXT_PUBLIC_SITE_URL: http://localhost:3000
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4
with:
version: 11.5.0

- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Restore Next.js build cache
id: next-cache
uses: actions/cache@v4
with:
path: .next/cache
# Cache the Next.js compiler/incremental cache so rebuilds reuse work.
key: ${{ runner.os }}-nextjs-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.[jt]s', 'src/**/*.[jt]sx', '*.ts', '*.mjs') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('pnpm-lock.yaml') }}-

- name: Build with metrics (incremental via restored .next/cache)
env:
NEXT_BUILD_CACHE_HIT: ${{ steps.next-cache.outputs.cache-hit }}
run: pnpm build:metrics

- name: Upload build metrics (duration + cache hit)
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: build-metrics
path: reports/build-metrics.json
if-no-files-found: warn
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Requires Node 22+, pnpm 11.5.0, the Supabase CLI, and Docker.
| --------------- | ---------------------------------------------------- |
| Dev server | `pnpm dev` |
| Build | `pnpm build` |
| Build + metrics | `pnpm build:metrics` |
| Lint | `pnpm lint` |
| Type-check | `pnpm typecheck` |
| Format | `pnpm format` (write) / `pnpm format:check` (verify) |
Expand All @@ -33,6 +34,8 @@ Requires Node 22+, pnpm 11.5.0, the Supabase CLI, and Docker.

**Before committing or finishing a task, run:** `pnpm lint && pnpm typecheck && pnpm test`. The Husky `pre-commit` hook enforces `lint-staged` (Prettier check + ESLint with `--max-warnings=0`), `pnpm typecheck`, and `pnpm tech-debt`, so commits fail on any lint warning, format drift, type error, or untracked TODO marker. The `Code Quality` GitHub Actions workflow (`.github/workflows/code-quality.yml`) additionally runs `knip`, `jscpd`, and the tech-debt scan on every push and PR.

**Build performance:** the same workflow has a separate `build` job that restores the Next.js incremental cache (`.next/cache`, keyed on the lockfile and source hash via `actions/cache`) so rebuilds reuse compiler work, then runs `pnpm build:metrics`. `scripts/build-metrics.mjs` times the production build, writes `reports/build-metrics.json` (duration, `cacheRestored` flag, Next/Node versions, commit), appends a summary to the GitHub job page, and uploads the JSON as a `build-metrics` artifact so build-duration regressions are visible across runs.

## Conventions

- **Language:** TypeScript in strict mode. Do not introduce `any`; prefer precise types. JS files are not allowed (`allowJs: false`).
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"scripts": {
"dev": "next dev",
"build": "next build",
"build:metrics": "node scripts/build-metrics.mjs",
"start": "next start",
"lint": "eslint .",
"format": "prettier . --write",
Expand Down
79 changes: 79 additions & 0 deletions scripts/build-metrics.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env node
// Runs the production build, measures wall-clock duration, and exports a
// machine-readable build-metrics report. Used in CI to track build performance
// over time and surface regressions; appends a summary to the GitHub job page
// when GITHUB_STEP_SUMMARY is available. Forwards the build's exit code.
import { spawn } from "node:child_process";
import { mkdirSync, readFileSync, appendFileSync, writeFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const reportPath = resolve(repoRoot, "reports", "build-metrics.json");

function readNextVersion() {
try {
const pkg = JSON.parse(readFileSync(resolve(repoRoot, "package.json"), "utf8"));
return pkg.dependencies?.next ?? pkg.devDependencies?.next ?? "unknown";
} catch {
return "unknown";
}
}

function runBuild() {
return new Promise((resolveRun) => {
const start = process.hrtime.bigint();
const child = spawn("pnpm", ["build"], { stdio: "inherit", cwd: repoRoot, env: process.env });

let settled = false;
const settle = (code) => {
if (settled) return;
settled = true;
const durationMs = Number((process.hrtime.bigint() - start) / 1_000_000n);
resolveRun({ code: code ?? 1, durationMs });
};

child.on("error", (err) => {
console.error("Failed to run pnpm build", err);
settle(1);
});
child.on("close", (code) => settle(code));
});
}
Comment thread
factory-sam marked this conversation as resolved.

const { code, durationMs } = await runBuild();
const durationSec = Math.round(durationMs / 100) / 10;

const metrics = {
status: code === 0 ? "success" : "failure",
durationMs,
durationSec,
timestamp: new Date().toISOString(),
nodeVersion: process.version,
nextVersion: readNextVersion(),
cacheRestored: process.env.NEXT_BUILD_CACHE_HIT === "true",
commit: process.env.GITHUB_SHA ?? null
};

mkdirSync(dirname(reportPath), { recursive: true });
writeFileSync(reportPath, `${JSON.stringify(metrics, null, 2)}\n`);

const summary =
`### Build metrics\n\n` +
`- Status: ${metrics.status}\n` +
`- Duration: ${durationSec}s\n` +
`- Cache restored: ${metrics.cacheRestored}\n` +
`- Next.js: ${metrics.nextVersion}\n` +
`- Node: ${metrics.nodeVersion}\n`;

console.log(`\nBuild ${metrics.status} in ${durationSec}s (metrics -> ${resolve("reports/build-metrics.json")})`);

if (process.env.GITHUB_STEP_SUMMARY) {
try {
appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${summary}\n`);
} catch {
// Non-fatal: summary is best-effort.
}
}

process.exit(code);
Loading