From 1206a1d23d40125740f198ecb85f863b8bbd42b6 Mon Sep 17 00:00:00 2001 From: laurenhitchon Date: Thu, 30 Jul 2026 13:43:56 +1000 Subject: [PATCH] docs(onboarding): rewrite as a complete new-repo fleet runbook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ONBOARDING.md: full path from repo creation to fleet membership — repo creation with branch auto-delete, package.json baseline (engines, gate scripts), toolchain install (prettier/eslint packages with entry-point choice), sync group selection, deploy key, Protect main ruleset recipe (7 contexts, DeployKey bypass), Snyk import, Renovate selection, CI_SKIP variables, npm trusted publishing, verification PR. Existing-repo pre-flight retained as Path B. README: seven stubs (ci.yml merge gate was missing from the checks table), group table includes the config packages, repo-local automation in layout. linting-and-formatting: fleet packages replace the per-repo-config and 80-column narratives; gate table includes format + Snyk. config-single-source-of-truth: fleet count and prettier-key consumers. --- ONBOARDING.md | 403 ++++++++++++++---- README.md | 17 +- docs/best-practices/linting-and-formatting.md | 74 ++-- docs/config-single-source-of-truth.md | 4 +- 4 files changed, 372 insertions(+), 126 deletions(-) diff --git a/ONBOARDING.md b/ONBOARDING.md index 7ffbb22..a7c9a9a 100644 --- a/ONBOARDING.md +++ b/ONBOARDING.md @@ -1,57 +1,78 @@ # Onboarding a repo onto the shared tooling -Adding a repo is mostly one line in `.github/sync.yml`, but the pre-flight -checks matter — every latent problem a repo has (broken lockfile, stale build -artifacts, odd branch protection) surfaces the moment the first sync PR opens -its checks. Both of those examples actually happened during the initial -rollout, so do the pre-flight properly and the onboarding is boring. +This is the complete path from a repo that does not exist yet (or exists +outside the fleet) to a fully configured fleet member. A fleet member has: +the synced tooling files, the seven shared CI workflows, a protected `main` +with required checks, Renovate and Snyk coverage, the shared ESLint and +Prettier configs, and the release pipeline. -## 1. Pre-flight checks +Start at **Path A** for a brand-new repo. For an existing repo, run the +**Path B pre-flight** first, then continue from Path A step 2. -Run these before touching sync.yml. Five minutes here saves an afternoon. +## What a fleet repo ends up with -**a) The lockfile parses.** Sounds silly; isn't. nswds-public-sans sat on -`main` for three weeks with unresolved merge-conflict markers inside -`package-lock.json` (two Snyk PRs merged without regenerating it), and every -`npm ci` failed. The new commitlint check was just the first thing to notice. +| Layer | Contents | Delivered by | +|---|---|---| +| Synced files | `scripts/` (shared shell tooling), `commit-types.mjs`, `commitlint.config.mjs`, `git-conventional-commits.yaml`, `release.config.mjs` (group 1 only), `renovate.json`, `.nvmrc`, `.npmrc` | File sync (step 5) | +| Workflow stubs | `ci.yml`, `commitlint.yml`, `validate-branch-name.yml`, `commit-types-sync.yml`, `ai-pr-title.yml`, `openai-pr-description.yml`, `release.yml` — thin callers pinned to `@v1` | File sync (step 5) | +| Lint/format config | `@nswds/eslint-config` (npm) + `eslint.config.mjs`; `@nswds/prettier-config` (npm) + `.prettierrc.mjs` or `package.json` key | Manual (steps 2–3) | +| Branch governance | "Protect main" ruleset (7 required checks, DeployKey bypass), branch auto-delete on merge | Manual (steps 1, 7) | +| Release | semantic-release via the synced stub, pushing over `RELEASE_DEPLOY_KEY` | Manual key setup (step 6) | +| Dependency management | Renovate (Mend app, org preset) and Snyk PR scanning | Console (steps 8–9) | + +## Path A — brand-new repo + +### 1. Create the repo ```sh -node -e "JSON.parse(require('fs').readFileSync('package-lock.json','utf8')); console.log('lockfile ok')" +gh repo create digitalnsw/ --private --clone ``` -If it's corrupt: find the last commit where it parsed (`git log -- package-lock.json`, -test each), restore that version, then `npm install --package-lock-only` to -apply whatever package.json changed since. Don't just regenerate from -scratch — that resolves every transitive dep fresh and lights up Snyk. - -**b) No required-status-check ruleset with old names.** Reusable workflows -report checks as `commitlint / commitlint`, not `commitlint`. A ruleset -requiring the old name blocks every PR with "Expected — waiting for status to -be reported" (this bit nswds-design). Check with: +Immediately enable automatic branch deletion after PR merge (the fleet +standard — merged PR branches never linger): ```sh -gh api repos/digitalnsw//rulesets --jq '.[].id' | while read id; do - gh api repos/digitalnsw//rulesets/$id \ - --jq '.name + ": " + ([.rules[] | select(.type=="required_status_checks") | .parameters.required_status_checks[].context] | join(", "))' -done +gh api -X PATCH repos/digitalnsw/ -F delete_branch_on_merge=true ``` -If any required context matches a shared check's old single name, rename it -to the `job / job` form. +Note the GitHub slug is what goes in `sync.yml` later, and it is not always +the local folder name (local `nswds-images` is `digitalnsw/images`). + +### 2. Baseline `package.json` -**c) Does it have a bespoke release pipeline?** If the repo publishes to npm -with its own verification steps, or its `release.yml` is something else -entirely (ictds-portal-flows' is a Power Platform prod deploy), it does NOT -go in group 1. Read the existing `release.yml` before deciding. When in -doubt, exclude the release file(s) first and add them in a follow-up once -you've confirmed the stock ones fit. +Create `package.json` with, at minimum: -**d) Node version.** The reusable workflows use `.nvmrc` when present and -fall back to Node 24. If the repo needs something specific, add a `.nvmrc`. +- `"name"` matching the repo slug. +- The fleet engines range — `engines` is **not** a synced file and the + synced `.npmrc` sets `engine-strict=true`, so without this block the + Node-floor control is inert: -## 2. Prepare the repo itself +```json +"engines": { + "node": "^22.22.2 || >=24.15.0" +} +``` -Install the tooling's dependencies (skip any it already has): +- The helper script entries (the scripts themselves arrive via sync): + +```json +"branch:create": "scripts/create-branch.sh", +"branch:suggest": "scripts/suggest-branch-name.sh", +"branch:suggest:create": "scripts/suggest-branch-name.sh --create", +"commit": "scripts/git-commit.sh", +"pr": "scripts/pull-request.sh" +``` + +- Gate scripts as applicable: `"lint": "eslint ."`, + `"format": "prettier --write ."`, `"format:check": "prettier --check ."`, + and a real `"test"` script if the repo has a suite. The shared CI jobs run + `lint`/`test` with `--if-present` and the format job only runs when a + Prettier config exists — a missing script means a silently green gate, so + wire up every one the repo can honestly run. + +### 3. Install the toolchain + +Commit/release stack (every repo): ```sh npm install -D husky @commitlint/cli @commitlint/config-conventional \ @@ -60,72 +81,280 @@ npm install -D husky @commitlint/cli @commitlint/config-conventional \ @semantic-release/release-notes-generator ``` -Add the script entries to `package.json`: +Prettier (every repo that has formattable source): + +```sh +npm install -D prettier @nswds/prettier-config +``` + +Then either the `package.json` key (base config only, no local additions): ```json -"branch:create": "scripts/create-branch.sh", -"branch:suggest": "scripts/suggest-branch-name.sh", -"branch:suggest:create": "scripts/suggest-branch-name.sh --create", -"commit": "scripts/git-commit.sh", -"pr": "scripts/pull-request.sh" +"prettier": "@nswds/prettier-config" ``` -Secrets: `OPENAI_API_KEY` is an org-level secret so there's normally nothing -to do. `RELEASE_DEPLOY_KEY` is only needed if the repo's `main` is -ruleset-protected — see MAINTENANCE.md for the recipe. +or, for repos that layer plugins (any Tailwind repo), a `.prettierrc.mjs`: + +```js +import base from '@nswds/prettier-config' with { type: 'json' } + +const config = { + ...base, + plugins: ['prettier-plugin-organize-imports', 'prettier-plugin-tailwindcss'], + tailwindFunctions: ['clsx'], + tailwindStylesheet: './src/app/globals.css', +} + +export default config +``` + +Add a `.prettierignore` that excludes `CHANGELOG.md` (semantic-release +rewrites it unformatted on every release — leaving it checked re-reds the +format gate at the next release) plus any generated or machine-written +output (build artefacts, ORM meta snapshots, generated modules, email +template HTML). + +ESLint — pick the entry point that matches the repo: + +- **Next.js app** — `@nswds/eslint-config` (the `.` entry point): + + ```sh + npm install -D @nswds/eslint-config eslint eslint-config-next \ + eslint-config-prettier eslint-plugin-prettier @eslint/compat + ``` + + ```js + // eslint.config.mjs + import { defineConfig, globalIgnores } from 'eslint/config' + import nswds from '@nswds/eslint-config' + + export default defineConfig([ + ...nswds, + // repo-specific ignores go here + ]) + ``` + +- **Everything else** (IaC, pipelines, node scripts, plain sites) — + `@nswds/eslint-config/base`: + + ```sh + npm install -D @nswds/eslint-config eslint @eslint/js typescript-eslint \ + eslint-config-prettier eslint-plugin-prettier @eslint/compat prettier + ``` + + ```js + // eslint.config.mjs + import nswds from '@nswds/eslint-config/base' + + export default nswds + ``` + +Run `npm run lint`, `npx prettier --write .` and commit the results — the +repo must be gate-clean before the gates become required. -The sync App is installed org-wide ("All repositories"), so there's no app -step for new repos. +### 4. First push -## 3. Add it to the sync +Push `main` with the baseline in place **before** adding branch protection, +or the first push itself is blocked. -In this repo, add the target under the right group in -[.github/sync.yml](.github/sync.yml) (group 1 unless pre-flight said -otherwise), commit, merge to `main`. The sync fires on that push and opens -the PR in the target repo. You can also trigger it manually: Actions → "Sync -shared files to repos" → Run workflow. +### 5. Add the repo to the sync -Note the repo slug is the GitHub name, which isn't always the local folder -name (nswds-images is `digitalnsw/images` on GitHub). +In this repo, add the slug under the right group in +[.github/sync.yml](.github/sync.yml): -## 4. Review and merge the first sync PR +- **Group 1** — the default: full file set including the stock release stub + and `release.config.mjs`. +- **Group 2** — repos with a bespoke `release.yml` AND release config (npm + publishers with their own verification: nswds-ui, nswds-tokens, and the + two `@nswds` config packages). The CI stub lands as `shared-ci.yml` where + the repo keeps its own `ci.yml`. +- **Group 3** — own `release.config.mjs`, stock release stub (nswds-app). +- **Group 4** — `release.yml` is a different pipeline entirely + (ictds-portal-flows: Power Platform deploy); the release stub maps to + `semantic-release.yml`. -What to check in the diff: +When in doubt, exclude the release files first and add them once the stock +ones are confirmed to fit. -- Only expected paths: `scripts/` (shared files only — the sync never touches - the repo's own scripts), the root configs, and `.github/workflows/` stubs. +Merge to `main`; the sync opens a `chore(ci): …` PR in the target repo +(manual trigger: Actions → "Sync shared files to repos" → Run workflow). + +Reviewing that first sync PR: + +- Only expected paths: `scripts/` shared files, the root configs, and + `.github/workflows/` stubs. Nothing repo-specific replaced. - Script exec bits survived (`git ls-tree` on the branch shows `100755`). -- Nothing repo-specific was replaced. +- `check-branch-name` reports red on this one PR only — the check reads + branch policy from the PR *base*, which gains the `chore/repo-sync` + exemption when this PR merges. It is not a required check; merge past it. +- The stub workflows take effect on the *next* PR (GitHub runs + `pull_request` workflows from the base branch), so the full check set + first appears on the verification PR in step 10. -What to expect: +### 6. Release deploy key -- **`check-branch-name` will be red — once.** The check reads branch policy - from the PR *base*, which doesn't have the `chore/repo-sync` exemption - until this very PR merges it in. It's not a required check; merge past it. - Every subsequent sync PR is green. -- The stub workflows only take effect on the *next* PR (GitHub runs - `pull_request` workflows from the base branch's definitions), so open any - trivial follow-up PR afterwards to watch all six stubs go green. +`RELEASE_DEPLOY_KEY` lets semantic-release push release commits to the +protected `main` (the key is the ruleset's bypass actor; the default +`GITHUB_TOKEN` cannot push once required checks are enforced). Follow the +deploy-key recipe in [MAINTENANCE.md](MAINTENANCE.md): generate a keypair, +add the public half as a **write** deploy key named `release-bot`, store the +private half as the `RELEASE_DEPLOY_KEY` repo secret, delete both local key +files. -## 5. Post-merge cleanup +`OPENAI_API_KEY` (AI PR title/description workflows) is an org-level secret +— nothing to do per repo. The sync App is installed org-wide — nothing to +do per repo. -- If the repo had an older copy of the tooling, delete the superseded - commit-types file (`commit-types.js` or `commit-types.cjs`) — the synced - config imports `commit-types.mjs`. (Done for all 17 original repos.) -- Run `./scripts/setup-commitlint.sh` once locally to install the husky - hooks, and sanity-check `npm run commit` works. -- If an old standalone workflow was replaced by a stub (e.g. an existing - `semantic-release.yml`), confirm the next release run succeeds. +### 7. Branch protection -## Coverage +Create the "Protect main" ruleset. Two rules apply to the required-check +list: + +- Contexts from reusable workflows use the two-part `caller job / called + job` form (`install / install`, not `install`). +- Require only contexts the repo **demonstrably receives**. The Snyk + contexts come from the Snyk console integration (step 8) — require them + only after a PR has shown all three `…/snyk (DigitalNSW)` statuses + posting, or every merge blocks on "Expected — waiting for status". + +```sh +gh api -X POST repos/digitalnsw//rulesets --input - <<'EOF' +{ + "name": "Protect main", + "target": "branch", + "enforcement": "active", + "conditions": {"ref_name": {"include": ["~DEFAULT_BRANCH"], "exclude": []}}, + "bypass_actors": [{"actor_type": "DeployKey", "bypass_mode": "always"}], + "rules": [ + {"type": "deletion"}, + {"type": "non_fast_forward"}, + {"type": "required_status_checks", "parameters": { + "strict_required_status_checks_policy": true, + "required_status_checks": [ + {"context": "commitlint / commitlint"}, + {"context": "install / install"}, + {"context": "install / lint"}, + {"context": "install / test"}, + {"context": "install / format"}, + {"context": "security/snyk (DigitalNSW)"}, + {"context": "code/snyk (DigitalNSW)"} + ] + }} + ] +} +EOF +``` + +Bypass policy: **DeployKey only** — no admin or role bypass actors (the +rationale is in MAINTENANCE.md's bypass policy section). + +### 8. Snyk + +Import the repo into the `digitalnsw` Snyk organisation (Snyk console → Add +project → GitHub). Snyk then posts three commit statuses on every PR head — +`code/snyk`, `security/snyk`, `license/snyk (DigitalNSW)` — and retests +`main` against new advisories between PRs. The fleet requires `security` and +`code` as merge gates; `license` stays advisory. + +Snyk posts statuses on PR heads only, never on `main` merge commits — +absence on `main` is normal. If a PR head never receives the statuses +(Snyk sometimes skips force-updated, reopened App-authored branches), the +required contexts block that PR; the unblock is removing the two contexts +from the ruleset for the single merge and restoring them immediately after. + +### 9. Renovate + +Select the repo in the Mend Renovate GitHub App installation +(developer.mend.io/github/digitalnsw). The synced `renovate.json` extends +the org preset (`default.json` in this repo) — policy changes apply on +Renovate's next run with no per-repo work. + +### 10. Verify, then tighten + +Open a trivial PR (a README line). Expected checks: the four `install / *` +jobs, `commitlint / commitlint`, `check-branch-name / check-branch-name`, +`generate-title / generate-title`, `openai-pr-description / +openai-pr-description`, and the three Snyk statuses. Superseding a push +mid-run cancels the in-flight CI run (stub-level concurrency). + +Repo Actions **variables** (not secrets) tune the shared CI where needed: + +| Variable | Effect | Use when | +|---|---|---| +| `CI_SKIP_BUILD=true` | skips the build step in `install / install` | build needs deploy-time env, or is too slow for the 15-minute job timeout | +| `CI_SKIP_LINT=true` / `CI_SKIP_TESTS=true` / `CI_SKIP_FORMAT=true` | skips that job (still satisfies the required check) | temporary red-gate escape hatch while a repo converges | + +### 11. Local developer setup + +Each developer clones and runs: + +```sh +npm install +./scripts/setup-commitlint.sh # installs the husky hooks +npm run commit # sanity check the AI-assisted commit flow +``` + +Branch names must match the enforced types +(`feat|fix|hotfix|release|docs|build|test|refactor|style|chore|export|ai|copilot|cursor|claude|codex`); +`npm run branch:create` builds a compliant name interactively. -All 24 consumer repos are on the sync — the original rollout completed -2026-07-15, and dtl-sandbox was added 2026-07-22 (data, -nswds-community, nswds-signature and nswds-email-issues were onboarded last, -following exactly this doc — their prep PRs are worked examples: both -community and signature needed the lockfile repair from pre-flight (a), -email-issues needed its first lockfile and a real .gitignore, and community -carried the package.json name fix). +### 12. npm publishers only + +A repo that publishes a package does not belong in group 1 — decide its +group in step 5 (own release verification → group 2; own release config +only → group 3). Publishing uses **OIDC trusted publishing**: configure the +trusted publisher on npmjs.com (package → Settings → Trusted Publisher → +GitHub Actions, workflow `release.yml`) so releases carry provenance with +no `NPM_TOKEN` secret. The web UI's stricter publishing-access option can +silently fail to save; `npm access set mfa=publish ` from a +logged-in CLI is the reliable path. + +## Path B — existing repo pre-flight + +Run these before touching `sync.yml`; every latent problem surfaces the +moment the first sync PR opens its checks. + +**a) The lockfile parses and installs.** + +```sh +node -e "JSON.parse(require('fs').readFileSync('package-lock.json','utf8')); console.log('lockfile ok')" +npm clean-install +``` + +If it is corrupt: find the last commit where it parsed +(`git log -- package-lock.json`, test each), restore that version, then +`npm install --package-lock-only` to apply whatever `package.json` changed +since. Do not regenerate from scratch — that resolves every transitive +dependency fresh and floods the security scanner with new findings. + +**b) No required-status-check ruleset using old single-part names.** +Reusable workflows report `commitlint / commitlint`, not `commitlint`; a +ruleset requiring the old name blocks every PR with "Expected — waiting for +status to be reported". + +```sh +gh api repos/digitalnsw//rulesets --jq '.[].id' | while read id; do + gh api repos/digitalnsw//rulesets/$id \ + --jq '.name + ": " + ([.rules[] | select(.type=="required_status_checks") | .parameters.required_status_checks[].context] | join(", "))' +done +``` + +**c) Bespoke release pipeline?** If `release.yml` publishes to npm with its +own verification, or is a different pipeline that shares the filename, the +repo is not group 1 — see the group table in step 5. + +**d) Node version.** The reusable workflows use `.nvmrc` when present +(synced canonical: `24.16.0`) and fall back to Node 24. + +**e) Superseded tooling copies.** Delete any older `commit-types.js` / +`commit-types.cjs` (the synced config imports `commit-types.mjs`), and any +standalone workflow a stub will replace — then confirm the next release run +succeeds after the first sync merge. + +Then continue from Path A step 2, skipping anything the repo already has. + +## Coverage -One known leftover: `nswds-images/package.json` is still named -"ictds-portal-flows" — fix it next time someone's in that repo. +All 24 consumer repos are on the sync. The fleet-wide expectations this doc +targets (ruleset contexts, Snyk gating, format gate, engines range) are +live on every member; a new repo should arrive at the same end state. diff --git a/README.md b/README.md index 6243646..9152054 100644 --- a/README.md +++ b/README.md @@ -59,9 +59,11 @@ one-step rollout (and one-step rollback) for CI logic. ├── sync.yml # WHO gets WHAT (the four groups — see below) └── workflows/ ├── sync.yml # the sync driver (push to main + manual dispatch) - ├── reusable-*.yml # the six real CI implementations (never synced) + ├── reusable-*.yml # the seven real CI implementations (never synced) ├── ci.yml # shellcheck + actionlint, gates every merge here - └── the rest # this repo dogfooding its own stubs via local references + ├── promote-v1.yml # the only sanctioned way to move v1 (reviewer-gated) + ├── v1-drift-canary.yml # weekly: files an issue when v1 lags main on reusables + └── the rest # dogfood stubs, ccc-v10 canary, confluence sync ``` ## The four sync groups @@ -71,21 +73,22 @@ be overwritten. `.github/sync.yml` encodes this as four groups: | Group | Repos | What's different | |---|---|---| -| 1 | everything not listed below | full set: scripts, all four configs, all six stubs | -| 2 | nswds-ui, nswds-tokens | keep their own `release.yml` AND release config (both publish to npm with bespoke verification) | +| 1 | everything not listed below | full set: scripts, all four configs, all seven stubs | +| 2 | nswds-ui, nswds-tokens, nswds-eslint-config, nswds-prettier-config | keep their own `release.yml` AND release config (all publish to npm with bespoke verification); where the repo keeps its own `ci.yml`, the shared CI stub lands as `shared-ci.yml` | | 3 | nswds-app | keeps its own `release.config.mjs` (publishes `@nswds/app`); stock release stub is fine | -| 4 | ictds-portal-flows | ⚠️ its `release.yml` is a **Power Platform production deploy** that happens to share the filename — never overwrite it. The release stub maps to `semantic-release.yml` instead | +| 4 | ictds-portal-flows | its `release.yml` is a **Power Platform production deploy** that happens to share the filename — never overwrite it. The release stub maps to `semantic-release.yml` instead | -## The six shared CI checks +## The seven shared CI checks | Stub (in each repo) | What it does | |---|---| +| `ci.yml` | the merge gate: `install` (conflict-marker scan, `npm clean-install` lockfile check, build), `lint`, `test`, `format` (`prettier --check` when a Prettier config exists). Per-repo `CI_SKIP_*` variables opt individual jobs out. Superseded pushes cancel in-flight runs; npm tarballs are cached keyed on the lockfile | | `commitlint.yml` | lints PR commit messages against `commitlint.config.mjs` | | `validate-branch-name.yml` | enforces branch naming from `scripts/branch-name-config.sh` (read from the PR *base* so PRs can't alter their own policy) | | `commit-types-sync.yml` | fails if `commit-types.mjs` and `git-conventional-commits.yaml` disagree | | `ai-pr-title.yml` | generates/validates Conventional Commit PR titles via OpenAI | | `openai-pr-description.yml` | autofills empty PR descriptions | -| `release.yml` | semantic-release on push to main (handles npm OIDC publish, deploy-key push to protected branches, HUSKY=0) | +| `release.yml` | semantic-release on push to main (npm OIDC publish, deploy-key push to protected branches, HUSKY=0, failure-alert issue) | Note the check names: a reusable workflow reports as `commitlint / commitlint` (caller job / called job), not `commitlint`. Required-check rulesets must use diff --git a/docs/best-practices/linting-and-formatting.md b/docs/best-practices/linting-and-formatting.md index 31ada78..c883811 100644 --- a/docs/best-practices/linting-and-formatting.md +++ b/docs/best-practices/linting-and-formatting.md @@ -1,6 +1,6 @@ # Linting & Formatting Rules -What is linted, where the configs live, and the cross-repo constraints. +What is linted, where the configs live, and how the gates enforce it. ## The gates @@ -9,45 +9,56 @@ What is linted, where the configs live, and the cross-repo constraints. | commitlint | Every PR commit message | `commitlint / commitlint` (required check) | | shellcheck | Shared shell scripts | This repo's CI | | actionlint | Workflows + workflow stubs | This repo's CI | -| eslint | JS/TS source | Per-repo (`npm run lint`) | -| prettier | Formatting | Per-repo config; see constraint below | +| eslint | JS/TS source | `install / lint` (required check; runs `npm run lint --if-present`) | +| prettier | Formatting | `install / format` (required check; runs `prettier --check .` when the repo has a Prettier config) | +| Snyk | Dependencies + code | `security/snyk` and `code/snyk (DigitalNSW)` (required checks; posted by the Snyk console integration) | -## The 80-column rule for shared `.mjs` configs +`--if-present` and the format job's config probe mean a repo with no lint +script or no Prettier config passes those gates without running anything — +the gate is only as real as the repo's own wiring. Wire up every script the +repo can honestly run (see ONBOARDING.md step 2). -The synced configs (`commit-types.mjs`, `commitlint.config.mjs`, -`release.config.mjs`) are checked by **different prettier configs in -different repos** — nswds-ui runs printWidth 80 / no semicolons, others run -printWidth 100. The same bytes must satisfy both, so in these files: +## Prettier -- keep every line ≤ 80 columns, semicolon-free; -- if an expression is long, restructure it (block body, extracted variable) - rather than letting prettier wrap it — an 80-wrapped line gets *unwrapped* - by a width-100 config and fails the other repo's check. +Prettier owns formatting; linters own correctness. The fleet style is +defined once in the `@nswds/prettier-config` npm package (single quotes, no +semicolons, 2-space indentation, printWidth 100, trailing commas) and every +repo consumes it — via the `package.json` `"prettier"` key when nothing is +layered on top, or a `.prettierrc.mjs` that spreads the base and adds local +plugins (the Tailwind repos). Style changes are made in the package and +reach the fleet as a normal version bump; nothing is hand-copied. -## Prettier +The synced `.mjs` configs (`commit-types.mjs`, `commitlint.config.mjs`, +`release.config.mjs`) are formatted in fleet style so every consumer's +`format:check` passes on them. -Prettier owns formatting; linters own correctness. House style (per-repo -`.prettierrc`, JS/TS repos): +`.prettierignore` must exclude: -- single quotes, no semicolons, 2-space indentation, trailing commas (ES5); -- `.prettierignore` excludes generated output (`build/`, `node_modules/`); -- run via `npm run format`, or on save with editor integration. +- `CHANGELOG.md` — semantic-release rewrites it unformatted on every + release; leaving it checked re-reds the format gate at the next release. +- Generated and machine-written output — build artefacts, ORM meta + snapshots, generated modules, email template HTML. Formatting generated + files creates permanent churn against their generators, and email HTML is + whitespace-sensitive. -Repos differ on `printWidth` (80 vs 100) — which is exactly why the shared -`.mjs` configs follow the 80-column rule above. +Run `npm run format` locally (write mode); CI runs check mode only. ## ESLint -Flat config (`eslint.config.mjs`). Next.js repos extend -`next/core-web-vitals` + `next/typescript`, plus -`plugin:prettier/recommended` with: +Flat config (`eslint.config.mjs`), centralised in the +`@nswds/eslint-config` npm package with two entry points: -```js -'prettier/prettier': 'error' -``` +- `@nswds/eslint-config` — Next.js apps: `core-web-vitals` + + `typescript`, with the `fixupConfigRules` shim that keeps + `eslint-config-next` working on ESLint 10. +- `@nswds/eslint-config/base` — everything else (token pipelines, IaC, + node scripts, plain sites): `@eslint/js` + `typescript-eslint` + recommended. -so formatting violations surface as lint errors in `npm run lint` — one -command reports everything. +Both entry points enforce `'prettier/prettier': 'error'` and restrict +`no-console` to `warn`/`error`, so formatting violations surface as lint +errors and `npm run lint` reports everything in one command. Repos add only +thin, additive `globalIgnores` for repo-specific generated paths. ## Rules @@ -59,5 +70,8 @@ command reports everything. - Formatting changes ship as their own `style:` commit/PR — never mixed into a behaviour change, where they bury the real diff. (`style:` releases a patch, deliberately, so formatting-only fixes still ship.) -- New repos inherit the shared configs via the sync — don't fork them - locally; propose changes here so every repo moves together. +- New repos inherit the shared configs via the sync and the npm packages — + don't fork them locally; propose changes centrally so every repo moves + together. A bespoke config needs a header comment naming why it diverges + and what would let it converge (see the exceptions register in + MAINTENANCE.md). diff --git a/docs/config-single-source-of-truth.md b/docs/config-single-source-of-truth.md index de15980..8afaf9f 100644 --- a/docs/config-single-source-of-truth.md +++ b/docs/config-single-source-of-truth.md @@ -3,7 +3,7 @@ Six config files had drifted across the fleet with no canonical version: `.nvmrc`, `.npmrc`, `.gitignore`, `eslint.config.mjs`, `.prettierrc`, `.prettierignore`. This establishes one source of truth for each and a rollout -plan to converge the 22 repos onto it. +plan to converge the fleet (24 consumer repos) onto it. The files split into three distribution mechanisms, because the file-sync action (`BetaHuhn/repo-file-sync-action`) **overwrites the whole destination file** — @@ -34,7 +34,7 @@ express "shared base + local override"; an npm package can. The Prettier package has **two consumer shapes**, because the `package.json` key takes a bare package reference and cannot add options on top. Only base-only -repos can use it — in practice just nswds-tokens. Every Tailwind repo needs the +repos can use it — nswds-tokens and the two config packages. Every Tailwind repo needs the plugin block and an app-specific `tailwindStylesheet`, so it extends in a `.prettierrc.mjs` instead: