Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion .github/sync.yml
Original file line number Diff line number Diff line change
@@ -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.
#
Expand Down
52 changes: 41 additions & 11 deletions MAINTENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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).
Expand All @@ -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 `<!-- Key: value -->` 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/<repo>.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) |
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion ONBOARDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion default.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/best-practices/renovate.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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` /
Expand Down