From b3a87d2129493aea31b6e9a6e5fd67303f54d7e2 Mon Sep 17 00:00:00 2001 From: moose-lab Date: Sat, 30 May 2026 23:12:20 +0800 Subject: [PATCH] chore: add pre-commit quality gate --- .github/workflows/ci.yml | 17 ++--------- .husky/pre-commit | 1 + README.md | 11 +++++++ bun.lock | 3 ++ package.json | 8 ++++- src/core/__tests__/quality-gates.test.ts | 37 ++++++++++++++++++++++++ 6 files changed, 62 insertions(+), 15 deletions(-) create mode 100755 .husky/pre-commit create mode 100644 src/core/__tests__/quality-gates.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 07b4a5a..02b8ae4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,20 +33,9 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile - - name: Typecheck - run: bun run typecheck - - - name: Test - # TZ is pinned to Asia/Shanghai because cost-tracker.test.ts uses + - name: Quality gate + # quality:ci pins TZ for tests because cost-tracker.test.ts uses # +08:00 timestamps and aggregateCostReport bucket boundaries are # computed via dayjs(...).startOf("day"), which is system-TZ-coupled. # TODO: make aggregateCostReport TZ-agnostic and drop this pin. - env: - TZ: Asia/Shanghai - run: bun run test - - - name: Build (Next.js) - run: bun run build - - - name: Build (CLI) - run: bun run build:cli + run: bun run quality:ci diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..87b4ab3 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +bun run quality:precommit diff --git a/README.md b/README.md index 7a7cc97..47978c4 100644 --- a/README.md +++ b/README.md @@ -129,10 +129,21 @@ bun run dev # Run tests bun run test +# Run the pre-commit quality gate +bun run quality:precommit + +# Run the full CI quality gate +bun run quality:ci + # Run CLI locally bun dist/cli.js --help ``` +`bun install` installs Husky hooks through the `prepare` script. The pre-commit +hook runs `bun run quality:precommit` to check staged whitespace, TypeScript, and +tests before code is committed. CI runs `bun run quality:ci`, which adds the +production dashboard and CLI builds. + --- ## License diff --git a/bun.lock b/bun.lock index d79c09c..caf74b2 100644 --- a/bun.lock +++ b/bun.lock @@ -44,6 +44,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "husky": "^9.1.7", "shadcn": "^3.8.4", "tailwindcss": "^4", "tsup": "^8.5.1", @@ -904,6 +905,8 @@ "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], diff --git a/package.json b/package.json index 8d21fdf..b4cd285 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "scripts" ], "scripts": { + "prepare": "husky", "dev": "next dev", "build": "next build", "build:cli": "tsup", @@ -19,7 +20,11 @@ "start": "next start", "test": "node --test --import tsx --test-reporter spec 'src/core/__tests__/*.test.ts' 'src/components/sessions/*.test.ts'", "test:e2e:task-launch": "node scripts/task-launch-e2e.mjs", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "quality:precommit": "git diff --cached --check && bun run typecheck && TZ=Asia/Shanghai bun run test", + "quality:build": "bun run build:web && bun run build:cli", + "quality:ci": "bun run typecheck && TZ=Asia/Shanghai bun run test && bun run quality:build", + "quality": "bun run quality:ci" }, "keywords": [ "claude", @@ -80,6 +85,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "husky": "^9.1.7", "shadcn": "^3.8.4", "tailwindcss": "^4", "tsup": "^8.5.1", diff --git a/src/core/__tests__/quality-gates.test.ts b/src/core/__tests__/quality-gates.test.ts new file mode 100644 index 0000000..f32ade3 --- /dev/null +++ b/src/core/__tests__/quality-gates.test.ts @@ -0,0 +1,37 @@ +import assert from "node:assert/strict"; +import { readFileSync, statSync } from "node:fs"; +import { test } from "node:test"; + +const repoRoot = new URL("../../../", import.meta.url); + +function readRepoFile(path: string): string { + return readFileSync(new URL(path, repoRoot), "utf8"); +} + +test("package scripts define the shared DevLog quality gates", () => { + const pkg = JSON.parse(readRepoFile("package.json")); + + assert.equal(pkg.scripts.prepare, "husky"); + assert.equal( + pkg.scripts["quality:precommit"], + "git diff --cached --check && bun run typecheck && TZ=Asia/Shanghai bun run test", + ); + assert.equal( + pkg.scripts["quality:build"], + "bun run build:web && bun run build:cli", + ); + assert.equal( + pkg.scripts["quality:ci"], + "bun run typecheck && TZ=Asia/Shanghai bun run test && bun run quality:build", + ); + assert.equal(pkg.scripts.quality, "bun run quality:ci"); +}); + +test("Husky pre-commit hook runs the shared pre-commit gate", () => { + const hookPath = new URL(".husky/pre-commit", repoRoot); + const hook = readFileSync(hookPath, "utf8"); + const mode = statSync(hookPath).mode; + + assert.match(hook, /bun run quality:precommit/); + assert.equal(mode & 0o111, 0o111); +});