## Summary
Wire up Renovate to keep dependencies fresh without manual triage.
- **Workflow** — `.gitea/workflows/renovate.yml`: cron daily at 03:00 UTC + `workflow_dispatch`, runs on the existing self-hosted runners. Picked 03:00 specifically so Monday's tick sits inside Renovate's default `lockFileMaintenance.schedule` ("before 4am Monday") and triggers the weekly lockfile refresh in passing.
- **Config** — `renovate.json`: `config:recommended` baseline + groupings (Angular, Nx, NestJS, Prisma, Vitest, TypeScript tooling, ESLint, SWC, Tailwind), Conventional-Commits commit messages, OSV.dev as vulnerability source, dependency dashboard issue.
- **Docs** — new §5 in `docs/development.md` covering the bot onboarding (manual, one-shot), how to trigger Renovate manually, how to review its PRs. The placeholder roadmap entry is dropped from §8.
No ADR — Renovate is operational tooling, not an architectural decision (and on Gitea it's the only viable bot anyway, no real alternative to capture).
## Manual setup required after merge
The workflow is wired but inert until the bot is onboarded once on Gitea:
1. Create a non-admin Gitea user `apf-portal-bot`.
2. Add the bot as a **Write** collaborator on this repo.
3. Sign in as the bot, generate a PAT (scopes: read/write `repository`, read/write `issue`, read `user`).
4. Add the PAT as the repo secret `RENOVATE_TOKEN` (Settings → Actions → Secrets).
Detailed steps in [`docs/development.md`](docs/development.md) → "Dependency updates (Renovate)".
## Test plan
- [ ] After bot onboarding, trigger the workflow manually (Repo → Actions → Renovate → Run workflow).
- [ ] First run creates the "Renovate Dependency Dashboard" issue and a batch of grouped PRs (Angular, Nx, …).
- [ ] Each generated PR has CI green (`check`, `scan`, `commits`, `perf`, `a11y`).
- [ ] Commit subject matches `chore(deps): …` / `fix(deps): …` so the `commits` gate doesn't reject.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #11
25 KiB
Development guide
This document is the day-to-day reference for working on apf_portal. It covers the repo layout, the prerequisites, the initial setup from a fresh clone, and the commands you'll run during a typical development cycle. It is meant to grow — add sections as the team's workflow does.
For decision rationale, see the ADRs. For onboarding the local environment (terminal, Node, pnpm), see setup/.
1. Repo layout
apf_portal/
├── .gitea/workflows/ # CI pipelines (ADR-0015)
│ ├── ci.yml # per-PR + push to main: check / scan / commits / perf / a11y
│ └── security-scheduled.yml # weekly full-tree scan + prod Lighthouse
├── .github/ # Nx AI-tooling skills, prompts, agents (Nx-managed)
├── .husky/ # local git hooks (ADR-0007)
│ ├── pre-commit # → pnpm exec lint-staged
│ └── commit-msg # → pnpm exec commitlint
├── apps/
│ ├── portal-shell/ # Angular 21 SPA (zoneless, standalone, Signals, Vitest, SCSS)
│ │ ├── public/ # static assets
│ │ ├── src/ # entry, app config, routes, styles
│ │ ├── postcss.config.js # Tailwind PostCSS plugin
│ │ └── project.json # Nx project config (build, serve, test, lint targets)
│ ├── portal-shell-e2e/ # Playwright e2e for portal-shell
│ ├── portal-bff/ # NestJS 11 BFF (Express adapter, ValidationPipe, Jest)
│ │ ├── src/ # main, app module, controllers, services
│ │ ├── prisma/schema.prisma # Prisma 7 schema (postgresql)
│ │ ├── prisma.config.ts # Prisma 7 TS config (loads DATABASE_URL from .env)
│ │ ├── .env.example # env-vars catalog (committed); .env stays gitignored
│ │ └── project.json
│ └── portal-bff-e2e/ # Jest e2e for portal-bff
├── libs/
│ ├── feature/<name>/ # vertical feature libs (e.g. feature-auth)
│ └── shared/<scope>/ # cross-cutting libs (tokens, ui, util)
├── docs/
│ ├── README.md # doc index
│ ├── decisions/ # ADRs (MADR 4.0.0)
│ ├── setup/ # local-environment onboarding (Zsh, pnpm, Nx workspace)
│ └── development.md # this file
├── notes/ # personal scratchpad (gitignored)
├── CLAUDE.md # project rules + architecture summary
├── commitlint.config.cjs # Conventional Commits config
├── eslint.config.mjs # workspace ESLint with module boundaries
├── lighthouserc.js # Lighthouse CI thresholds (ADR-0017)
├── nx.json # Nx workspace config
├── package.json # workspace deps + scripts
├── pnpm-workspace.yaml # apps/* + libs/**
├── tsconfig.base.json # shared TS strict config
└── vitest.workspace.ts # Vitest workspace projects
The conventions that govern this layout are recorded in:
- ADR-0002 — Nx workspace shape
- ADR-0003 — naming convention (
portal-shell,portal-bff,feature-<name>,shared-<scope>) - ADR-0007 — local hooks
- ADR-0015 — CI/CD shape
2. Prerequisites
A working dev machine for apf_portal needs:
| Tool | Why | How |
|---|---|---|
| WSL 2 + Debian (Windows) or Linux/macOS native | All commands assume a POSIX shell | see setup/01 |
| Node.js 24 (latest LTS) | Runtime, pinned in .nvmrc |
nvm install 24 && nvm use (see setup/02) |
| pnpm 10+ | Mandatory package manager (no npm/yarn lockfile) | corepack enable && corepack prepare pnpm@latest --activate |
| Git ≥ 2.40 | Husky 9 + signed commits eventually | usually default |
| mkcert | Local HTTPS for cookie-prefix __Host- (ADR-0009) |
apt install mkcert then mkcert -install |
| Trivy (optional, locally) | Dep vuln scan when running ci:scan locally; CI uses an action |
apt install trivy or trivy install docs |
| gitleaks (optional, locally) | Same pattern; CI uses an action | apt install gitleaks or gitleaks install docs |
| Docker | For Postgres + Redis containers in dev (until on-prem infra ADR lands) | Docker Desktop on Windows, Docker Engine on Linux |
Work inside the WSL filesystem (~/Works/...), never /mnt/c/... — the latter has severe I/O penalties that break Nx caching.
3. Initial setup from a fresh clone
git clone gitea@git.unespace.com:julien/apf_portal.git
cd apf_portal
# Install deps (also runs `husky` to wire git hooks)
pnpm install
# Generate the Prisma client (until you set up the DB it errors on
# missing DATABASE_URL — that's expected; the generation only reads
# the schema, not the DB).
cd apps/portal-bff && pnpm exec prisma generate && cd ../..
# Sanity check
pnpm nx run-many -t lint test build
For the BFF to actually run end-to-end, you'll also need:
# .env from .env.example, then fill in DATABASE_URL with your local Postgres
cp apps/portal-bff/.env.example apps/portal-bff/.env
# Local Postgres via Docker (one-liner; will be replaced by a Docker Compose file in a later doc)
docker run -d --name apf-postgres -p 5432:5432 \
-e POSTGRES_USER=portal -e POSTGRES_PASSWORD=portal -e POSTGRES_DB=portal_dev \
postgres:17-alpine
A proper local-dev infra spec (Postgres HA-like, Redis, OTel collector) will land with the on-prem infrastructure ADR; in the meantime the one-liner above is sufficient to run the BFF.
4. Daily commands
Run the apps
pnpm nx serve portal-shell # http://localhost:4200 (Angular dev server)
pnpm nx serve portal-bff # http://localhost:3000/api (NestJS)
Both can run in parallel in two terminals; the SPA proxies API calls to the BFF in dev.
Test
pnpm nx test portal-shell # Vitest (single run; --configuration=watch for watch mode)
pnpm nx test portal-bff # Jest
pnpm nx run-many -t test # all projects
pnpm nx affected -t test # only projects affected since main
Run a single Vitest file:
pnpm nx test portal-shell --testFile=src/app/app.spec.ts
Lint, type-check, format
pnpm nx lint portal-shell # one project
pnpm nx run-many -t lint # all projects
pnpm nx affected -t lint # affected only
pnpm nx affected -t lint --fix # auto-fix where possible
pnpm nx affected -t type-check # explicit type-check (independent of test/build)
pnpm nx format:write # apply Prettier
pnpm nx format:check # CI-style verification
Build
pnpm nx build portal-shell # development build
pnpm nx build portal-shell --configuration=production # production build
pnpm nx run-many -t build
pnpm nx affected -t build
Generate
# Component in portal-shell
pnpm nx g @nx/angular:component <name> --project=portal-shell --standalone
# Service / controller / module in portal-bff
pnpm nx g @nx/nest:service <name> --project=portal-bff
pnpm nx g @nx/nest:controller <name> --project=portal-bff
# New shared lib (TS-only, consumable by both apps)
pnpm nx g @nx/js:library --name=shared-<scope> --directory=libs/shared/<scope> \
--bundler=tsc --unitTestRunner=vitest \
--tags="scope:shared,type:shared" --no-interactive
# New Angular feature lib (front-only)
pnpm nx g @nx/angular:library --name=feature-<name> --directory=libs/feature/<name> \
--standalone=true --unitTestRunner=vitest-analog \
--tags="scope:portal-shell,type:feature" --no-interactive
Sweep generated files for
process.env.X(dot notation) →process.env['X'](bracket notation), required by the strict-TS optionnoPropertyAccessFromIndexSignature: true. The Nx generators don't emit bracket form.
Prisma
# Regenerate the typed client after schema changes
cd apps/portal-bff && pnpm exec prisma generate && cd ../..
# Create and apply a migration in dev
cd apps/portal-bff && pnpm exec prisma migrate dev --name <migration-name> && cd ../..
# Deploy migrations in prod (run by deploy pipeline, not locally)
cd apps/portal-bff && pnpm exec prisma migrate deploy && cd ../..
# Inspect the dev DB
cd apps/portal-bff && pnpm exec prisma studio && cd ../..
CI scripts (runnable locally)
Mirror what the CI does on every PR:
pnpm ci:check # nx affected -t format:check lint test build
pnpm ci:audit # pnpm audit --audit-level=moderate
pnpm ci:commits # commitlint on the PR commit range (uses $COMMIT_LINT_FROM, defaults to origin/main)
pnpm ci:perf # production build + Lighthouse CI against the static-served bundle
ci:scan (Trivy + gitleaks) is currently invoked from CI YAML rather than as a pnpm script — those tools are Go binaries without clean npm wrappers. Run them locally if you've installed the binaries.
5. Dependency updates (Renovate)
Renovate runs as a scheduled workflow (.gitea/workflows/renovate.yml) and opens PRs against main for dependency updates. Daily at 03:00 UTC, plus on-demand via workflow_dispatch.
Behaviour is controlled by renovate.json at the repo root: groupings (Angular, Nx, NestJS, Prisma, Vitest, TypeScript tooling, ESLint, SWC, Tailwind), Conventional-Commits-compatible commit messages (chore(deps): … / fix(deps): … for vulnerability fixes), weekly lockfile maintenance, OSV.dev as the vulnerability data source.
One-time bot onboarding
Renovate authenticates as a dedicated bot user. Setup is manual on Gitea — done once per Gitea instance, then the workflow runs unattended.
- Create a bot user. Site Administration → Users → Create User. Suggested name:
apf-portal-bot. Strong password, mark as non-admin (least privilege). - Add the bot as a collaborator on this repo with Write access (Settings → Collaborators). Without write, Renovate can't push branches.
- Generate a PAT for the bot. Sign in as the bot, then User Settings → Applications → Generate New Token. Scopes needed: read/write
repository, read/writeissue, readuser. Avoidadmin. - Store the PAT as a repo secret. Settings → Actions → Secrets → New Secret. Name:
RENOVATE_TOKEN. Value: the token from step 3. - Sign out and forget the token locally. It is now only retrievable via the secret store.
To rotate the PAT: regenerate at step 3, update the secret at step 4. The schedule keeps running unattended with the new token.
Triggering manually
Repo → Actions → "Renovate" workflow → Run workflow. Useful when you've just changed renovate.json and want the next pass to happen immediately rather than wait for the next 03:00 UTC tick.
Reviewing Renovate PRs
- Each Renovate PR is gated by the same CI as a human PR —
check,scan,commits,perf,a11y. Don't merge until all are green. - The "Renovate Dependency Dashboard" issue (auto-created on first run) lists every pending update grouped by status. Use it to triage which PRs to expedite.
- For a major bump that introduces breaking changes, don't reflexively merge: read the changelog, then either accept the work or close the PR with a "rejected" label. Renovate respects that label and won't keep re-opening the same major.
- Adding or removing a dependency belongs in a feature PR, not in Renovate's scope. Renovate only updates versions of existing deps.
6. Conventional commit cycle
-
Branch from
mainwith a short slug:git switch -c feat/portal-shell/auth-login # or fix/..., chore/..., docs/... -
Commit using Conventional Commits. The local
commit-msghook (commitlint) rejects anything else.feat(portal-shell): add login flow stub fix(portal-bff): correct env var bracket access chore: bump @nx/* to 22.7.2 docs(decisions): add ADR-0018 for security baselinepre-commitrunslint-staged→ Prettier on staged files. Lint and tests stay in CI. -
Push and open a PR against
main. The CI runs:check(lint, type-check, test, build on affected)scan(audit, Trivy, gitleaks)commits(commitlint on the PR commit range)perf(Lighthouse on the production bundle)a11y(axe-core; placeholder until first real screens)
All five must be green to merge. PR title must itself be a Conventional Commits message — it becomes the squash-merge subject (ADR-0015).
-
Squash-merge into
main. Branch is auto-deleted. Linear history maintained. -
To cut a release: tag
vX.Y.Zonmain. Therelease.ymlworkflow will pick it up (currently a stub; populated alongside the on-prem deploy ADR).
PR conventions
The squash-merge subject on main is the PR title, not the individual commits on the feature branch (those collapse into the squash). Two practical consequences:
- The PR title must itself be a valid Conventional Commits message. Same format as a commit message —
<type>(<scope>): <description>, imperative mood, lowercase, no trailing period, target ≤ 70 chars. The CIcommitsjob (commitlint on the PR commit range) catches violations. - Individual commits on the feature branch can be exploratory. The local
commit-msghook still validates each commit's format, but the squash makes granular history irrelevant onmain. Granular history stays available in the PR for review.
Type vocabulary
| Type | When |
|---|---|
feat |
new user-facing feature or capability |
fix |
bug fix |
docs |
documentation only (no code) |
style |
formatting / whitespace (no logic change) |
refactor |
code change that is neither a fix nor a feature |
perf |
performance improvement |
test |
tests added or updated |
build |
build system, dependencies |
ci |
CI configuration |
chore |
maintenance, scaffolding, project metadata |
revert |
revert a previous commit |
Scope vocabulary (optional)
| Scope | Examples |
|---|---|
| App | portal-shell, portal-bff |
| Lib | shared-tokens, shared-ui, shared-util, feature-auth |
| Cross-cutting | decisions (ADR work), docs, ci, deps |
Scope is optional. Omit when the change spans too many areas to scope cleanly (e.g., a workspace-level rename).
PR body template
When a PR is opened against main, Gitea pre-populates the body from .gitea/pull_request_template.md:
- Summary — 1–3 bullets describing what changed.
- Motivation — why, with ADR / issue / incident links.
- Implementation notes — trade-offs, alternatives considered, follow-ups deferred.
- Verification — CI gates checked, manual test description, ADR / diagram update flags.
- Related — ADR-XXXX, related PRs, follow-up issues.
The template guides without enforcing — sections can be left blank when irrelevant. The point is to make "what does the reviewer need to know" explicit, not to add ceremony.
7. Where to look
| Question | Doc |
|---|---|
| Project rules and the why behind them | CLAUDE.md |
| All ADRs (decisions index) | docs/decisions/README.md |
| Initial environment setup (Zsh, Node, pnpm) | docs/setup/ |
| RSSI briefing for ASVS / HDS / etc. | notes/asvs-level-decision-briefing-rssi.md (gitignored, personal) |
| The dev-team rationale for the UI stack | notes/argumentaire-stack-ui-spartan-cdk-tailwind.md (gitignored, personal) |
8. Sections to come — roadmap by phase
This doc starts as a phase-1 + cross-cutting reference. As features for later phases land, the corresponding sections below are filled in directly. Each entry is mapped to the ADR / implementation work that unlocks it, so a contributor can see when each section becomes real and what triggers it.
When a section grows beyond a short subsection, it is extracted to its own file under docs/development/. Per the documentation convention (see README.md), we group into a folder once we have at least three related files; this doc is then re-organised into an index pointing at the extracted files. Until then, all sections live here.
| Future section | Phase | Triggered by |
|---|---|---|
| Local infra recipe — Docker Compose for Postgres, Redis, OTel Collector, and a Postgres-friendly viewer. | 2 / 3b | First feature that needs Redis (sessions, ADR-0010). Earlier if the BFF gains a meaningful amount of business code. |
| Auth dev-loop — Microsoft 365 Developer tenant configuration, MSAL Node connection, OIDC code-flow walkthrough, switching between dev and prod-like tenants. | 2 | Auth flow code lands (ADR-0009) once the dev tenant is provisioned by IT. |
Session inspection — reading the Redis session store in dev, decrypting the AES-GCM tokens blob with the dev key, force-logout patterns. |
2 | Sessions module lands (ADR-0010). |
MFA step-up debugging — triggering claims-challenge flows, verifying mfaVerifiedAt freshness, testing the SPA HTTP interceptor that handles 401 + claims challenge. |
2 | First @RequireMfa() route lands (ADR-0011). |
Observability dev-loop — running an OTel collector container locally, viewing traces (Tempo / Jaeger UI), reading Pino logs with pino-pretty, correlating front spans to BFF spans via traceparent. |
2 | OTel SDK setup lands (ADR-0012); needs the local collector from "Local infra recipe". |
Audit-log inspection workflow — querying audit.events as audit_reader, joining with app logs by trace_id, validating the append-only role grants in dev. |
2 | Audit module lands (ADR-0013). |
Downstream API integration recipe — adding a new DownstreamApiConfig, choosing the auth strategy (OBO vs service+assertion), wiring resilience policies, testing with a mocked downstream. |
2 | First downstream client lands (ADR-0014). |
| Component patterns library — the in-house, spartan-style components (Angular CDK + Tailwind) as they ship, with a11y notes per component (keyboard model, ARIA, screen-reader expectations). | 5b suite | First non-placeholder component in libs/shared/ui/. |
| a11y testing workflow — running axe-core via Playwright locally, screen-reader testing notes (NVDA / VoiceOver / TalkBack), the APF user-panel cadence and how to triage findings. | 3a | First Playwright e2e suite touching real screens (ADR-0016). |
Performance debugging — running Lighthouse CI locally with full config, reading the HTML reports, using source-map-explorer to investigate bundle bloat, interpreting BFF p95/p99 from OTel. |
3a | Lighthouse already wired in CI (ADR-0017); section grows when first real route is added to the critical-routes list. |
| Debugging tips — Angular DevTools, NestJS inspector, Prisma query log, OTel trace navigation, common gotchas. | cross | Accumulates organically as the team encounters them. |
Release workflow — tag-driven release, what release.yml does, version bumping, changelog generation from Conventional Commits. |
3b | On-prem infrastructure ADR + populated release.yml. |
| GitLab migration runbook — when the org migrates Gitea → GitLab, how the workflows are ported, which level-2 sections of ADR-0015 get superseded. | future | GitLab migration ADR (6–18 months horizon). |
| Architecture overview diagrams — high-level component diagrams, data-flow diagrams, trust boundaries (for security review). | cross | First major architecture review or onboarding cohort ≥ 3 contributors. |