From 00557c733a0578d1d685ae41ac59a9512c536654 Mon Sep 17 00:00:00 2001 From: laurenhitchon Date: Wed, 29 Jul 2026 19:49:10 +1000 Subject: [PATCH] docs(maintenance): correct stale operating claims and add the exceptions register MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - retire the ≤80-column .mjs mandate (fleet is uniformly printWidth 100) - reconcile the eslint-major block's removal condition (default.json + renovate.md): shim shipped, 14 repos on eslint ^10; nswds-ui is the remaining blocker, re-check by 2026-10 - fleet counts 17/21 → 24 across README/ONBOARDING/MAINTENANCE/renovate.md/sync.yml - README stub example matches the explicit-secrets stubs; v1 motion now via the gated workflow - exceptions register (sanctioned divergences incl. the Power Platform prod-gate compensating control) - document the repo-local automation (promote-v1, drift canary, ccc canary, confluence sync) - CODEOWNERS (advisory while solo-maintainer) --- .github/CODEOWNERS | 13 +++++++++ .github/sync.yml | 3 +- MAINTENANCE.md | 52 ++++++++++++++++++++++++++------- ONBOARDING.md | 3 +- README.md | 8 +++-- default.json | 2 +- docs/best-practices/renovate.md | 4 +-- 7 files changed, 66 insertions(+), 19 deletions(-) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..35eb8fa --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,13 @@ +# Advisory ownership routing. No required-review rule is attached while the +# team is a single active maintainer (GitHub forbids self-approval, so a +# >=1-approval rule would deadlock every PR) — attach one in the "Protect +# main" ruleset when a second active maintainer exists. Until then this file +# documents the blast-radius surfaces: push access to main here equals +# org-wide write (sync App key), fleet CI (v1 tag), and fleet Renovate +# policy (default.json, read from main at runtime). +* @laurenhitchon +/default.json @laurenhitchon +/.github/sync.yml @laurenhitchon +/.github/workflows/ @laurenhitchon +/workflow-stubs/ @laurenhitchon +/scripts/ @laurenhitchon diff --git a/.github/sync.yml b/.github/sync.yml index 9603d2e..6b98134 100644 --- a/.github/sync.yml +++ b/.github/sync.yml @@ -1,7 +1,8 @@ # File-sync map for BetaHuhn/repo-file-sync-action (driven by workflows/sync.yml). # Changes merged to main open a "chore(ci): …" PR in every repo listed below. # -# ROLLOUT COMPLETE 2026-07-15: all 17 repos active (pilots + batches 2–4). +# ROLLOUT COMPLETE 2026-07-15: all consumer repos active (24 as of 2026-07-22, +# when dtl-sandbox joined group 1). # Onboarding a new repo = add it to the right group (group 1 unless it has a # bespoke release pipeline) — see SETUP.md. # diff --git a/MAINTENANCE.md b/MAINTENANCE.md index 2c99689..3906e35 100644 --- a/MAINTENANCE.md +++ b/MAINTENANCE.md @@ -14,18 +14,17 @@ they broke. 1. Edit here, on a branch, PR into `main`. CI shellchecks the scripts and actionlints the workflows; the commit-types-sync check keeps the YAML in lockstep with `commit-types.mjs`. -2. On merge, the sync opens a `chore(ci): …` PR in all 17 repos. They merge +2. On merge, the sync opens a `chore(ci): …` PR in all 24 consumer repos. They merge on their own schedule; until they do they just run the previous version. 3. That's it. Never edit these files in a consumer repo — the next sync overwrites it silently. -Two formatting constraints on the `.mjs` configs, learned the hard way: -nswds-ui runs `prettier --check` in CI with printWidth 80 / no semicolons, -while other repos use printWidth 100. Keep every line in these files ≤80 -columns and semicolon-free and the same bytes satisfy both configs. If you -add a long expression, restructure it (block body, extracted variable) -rather than letting prettier wrap it — an 80-wrapped line gets *unwrapped* -by a width-100 config and then fails the other check. +One formatting constraint on the `.mjs` configs: the whole fleet now formats +with `@nswds/prettier-config` (printWidth 100, no semicolons) — including +nswds-ui, whose workspace prettier config extends it. Keep these files in +that style and every consumer's `format:check` stays green. (The old +"≤80 columns for nswds-ui" rule is gone: nswds-ui moved to the shared +config on 2026-07-28.) **Changing CI logic** (`reusable-*.yml`): merge to `main` as usual — nothing reaches consumers yet, because stubs pin `@v1`. Ship it with the **Promote @@ -39,8 +38,11 @@ re-run the workflow with the previous SHA from the last promotion's summary. Don't wait for a release commit to promote: Renovate's `chore(deps)` bumps to the reusables never cut a release, so any green commit on `main` -qualifies. The **v1 drift canary** (weekly) opens a tracking issue when -unpromoted reusable-workflow changes sit on `main` for over a week. +qualifies. Release commits themselves are `[skip ci]` and carry **no check +runs**, so the workflow refuses them — promote the merge commit beneath (a +release commit only adds CHANGELOG/version on top of it). The **v1 drift +canary** (weekly) opens a tracking issue when unpromoted reusable-workflow +changes sit on `main` for over a week. Emergency fallback if the promotion workflow itself is broken: temporarily disable the tag ruleset's enforcement, push the tag, re-enable — the same @@ -161,6 +163,19 @@ are repository secrets `CONFLUENCE_USER` / `CONFLUENCE_TOKEN` (Atlassian API token); page edits are attributed to that account, so move to a service account if the token owner ever leaves. +**Repo-local automation** (none of these sync to consumers): + +- `promote-v1.yml` — the only sanctioned way to move `v1` (see "Changing CI + logic" above): environment-gated, deploy-key push, records the previous + target. +- `v1-drift-canary.yml` — weekly; opens a `v1-drift` issue when unpromoted + `reusable-*.yml` changes sit on `main` for over a week. +- `ccc-v10-canary.yml` — weekly; probes whether the upstream + release-notes-generator fix has landed and opens a `ccc-v10-canary` issue + the day the Renovate ccc block can be lifted. +- `confluence-sync.yml` — mirrors `docs/best-practices/` to Confluence + (title-matched pages; retitling a guide orphans its page). + ## Troubleshooting Every entry below is something that actually happened (2026-07-15 onward). @@ -177,7 +192,7 @@ Every entry below is something that actually happened (2026-07-15 onward). | Renovate "lock file maintenance" PR red on `install / install` (and commitlint) with `npm ci … not in sync` | from-scratch lockfile regeneration hits an npm peer-nesting bug: `@conventional-changelog/git-client@3` peers need `conventional-commits-filter@^6` while semantic-release's stack needs `^5`; regen hoists 5.0.0 and nests nothing (incremental Renovate updates resolve the same tree correctly) | close the PR (nswds-email#454 has the full write-up); retry from the Dependency Dashboard after the commitlint/semantic-release stacks re-align | | Snyk license/security red on a lockfile change | Snyk's baseline of main was unparseable, so every pre-existing issue reads as "introduced" | merge the lockfile fix; Snyk re-baselines. MPL-2.0 flags on lightningcss binaries come via @nswds/app in every repo — org license-policy call, not a repo bug | | `check-npm-artifacts` red (nswds-app) | committed `dist/` built before semantic-release bumped the version it inlines | `npm run build:npm` on the branch, commit dist | -| prettier --check red on synced configs (nswds-ui) | config formatting not stable across consumer prettier configs | see the ≤80-column rule under day-to-day changes | +| prettier --check red on synced configs | a synced `.mjs` was committed in non-fleet style | format it with `@nswds/prettier-config` (printWidth 100, no semicolons) before merging centrally | | Push to this repo rejected mid-work | semantic-release pushed a `chore(release): x.y.z [skip ci]` commit after your last fetch | `git pull --rebase`, push again — routine | | Confluence pages: the "Synced from GitHub" banner renders as a raw code block with a stray `-->` | mark's metadata parser consumes the first non-header line after the `` block; the multi-line `ac:box` Include sat directly against the headers, so its opening line was eaten and the rest rendered as indented code | keep the blank line between the header comments and the Include in `confluence-sync.sh` (fixed 2026-07-19; verified against mark 16.5.1 with `--compile-only`) | | Release run: `GH013` on `git push … https://github.com/.git` even with `RELEASE_DEPLOY_KEY` set | `package.json` `repository.url` was an `https://` URL — semantic-release prefers it over the SSH origin the deploy-key checkout configures, so the push skips the bypass actor | use the `git+ssh://git@github.com/…` form in `repository.url` (fixed here 2026-07-17; consumer sweep same day found nswds-tokens as the only other exposure — fixed via tokens #127; the rest have no `repository` field and fall back to the SSH origin) | @@ -193,6 +208,21 @@ Every entry below is something that actually happened (2026-07-15 onward). (`branch-name-config.sh`) are fleet-wide decisions. Changing them changes policy everywhere; announce before merging. +## Exceptions register + +Sanctioned divergences from the fleet baseline. Anything diverging and NOT +listed here should be treated as drift and converged. + +| Repo | Divergence | Why | Converge when | +|---|---|---|---| +| nswds-ui | Workspace eslint-config package (ESLint ^9, only-warn + per-app `--max-warnings 0`) instead of `@nswds/eslint-config` | Turborepo needs per-package presets; its `eslint-plugin-react` import lacks the fixup shim, so an eslint major would crash lint | Its workspace base wraps or adopts `@nswds/eslint-config/base`; pair with lifting the Renovate eslint-major block | +| nswds-tokens | Bespoke `eslint.config.js` (documented in its file header) | Token pipeline, predates the `./base` entry point | Next config change — adopt `@nswds/eslint-config/base` | +| nswds-ui | `engines` `^22.14.0 \|\| >=24.10.0`, `.npmrc` `provenance=false` | Documented in `.npmrc-nswds-ui` (private-repo npm E422) | Repo goes public / engines revalidated | +| ictds-portal-flows | `release.yml` is a Power Platform PROD deploy; the release stub maps to `semantic-release.yml` (sync group 4) | Filename collision with a production pipeline | n/a — permanent | +| ictds-portal-flows | PROD deploy approval is the `RELEASE_APPROVERS` allowlist in `release.yml`, not GitHub environment required reviewers | Required reviewers on private repos is Enterprise-only (org is on Team) | Enterprise upgrade or repo visibility change | +| dtl-sandbox | Deploys are manual `pulumi up` from operator machines; CI gates are typecheck + lint only | Sandbox stack; `pulumi preview` on PRs via Azure OIDC is planned, pending federated-credential setup | OIDC federation lands | +| digitalnsw, images, nswds-email-issues | No ESLint over mirror/static content (digitalnsw lints `api/` + `scripts/` only) | Scraped mirror / static assets / issue tracker — nothing meaningful to lint | n/a | + ## History / decision notes - Consolidated 2026-07-15 from copy-pasted files that had drifted into 3–4 diff --git a/ONBOARDING.md b/ONBOARDING.md index 497df74..7ffbb22 100644 --- a/ONBOARDING.md +++ b/ONBOARDING.md @@ -119,7 +119,8 @@ What to expect: ## Coverage -All 21 digitalnsw repos are on the sync as of 2026-07-15 (data, +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), diff --git a/README.md b/README.md index b96cda9..6243646 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,13 @@ that call them, pinned to the floating `v1` tag: jobs: commitlint: uses: digitalnsw/nswds-devops/.github/workflows/reusable-commitlint.yml@v1 - secrets: inherit ``` -Moving the `v1` tag changes CI for all 17 repos at once, instantly, with no -PRs. That's the point — and also why the tag only moves deliberately, after +(Stubs that need a secret map it explicitly — `secrets: inherit` is not used.) + +Moving the `v1` tag changes CI for all 24 consumer repos at once, instantly, +with no PRs. That's the point — and also why the tag is ruleset-protected and +only moves through the **Promote v1** workflow, behind a reviewer gate, after CI here is green. See MAINTENANCE.md for the exact motion. I deliberately chose this split over publishing an npm package: the sync PRs diff --git a/default.json b/default.json index 4317f03..f5b801b 100644 --- a/default.json +++ b/default.json @@ -41,7 +41,7 @@ "enabled": false }, { - "description": "eslint + @eslint/js majors are blocked, and the two MUST move in lockstep: @eslint/js@10 declares peerDependencies { eslint: ^10.0.0 } (v9 declared none), so bumping @eslint/js alone leaves eslint at ^9 and yields an unsatisfiable graph — Renovate cannot regenerate the lockfile (ERESOLVE) and every npm ci fails (EUSAGE). This matcher previously listed only \"eslint\", which let the eslint-monorepo group split the pair and broke nswds-tokens#132 (superseded by the combined bump in nswds-tokens#140). The block itself stays because ESLint 10 removed context.getFilename(), which eslint-plugin-react 7.37.5 (pulled in transitively by eslint-config-next, incl. 16.2.11) still calls in its React-version detection path — every eslint invocation then crashes with a TypeError before linting a single file (reproduced on nswds-app#418 and locally against eslint-config-next@16.2.11 + eslint@10; upstream jsx-eslint/eslint-plugin-react#3977, vercel/next.js#89764). PR CI does not run lint, so the breakage would merge green and only surface in local dev. The crash is confined to config-next consumers: repos with no eslint-plugin-react in the tree (e.g. nswds-tokens) are unaffected, and the Next.js repos can unblock now by wrapping the config-next presets in @eslint/compat's fixupConfigRules — the pattern already shipping in nswds-email (eslint@10, eslint-config-next@16.2.11). Remove this rule once the Next.js repos adopt that shim (or eslint-config-next ships an ESLint-10-compatible eslint-plugin-react — watch vercel/next.js#89764), and re-check the other capped peers (eslint-plugin-import, eslint-plugin-jsx-a11y) at the same time.", + "description": "eslint + @eslint/js majors are blocked, and the two MUST move in lockstep: @eslint/js@10 declares peerDependencies { eslint: ^10.0.0 } (v9 declared none), so bumping @eslint/js alone leaves eslint at ^9 and yields an unsatisfiable graph — Renovate cannot regenerate the lockfile (ERESOLVE) and every npm ci fails (EUSAGE). This matcher previously listed only \"eslint\", which let the eslint-monorepo group split the pair and broke nswds-tokens#132 (superseded by the combined bump in nswds-tokens#140). The block itself stays because ESLint 10 removed context.getFilename(), which eslint-plugin-react 7.37.5 (pulled in transitively by eslint-config-next, incl. 16.2.11) still calls in its React-version detection path — every eslint invocation then crashes with a TypeError before linting a single file (reproduced on nswds-app#418 and locally against eslint-config-next@16.2.11 + eslint@10; upstream jsx-eslint/eslint-plugin-react#3977, vercel/next.js#89764). STATUS 2026-07-29: the fixupConfigRules shim ships inside @nswds/eslint-config, the 14 Next.js repos run eslint ^10 through it, and the shared CI lint gate now runs on every PR — the original unblock condition is met for config-next consumers. The block stays for one remaining reason: nswds-ui's workspace eslint-config imports eslint-plugin-react directly with no shim, so an eslint major there would crash its required lint check. Remove this rule once nswds-ui wraps its presets or adopts @nswds/eslint-config/base, and re-check the capped peers (eslint-plugin-import, eslint-plugin-jsx-a11y) at the same time — re-check by 2026-10.", "matchPackageNames": ["eslint", "@eslint/js"], "matchUpdateTypes": ["major"], "enabled": false diff --git a/docs/best-practices/renovate.md b/docs/best-practices/renovate.md index cce8b66..4ac6708 100644 --- a/docs/best-practices/renovate.md +++ b/docs/best-practices/renovate.md @@ -46,7 +46,7 @@ the branch-naming policy in `scripts/branch-name-config.sh`. job**, not Renovate's — `vulnerabilityAlerts` is disabled in the preset so the two bots never open duplicate PRs for the same CVE.) - **Central policy, fleet-wide effect.** One preset file in this repo - governs all 17+ repos. A policy change lands everywhere on Renovate's + governs all 24 consumer repos. A policy change lands everywhere on Renovate's next run with no per-repo work at all. ## Where the config lives (and how it propagates) @@ -146,7 +146,7 @@ is the source of truth; this table is the summary: | All updates to `overrides`-pinned packages | Two ways to the same broken lockfile: in-range bumps go in as a direct-dep install that conflicts with the override → `EOVERRIDE` → stale lockfile (nswds-email#459); range bumps half-apply it — outgoing entry removed, resolved one never added → `npm ci` fails `EUSAGE Missing: …`, with no artifact-update warning on the PR to give it away (nswds-email#485) | Renovate's npm manager writes correct lockfiles for `overrides`. Until then monthly lock file maintenance keeps the resolved versions fresh, and Snyk drives the range bumps by hand | | `conventional-changelog-conventionalcommits` v10 | incompatible with release-notes-generator 14: releases succeed but changelogs silently come out empty (nswds-email#437; upstream #992) | a v10.x compatible with release-notes-generator 14 ships | | `typescript` majors (6/7) | TS7 is the native compiler with no JS API: `next build` fails, typescript-eslint crashes, import-sorting silently no-ops (nswds-email#444) | Next.js + typescript-eslint declare TS 6/7 support | -| `eslint` majors (10) | ESLint 10 removed `context.getFilename()`, still called by eslint-plugin-react — every lint invocation crashes, and PR CI wouldn't catch it (nswds-app#418; vercel/next.js#89764) | eslint-config-next ships an ESLint-10-compatible plugin set | +| `eslint` majors (10) | ESLint 10 removed `context.getFilename()`, still called by eslint-plugin-react — every lint invocation crashes, and PR CI wouldn't catch it (nswds-app#418; vercel/next.js#89764). The `fixupConfigRules` shim now ships inside `@nswds/eslint-config`, and the 14 Next.js repos are on eslint ^10 through it — the block stays for **nswds-ui**, whose workspace config imports eslint-plugin-react with no shim | nswds-ui's workspace config wraps or adopts `@nswds/eslint-config/base` (re-check 2026-10) | **Adding a block** (the pattern): when an update breaks the fleet, add a `packageRules` entry to `default.json` with `matchPackageNames` /