From 084ff5c3bfaec3b7d92f8e2cba1a8dc10b6f5868 Mon Sep 17 00:00:00 2001 From: Julien Gautier Date: Wed, 29 Apr 2026 21:01:49 +0200 Subject: [PATCH] docs: add ADR-0007 for pre-commit hooks and align documentation references - decisions/0007-pre-commit-hooks-and-conventional-commits.md formalizes Husky + lint-staged + commitlint with Conventional Commits as the local quality-gate baseline. - decisions/README.md index updated. - docs/setup/03 section 8 rewritten to reference the ADR and document the full hook setup (pre-commit, commit-msg, commitlint config). - docs/setup/03 future-work table 'ADR(s)' column removed; future ADR numbers are now assigned at the moment each ADR is written, not pre-reserved. - CLAUDE.md aligned: pre-allocated phase-2 ADR numbers replaced by phase references; a pointer to ADR-0007 added under 'Local quality gates'. --- CLAUDE.md | 7 +- ...e-commit-hooks-and-conventional-commits.md | 106 ++++++ decisions/README.md | 1 + docs/setup/03-angular-nx-monorepo.md | 350 +++++++++++++++--- 4 files changed, 408 insertions(+), 56 deletions(-) create mode 100644 decisions/0007-pre-commit-hooks-and-conventional-commits.md diff --git a/CLAUDE.md b/CLAUDE.md index 7a8017d..299467c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,9 +36,10 @@ The structural choices are recorded as ADRs and summarized below. Any change to - **Frontend (`portal-shell`):** Angular at the latest LTS major — standalone APIs, zoneless change detection, Signals, **CSR only (no SSR)**, Vitest, SCSS — see [ADR-0004](decisions/0004-frontend-stack-angular-csr-zoneless-signals.md). - **Backend (`portal-bff`):** NestJS at the latest stable major, mounted on the Express adapter (Fastify adapter swappable later) — see [ADR-0005](decisions/0005-backend-stack-nestjs.md). - **Persistence:** PostgreSQL (latest stable major) via Prisma — see [ADR-0006](decisions/0006-persistence-postgresql-prisma.md). -- **Sessions / cache:** Redis self-hosted — to be locked-in by ADR-0009 (phase 2). -- **Identity:** dual-audience Microsoft Entra (workforce + Entra External ID for customers), OIDC Auth Code + PKCE flow via `@azure/msal-node` — to be locked-in by ADRs 0007, 0008, 0010 (phase 2). -- **Observability:** Pino structured logs + OpenTelemetry traces with W3C Trace Context propagation — to be locked-in by ADR-0012 (phase 2). +- **Sessions / cache:** Redis self-hosted — to be locked-in in phase 2. +- **Identity:** dual-audience Microsoft Entra (workforce + Entra External ID for customers), OIDC Auth Code + PKCE flow via `@azure/msal-node` — to be locked-in in phase 2. +- **Observability:** Pino structured logs + OpenTelemetry traces with W3C Trace Context propagation — to be locked-in in phase 2. +- **Local quality gates:** Husky + lint-staged + commitlint with Conventional Commits — see [ADR-0007](decisions/0007-pre-commit-hooks-and-conventional-commits.md). - **Runtime:** Node.js latest LTS major. ## Repository status diff --git a/decisions/0007-pre-commit-hooks-and-conventional-commits.md b/decisions/0007-pre-commit-hooks-and-conventional-commits.md new file mode 100644 index 0000000..ce49f0a --- /dev/null +++ b/decisions/0007-pre-commit-hooks-and-conventional-commits.md @@ -0,0 +1,106 @@ +--- +status: accepted +date: 2026-04-29 +decision-makers: R&D Lead +tags: [process] +--- + +# Pre-commit hooks and Conventional Commits + +## Context and Problem Statement + +Without local enforcement, trivial issues — unformatted code, lint errors, ill-formed commit messages — only surface in CI. Each round-trip costs minutes of feedback latency and pollutes the commit history with fixup noise. We need a lightweight, well-known mechanism that catches these issues at commit time and enforces a consistent commit message format, while staying fast enough not to discourage frequent commits. + +Which tooling do we adopt for git pre-commit checks and commit message validation? + +## Decision Drivers + +* Fast feedback: catch trivial issues locally; CI is defense in depth, not the first line. +* Consistency of code style and commit history across contributors. +* Conventional commit history that machines can read (later automation: changelog, release notes, semver bumps). +* Mainstream tooling — no exotic shell hooks, no single-maintainer projects. +* Easy onboarding: no extra runtime to manage beyond Node/pnpm. + +## Considered Options + +* **Husky + lint-staged + commitlint with Conventional Commits.** (Chosen.) +* No git hooks — rely on CI. +* `pre-commit` (Python framework). +* `lefthook` (Go single-binary hook manager). +* Custom shell scripts in `.git/hooks/`. + +## Decision Outcome + +Chosen option: **Husky + lint-staged + commitlint, with [Conventional Commits](https://www.conventionalcommits.org/) as the commit-message specification**. + +Hooks installed under `.husky/`: + +| Hook | Action | +| --- | --- | +| `pre-commit` | `pnpm exec lint-staged` — runs lint and format on staged files only | +| `commit-msg` | `pnpm exec commitlint --edit "$1"` — validates the commit message against Conventional Commits | + +Configuration: +- `package.json`: `"prepare": "husky"` script (Husky 9+ pattern), `lint-staged` config block. +- `commitlint.config.cjs`: `module.exports = { extends: ['@commitlint/config-conventional'] }`. + +Scope of `lint-staged`: only fast checks on staged files (lint, format). Type-check and tests stay in `pnpm nx affected -t lint test build` in CI — running the full graph on every commit would slow commits unacceptably and discourage frequent commits. + +CI re-runs the same checks (defense in depth — hooks can be bypassed with `--no-verify`). + +### Consequences + +* Good, because trivial issues are caught locally and don't pollute PR history. +* Good, because Conventional Commits gives us a machine-readable history — enables automated changelogs, semver inference, and release tooling without retroactive cleanup. +* Good, because Husky / lint-staged / commitlint are the de facto standard in JS/TS projects — wide community, abundant documentation, low surprise factor. +* Good, because lint-staged keeps commit time low (only changed files are checked). +* Bad, because hooks can be bypassed (`git commit --no-verify`); CI must remain authoritative. +* Bad, because Husky 9 changed its installation pattern (no `husky install`, just `husky`); contributors with stale instructions can be confused. Mitigated by the setup guide. +* Bad, because Conventional Commits adds a small learning curve; mitigated by IDE plugins, `commitlint` error messages, and a one-page contributor cheatsheet (future doc). + +### Confirmation + +* `package.json` declares `husky`, `lint-staged`, `@commitlint/cli`, `@commitlint/config-conventional` as `devDependencies`. +* `package.json` has a `"prepare": "husky"` script. +* `.husky/pre-commit` runs `pnpm exec lint-staged` and is executable. +* `.husky/commit-msg` runs `pnpm exec commitlint --edit "$1"` and is executable. +* `commitlint.config.cjs` exists at the workspace root and extends `@commitlint/config-conventional`. +* CI pipeline (future ADR) re-runs lint and format checks on every push, plus a Conventional Commits validation on the PR commit range. + +## Pros and Cons of the Options + +### Husky + lint-staged + commitlint (chosen) + +* Good, because the trio is the de facto standard in the JS/TS ecosystem. +* Good, because each tool is single-purpose and composable. +* Good, because the configuration lives with the repo and is versioned with the code. +* Bad, because three packages instead of one — slightly more dependency surface. + +### No git hooks + +* Good, because zero local setup. +* Bad, because every trivial issue costs a CI round-trip. Wasteful and noisy. + +### `pre-commit` (Python) + +* Good, because language-agnostic, used in mixed-language repos (Python + JS + Go). +* Bad, because adds a Python runtime dependency to a Node-only project — extra setup for contributors, especially under WSL where Python toolchains are not always uniform. + +### lefthook + +* Good, because single Go binary, very fast, parallel hook execution, declarative YAML config. +* Bad, because smaller community than Husky in JS/TS land; less prior art and fewer answers when something breaks. +* Status: kept on the watch list for re-evaluation if commit performance becomes a pain. + +### Custom shell scripts in `.git/hooks/` + +* Good, because zero dependency. +* Bad, because hooks aren't versioned with the repo by default; a wrapper layer is needed anyway. Bricolage. + +## More Information + +* Husky: https://typicode.github.io/husky/ +* lint-staged: https://github.com/lint-staged/lint-staged +* commitlint: https://commitlint.js.org/ +* Conventional Commits: https://www.conventionalcommits.org/ +* Related: future quality / CI ADR will re-run these checks as defense in depth. diff --git a/decisions/README.md b/decisions/README.md index 153d0d3..af8452f 100644 --- a/decisions/README.md +++ b/decisions/README.md @@ -50,3 +50,4 @@ ADRs are listed in numerical order. To slice by topic, filter on the `Tags` colu | [0004](0004-frontend-stack-angular-csr-zoneless-signals.md) | Frontend stack — Angular (latest LTS), standalone, zoneless, Signals, CSR-only, Vitest | accepted | `frontend` | 2026-04-29 | | [0005](0005-backend-stack-nestjs.md) | Backend stack — NestJS over Express, Fastify, Hono | accepted | `backend` | 2026-04-29 | | [0006](0006-persistence-postgresql-prisma.md) | Persistence — PostgreSQL with Prisma | accepted | `data`, `backend` | 2026-04-29 | +| [0007](0007-pre-commit-hooks-and-conventional-commits.md) | Pre-commit hooks and Conventional Commits | accepted | `process` | 2026-04-29 | diff --git a/docs/setup/03-angular-nx-monorepo.md b/docs/setup/03-angular-nx-monorepo.md index 6e1749e..4134bd8 100644 --- a/docs/setup/03-angular-nx-monorepo.md +++ b/docs/setup/03-angular-nx-monorepo.md @@ -1,76 +1,270 @@ -# 🏗️ Angular monorepo with Nx + pnpm +# 🏗️ Bootstrap the Nx monorepo for adastra-portal -> ⚠️ This guide is **pending revision**: the placeholder workspace name (`my-workspace`) and app name (`web`) must be replaced with project-specific values, and the procedure must be extended to cover the Node API / BFF side. Treat the steps below as a baseline only. +> **Status.** Aligned with phase-1 ADRs ([0002](../../decisions/0002-adopt-nx-monorepo-apps-preset.md), [0003](../../decisions/0003-workspace-and-app-naming-convention.md), [0004](../../decisions/0004-frontend-stack-angular-csr-zoneless-signals.md), [0005](../../decisions/0005-backend-stack-nestjs.md), [0006](../../decisions/0006-persistence-postgresql-prisma.md)). Replaces the original placeholder-based draft. ## Goal -Create a scalable and maintainable architecture. +Scaffold the `adastra-portal` Nx monorepo containing: + +- `apps/portal-shell` — Angular SPA frontend (CSR, standalone, zoneless, Signals, Vitest, SCSS, strict TS). +- `apps/portal-bff` — NestJS BFF backend (Express adapter, global `ValidationPipe`, Jest test runner). +- `libs/feature/` and `libs/shared/` — feature and shared libraries. + +Out of scope here (covered by later phases): authentication (Entra ID), sessions (Redis), observability (Pino + OpenTelemetry), audit trail, accessibility / performance / security baselines in CI, downstream API integration, on-prem infrastructure stack. --- -## 1) Workspace creation +## Prerequisites + +- WSL + Zsh — see [01-wsl-terminal-setup.md](01-wsl-terminal-setup.md). +- Node.js latest LTS + pnpm via corepack — see [02-dev-web-stack.md](02-dev-web-stack.md). +- Working tree under the WSL filesystem (`~/dev/`, `~/Works/`), never `/mnt/c/`. +- The repository `adastra-portal` already exists locally with the ADRs and docs committed (you are reading one of them). + +--- + +## 1) Initialize the Nx workspace inside the existing repository + +The repository already carries documentation, ADRs, `.vscode/`, and `.gitignore`. `create-nx-workspace` always generates into a *new* directory, so we generate next to the repo and copy the workspace files in. ```bash -cd ~/dev - -pnpm dlx create-nx-workspace@latest my-workspace \ - --preset=angular-monorepo \ +# 1. Generate a clean Nx workspace next to the repo +cd ~/dev # or any scratch location outside the repo +pnpm dlx create-nx-workspace@latest adastra-portal-bootstrap \ + --preset=apps \ --pm=pnpm \ - --appName=web \ - --style=scss \ + --ci=skip \ + --useGitHub=false + +# 2. Copy the workspace files into the existing repo +cd adastra-portal-bootstrap +cp -v package.json pnpm-workspace.yaml nx.json tsconfig.base.json \ + .prettierrc .prettierignore .editorconfig \ + eslint.config.mjs \ + ~/Works/adastra_portal/ + +# 3. Drop the scratch workspace +cd ~ && rm -rf ~/dev/adastra-portal-bootstrap +``` + +The `apps` preset is generic — it does not privilege a runtime ([ADR-0002](../../decisions/0002-adopt-nx-monorepo-apps-preset.md)). Plugins are added in step 2. + +Verify: + +```bash +cd ~/Works/adastra_portal +pnpm install +ls nx.json package.json tsconfig.base.json +``` + +--- + +## 2) Install Angular and Node plugins + +```bash +pnpm add -D @nx/angular @nx/nest @nx/vite @nx/eslint @nx/js +``` + +--- + +## 3) Generate the frontend app — `portal-shell` + +```bash +pnpm nx g @nx/angular:application portal-shell \ + --directory=apps/portal-shell \ --routing=true \ - --unitTestRunner=vitest + --standalone=true \ + --style=scss \ + --bundler=esbuild \ + --unitTestRunner=vitest \ + --e2eTestRunner=playwright \ + --strict=true \ + --tags="scope:portal-shell,type:app" ``` ---- +### Enable zoneless change detection (ADR-0004) -## 2) Recommended layout +Edit `apps/portal-shell/src/app/app.config.ts`: -```text -apps/ - web/ +```typescript +import { ApplicationConfig, provideZonelessChangeDetection } from '@angular/core'; +import { provideRouter } from '@angular/router'; +import { routes } from './app.routes'; -libs/ - shared/ui/ - shared/data-access/ - shared/util/ - feature/auth/ +export const appConfig: ApplicationConfig = { + providers: [ + provideZonelessChangeDetection(), + provideRouter(routes), + ], +}; ``` ---- +Remove every reference to `zone.js`: -## 3) Generate libraries +- delete any `import 'zone.js'` from `apps/portal-shell/src/main.ts` and `apps/portal-shell/src/test-setup.ts`; +- drop `zone.js` from any `polyfills` array in `apps/portal-shell/project.json`; +- check the lockfile no longer pulls `zone.js`. + +Verify: ```bash -pnpm nx g @nx/angular:library shared-ui -pnpm nx g @nx/angular:library feature-auth +grep -RIn "zone.js" apps/portal-shell || echo "no zone.js references — OK" +``` + +### Strict TypeScript (workspace-wide) + +In `tsconfig.base.json`: + +```jsonc +{ + "compilerOptions": { + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true + } +} ``` --- -## 4) Main commands +## 4) Generate the backend app — `portal-bff` ```bash -pnpm nx serve web -pnpm nx build web -pnpm nx test web -pnpm nx lint web +pnpm nx g @nx/nest:application portal-bff \ + --directory=apps/portal-bff \ + --unitTestRunner=jest \ + --tags="scope:portal-bff,type:app" +``` + +> **Test runner note.** NestJS ships with Jest. Vitest unification across the monorepo is possible (via `@nx/vite:configuration`) and remains a future option — no ADR yet. + +### Wire the global `ValidationPipe` (ADR-0005) + +Edit `apps/portal-bff/src/main.ts`: + +```typescript +import { NestFactory } from '@nestjs/core'; +import { ValidationPipe } from '@nestjs/common'; +import { AppModule } from './app/app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + forbidNonWhitelisted: true, + transform: true, + }), + ); + + // Phase-2 security ADRs will add: helmet, CORS allowlist, cookie-session, + // CSRF protection, rate limiting, auth guards, structured error filter. + + await app.listen(process.env.PORT ?? 3000); +} +bootstrap(); ``` --- -## 5) Workspace-wide commands +## 5) Add PostgreSQL + Prisma (ADR-0006) ```bash -pnpm nx run-many -t lint test build -pnpm nx affected -t lint test build +pnpm add -D prisma +pnpm add @prisma/client nestjs-prisma + +mkdir -p apps/portal-bff/prisma +pnpm exec prisma init \ + --datasource-provider postgresql \ + --schema apps/portal-bff/prisma/schema.prisma +``` + +Wire `PrismaModule` in `apps/portal-bff/src/app/app.module.ts`: + +```typescript +import { Module } from '@nestjs/common'; +import { PrismaModule } from 'nestjs-prisma'; + +@Module({ + imports: [ + PrismaModule.forRoot({ isGlobal: true }), + ], +}) +export class AppModule {} +``` + +Commit a `.env.example` (the actual `.env` is git-ignored): + +```dotenv +DATABASE_URL="postgresql://portal:portal@localhost:5432/portal_dev?schema=public" +``` + +> **Out of scope.** PostgreSQL provisioning (HA, backups, RLS policies for the dual-audience model) and Prisma migration deployment will be handled by infrastructure ADRs (phase 3). + +--- + +## 6) Generate the first libraries + +Convention ([ADR-0003](../../decisions/0003-workspace-and-app-naming-convention.md)): +- `libs/feature/` for vertical features; +- `libs/shared/` for cross-cutting concerns. + +Examples: + +```bash +# Frontend feature lib +pnpm nx g @nx/angular:library feature-auth \ + --directory=libs/feature/auth \ + --standalone=true \ + --tags="scope:portal-shell,type:feature" + +# Shared UI lib (frontend-only) +pnpm nx g @nx/angular:library shared-ui \ + --directory=libs/shared/ui \ + --standalone=true \ + --tags="scope:portal-shell,type:shared" + +# Shared utility lib (consumed by both apps) +pnpm nx g @nx/js:library shared-util \ + --directory=libs/shared/util \ + --tags="scope:shared,type:shared" ``` --- -## 6) Prettier +## 7) Enforce module boundaries -`.prettierrc`: +In `eslint.config.mjs`, configure `@nx/enforce-module-boundaries` with the project tags above: + +```js +{ + '@nx/enforce-module-boundaries': [ + 'error', + { + depConstraints: [ + { sourceTag: 'scope:portal-shell', onlyDependOnLibsWithTags: ['scope:portal-shell', 'scope:shared'] }, + { sourceTag: 'scope:portal-bff', onlyDependOnLibsWithTags: ['scope:portal-bff', 'scope:shared'] }, + { sourceTag: 'type:app', onlyDependOnLibsWithTags: ['type:feature', 'type:shared'] }, + { sourceTag: 'type:feature', onlyDependOnLibsWithTags: ['type:feature', 'type:shared'] }, + { sourceTag: 'type:shared', onlyDependOnLibsWithTags: ['type:shared'] }, + ], + }, + ], +} +``` + +This prevents `portal-shell` from importing `portal-bff` code (and vice versa), and keeps `shared-*` libraries free of feature-level dependencies. + +--- + +## 8) Prettier, git hooks, and Conventional Commits + +See [ADR-0007](../../decisions/0007-pre-commit-hooks-and-conventional-commits.md) for the rationale (Husky + lint-staged + commitlint with Conventional Commits). + +`.prettierrc` (project-wide): ```json { @@ -80,37 +274,87 @@ pnpm nx affected -t lint test build } ``` ---- - -## 7) Git hooks +Install Husky, lint-staged, commitlint: ```bash -pnpm add -D husky lint-staged +pnpm add -D husky lint-staged @commitlint/cli @commitlint/config-conventional pnpm exec husky init ``` +`.husky/pre-commit`: + +```bash +pnpm exec lint-staged +``` + +`.husky/commit-msg`: + +```bash +pnpm exec commitlint --edit "$1" +``` + +`commitlint.config.cjs` (workspace root): + +```js +module.exports = { extends: ['@commitlint/config-conventional'] }; +``` + +`package.json` excerpt: + +```json +{ + "scripts": { "prepare": "husky" }, + "lint-staged": { + "*.{ts,html,scss,md,json}": ["pnpm nx format:write --uncommitted"] + } +} +``` + --- -## 8) CI/CD +## 9) CI/CD -> ⚠️ The original procedure targeted GitLab CI. The project repository now lives on Gitea, so this section needs to be rewritten for Gitea Actions (or the chosen CI runner). Tracked under the next round of CI ADRs. - ---- - -## 9) Daily workflow +The pipeline targets the Gitea repository (`gitea@git.unespace.com:julien/adastra_portal.git`). Pipeline shape (Gitea Actions vs. third-party runner) and branch protection are deferred to phase-3 ADRs (planned: ADR-0017). Locally, the equivalent of the future CI check is: ```bash -pnpm install -pnpm start -pnpm nx g component login pnpm nx affected -t lint test build ``` --- -## Result +## 10) Daily workflow -* Scalable monorepo -* Optimized builds -* Fast CI -* Clear architecture +```bash +# Dev servers +pnpm nx serve portal-shell # http://localhost:4200 +pnpm nx serve portal-bff # http://localhost:3000 + +# Tests +pnpm nx test portal-shell # Vitest +pnpm nx test portal-bff # Jest + +# Quality sweeps +pnpm nx run-many -t lint test # full +pnpm nx affected -t lint test build # only what changed since main + +# Generators +pnpm nx g @nx/angular:component --project=portal-shell --standalone +pnpm nx g @nx/nest:resource --project=portal-bff +``` + +--- + +## What this guide does *not* cover (yet) + +| Concern | Phase | +| --- | --- | +| Authentication (Entra ID workforce + External ID) | 2 | +| Sessions / cache (Redis self-hosted) | 2 | +| Downstream API access (existing apps) | 2 | +| Observability (Pino + OpenTelemetry + W3C) | 2 | +| Audit trail | 2 | +| Accessibility baseline (WCAG 2.2 AA + axe-core in CI) | 3 | +| Performance budgets (Lighthouse CI) | 3 | +| Security baseline (CSP, dep / secret scanning, OWASP ASVS) | 3 | +| CI/CD on Gitea | 3 | +| On-prem infrastructure stack (orchestration, Postgres HA, Redis HA, observability backend) | 3 |