Skip to content

Lint and format-check the website and calculator in CI - #1366

Merged
MaxGhenis merged 9 commits into
mainfrom
ci/lint-website-calculator-app
Oct 20, 2026
Merged

MaxGhenis merged 9 commits into
mainfrom
ci/lint-website-calculator-app

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

CI's Lint and format job checked only app/. The two Next.js apps that are now deployed, website/ (policyengine.org) and calculator-app/ (app.policyengine.org), had no lint or format step. The website was linted only as a side effect of the Build job: next build runs ESLint over whichever of app, pages, components, lib and src exist (src for the website) and fails on errors, but never on warnings (maxWarnings -1 in next/dist/lib/eslint/runLintCheck.js), and with the link rule below switched off. The calculator had no ESLint config, so its build skipped linting. Neither app was format-checked. This PR runs ESLint and a Prettier check for both apps in pr.yaml and push.yaml, and fixes what they found on main.

ESLint

Website. bun run lint passed on main, but one of its rules was off. The config set settings.next.rootDir to "website/", which @next/eslint-plugin-next globs from the folder ESLint runs in. Run from website/, as bun run lint does, the glob matched nothing. @next/next/no-html-link-for-pages then printed "Pages directory cannot be found at ." and returned no checks (dist/rules/no-html-link-for-pages.js, dist/utils/get-root-dirs.js). rootDir is now the config's own folder, with (, ), {, }, |, + and [ in the path wrapped in brackets. The plugin treats the path as a glob, so otherwise a parent folder named like Projects (1), a{b,c} or p|q matches nothing and switches the rule off again (found in review). I checked this two ways: by globbing through the plugin's getRootDirs under folder names containing those characters and ], !, @, * and ?, and by linting from copies under par (1)/, c (d)|e+/ and a[b+c (1)|d/. A new website test, src/__tests__/config/eslint-config.test.ts, fails if the rule stops reporting a plain <a> link to a website page; it fails with the old "website/". With the rule back on, its one finding was PE84Banner.tsx, which nothing has imported since c09c088 took the banner off the homepage, so it is deleted.

Calculator. calculator-app/ had no ESLint setup. It now uses the website's Next.js config (next/core-web-vitals, next/typescript) plus two settings copied from app/eslint.config.js: @typescript-eslint/no-explicit-any off, and app's no-unused-vars options. The calculator's routes and pages wrap app's code and are written in app's style; those two settings cover 9 of the 21 problems a plain Next config reported. The other findings are fixed:

  • next.config.ts takes webpack from the options Next passes to the webpack() hook instead of calling require("webpack"). In Next 15.5 both resolve to the same object. The option is bundler, which next/dist/shared/lib/get-webpack-bundler.js takes from next/dist/compiled/webpack/webpack. The require hook in next/dist/server/config-utils.js sends require("webpack") to webpack-lib, which re-exports that same export. next build passes, and none of the 22 import.meta.env.* uses in app/src are left in .next/.
  • postcss.config.mjs names its default export, as the website's does.
  • The two error.tsx pages keep their plain <a href="/us/reports">, with a disable comment saying why: a full page load discards the client state that may have caused the error.

Both.

  • The website and calculator lint scripts, and app's eslint script, pass --max-warnings 0, so a warning fails CI. The repo guidance already said CI did this, but it didn't. All three have zero warnings today.
  • @eslint/eslintrc, which both Next configs import, is declared in each app's devDependencies instead of being reached through hoisting.
  • Both configs ignore public/. It links to app/public, and website/scripts/build.sh swaps in a real copy of those assets while it builds. A lint run during a local build failed on a bundled .js file in that copy.

Formatting: the website and calculator now get a Prettier check

I decided to add the check. Both apps follow Prettier's default style, but nothing enforced it, and they had drifted. On main, 74 of the website's 192 ts, tsx, mjs and css files differed from the defaults. 39 of those were in app/'s style (single quotes, width 100), probably copied over from app/, and 35 matched neither style. In the calculator, 4 of 47 files differed. app/ has had a check all along; the deployed apps should too.

  • Defaults, not app/'s config. Moving to app/'s config would have reformatted 150 website files and 46 calculator files instead of 74 and 4.
  • Pinned with a prettier.config.mjs in each app. Without a config file, Prettier keeps looking in parent folders and uses the first config it finds. I checked this: a .prettierrc two folders up changed the output. The configs also set Prettier's defaults for the five options EditorConfig can set (useTabs, tabWidth, printWidth, singleQuote, endOfLine). Prettier stops looking for .editorconfig files at a .git or .hg folder or at an .editorconfig marked root = true. This repo has none of those, and a worktree's .git is a file, not a folder. Without these settings, a home-folder .editorconfig changed indentation, quotes, width and line endings in a worktree (found in review, then reproduced). app/.prettierrc.mjs already set three of them and now sets useTabs and tabWidth too, because make format runs it.
  • Scripts. format and format:check cover **/*.{ts,tsx,mjs,css}. public/ is ignored for the same reason as in ESLint.
  • The reformat is its own commit (e8d04db): bun run format in both apps and nothing else. It is listed in .git-blame-ignore-revs; git config blame.ignoreRevsFile .git-blame-ignore-revs makes local git blame skip it. That needs a merge commit, not a squash, so the SHA survives.
  • The reformat changes no code. All 76 changed ts, tsx and mjs files parse (typescript-estree) to the same syntax tree before and after, with the same comment text. JSX text is compared after applying JSX's own whitespace rules, and {" "} is folded into the text around it. As a check on the checker, it reported a planted string change and a dropped {" "}. The two css files change only quote style and the wrapping of --font-sans; the built CSS still has --tw-font-sans:"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif.
  • Open PRs. Of the 49 open PRs, five touch reformatted files: Add website dark mode #1043 (draft, last updated 2026-05-15, 20 files), UK chatbot beta launch: blog post, homepage CTA, featured tools #1073 (draft, 2), and Add Paper type to research page with three SSRN working papers #1160, Update homepage typewriter prompts with PolicyEngine-computed shares #1022 and Add Tools page with showcase and homepage preview #857 (1 each). Running bun run format on those branches resolves the formatting side of any conflict.

push.yaml

push.yaml's lint, typecheck and test jobs checked only app/, while pr.yaml also typechecked both Next apps and ran the website's tests. Every job in push.yaml now has the same steps as in pr.yaml (compared job by job after parsing both files).

Local commands

make lint and make format now cover website/ and calculator-app/ as well as app/. The before-committing steps in docs/engineering/skills/repository-guidance.md say so.

Verification

  • On this branch, lint, format:check and typecheck pass in website/ and calculator-app/, and app/'s Prettier check passes. The website's tests pass (39 files, 337 tests). next build passes for both apps.
  • Each new check fails when it should:
    • A planted <a href="/us/research"> fails lint in both apps with no-html-link-for-pages. Before the rootDir fix, the website reported nothing for the same kind of link (PE84Banner).
    • A planted unused variable on its own fails lint in both apps. In the website it is a warning, which fails through --max-warnings 0; in the calculator it is an error.
    • format:check fails on main's files: 74 in the website, 4 in the calculator.
  • bun install --frozen-lockfile passes. The lockfile only gains the new devDependency specs; bun 1.3's configVersion line is stripped.

Invariants

  • Each new step exits non-zero when its app has any ESLint error, any ESLint warning, or any covered file Prettier would change. The probes above check each case.
  • The reformat commit leaves every syntax tree and every comment unchanged.
  • pr.yaml and push.yaml run the same steps in every job.

Review

GPT-6.1 Sol (Subfleet review, hard tier), round 1 at ccc108c: REQUEST_CHANGES with three findings, all fixed in 0183ac7:

  1. A parent .editorconfig could still change the formatting in a worktree. Fixed by setting the five options explicitly.
  2. Glob characters in an absolute rootDir could switch the link rule off. Fixed by bracketing (, ), { and }, with the new test.
  3. This description said the website was never linted in CI, but next build lints it for errors. Corrected above.

Round 2 at 0183ac7: APPROVE. Its nits:

  • | and + could still break the rootDir glob; they are bracketed too in 9fb4375.
  • Two wording fixes, made above.
  • Its third nit, that [ and ] break the glob, didn't hold when executed: Backup [old], x [1] and lone brackets all resolve.

Delta review of 9fb4375 (an Opus 5.5 agent): APPROVE. Across 6,838 generated folder names, the new escaping fixed 1,145 names and broke 57. Almost all of the 57 had an unmatched [ plus a +. Bracketing [ as well (53ce3c8) fixes those and keeps realistic names working.

Delta review of 53ce3c8 (same agent): APPROVE. Rerun on the same sweep, it fixed 698 names and broke 1 (!![d, a folder name starting with !!). All 54 hand-picked realistic names still resolve, among them Projects (1), Backup [old], [WIP] app, Dropbox (Personal) and C++ projects.

The reviewer also confirmed:

  • All 78 reformatted files reproduce byte for byte with the installed Prettier, and their syntax trees and comments match.
  • The hook's webpack and require("webpack") are the same object.
  • The probes fail as described, both workflows parse and match job by job, and the lockfile matches the package specs.

Not in this PR

  • CI never runs app/'s Stylelint. bun run lint in app/ runs it locally, but CI runs only bun run eslint. It passes on main today.
  • make typecheck and make test still cover only app/.
  • The calculator's error pages send every user to /us/reports, UK users included.
  • website/src/app/pe84/layout.tsx is a layout with no page; /pe84 redirects to the PE-84 zone before it could render.

Overlap with open PRs

#1363 and #1346 edit other jobs in pr.yaml and push.yaml; their hunks don't touch these. bun.lock may need a bun install after either one lands.

axiom: n/a: CI and formatting only

🤖 Generated with Claude Code

The Lint and format job ran ESLint only in app/. The two Next.js apps
that are now deployed, website/ and calculator-app/, were never linted
in CI.

- pr.yaml and push.yaml run `bun run lint` in website/ and
  calculator-app/ after app/'s checks.
- website: `settings.next.rootDir` was "website/", which matched no
  folder when ESLint ran from website/. @next/next/no-html-link-for-pages
  then printed "Pages directory cannot be found" and switched itself
  off. rootDir is now the config's own folder. With the rule back on,
  the only finding was PE84Banner, which nothing has imported since
  the banner left the homepage (c09c088), so it is removed.
- calculator-app had no ESLint setup. It gets the website's Next.js
  config, plus app/'s settings for `any` and unused variables, since
  its routes are written in app/'s style. Fixes for the findings:
  next.config.ts takes webpack from the options Next passes instead
  of require("webpack"); postcss.config.mjs names its export; the two
  error pages keep their plain <a> (a full page load) with a disable
  comment saying why.
- website and calculator lint, and app's eslint, run with
  --max-warnings 0, so warnings fail CI as CLAUDE.md already says.
- @eslint/eslintrc, which both eslint configs import, is declared in
  each app instead of relying on hoisting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
policyengine-calculator-next Ready Ready Preview Oct 20, 2026 5:16pm UTC
policyengine-website Ready Ready Preview Oct 20, 2026 5:16pm UTC

Request Review

Neither app had a Prettier config, so Prettier used its defaults unless
it found a config in a parent folder (a developer's home folder, say).
Each app now has an empty prettier.config.mjs, which keeps the defaults
and stops that search, plus `format` and `format:check` scripts over
ts, tsx, mjs and css files. public/ is ignored: in both apps it links
to app/public, which holds built assets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 4 commits October 20, 2026 11:59
`bun run format` in website/ and calculator-app/, nothing else. 74
website files and 4 calculator files had drifted from Prettier's
default style; most were copied from app/ with app/'s single quotes.

Every changed ts, tsx and mjs file parses to the same syntax tree
before and after, and keeps the same comments. The css changes are
quote style and one wrapped custom property.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- pr.yaml and push.yaml run `bun run format:check` in website/ and
  calculator-app/, next to each app's ESLint step.
- `make lint` and `make format` cover website/ and calculator-app/
  as well as app/, and the before-committing guidance says so.
- Both ESLint configs ignore public/. It links to app/public, and
  website/scripts/build.sh swaps in a real copy of those assets while
  it builds; ESLint run during a build then linted a bundled .js file
  in that copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
push.yaml's typecheck job checked only app/ and its test job ran only
app/'s tests, while pr.yaml also typechecks website/ and calculator-app/
and runs website/'s tests. Every job in push.yaml now has the same
steps as in pr.yaml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review:

- @next/eslint-plugin-next globs settings.next.rootDir, so an absolute
  path under a folder named like "Projects (1)" or "a{b,c}" matched
  nothing and @next/next/no-html-link-for-pages switched itself off
  again, without failing lint. Both configs now bracket (, ), { and }
  in the path. A new website test fails if the rule stops reporting a
  plain <a> link to a website page; it fails with the old "website/".
- Prettier merges .editorconfig settings under its config, and it
  stops looking for them only at a .git folder. A worktree's .git is a
  file, so a home-folder .editorconfig could change indentation,
  quotes, width or line endings. The website and calculator configs
  now set Prettier's defaults for the five options EditorConfig can
  set, and app/'s config sets the two it left open (useTabs, tabWidth).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review round 2. A parent folder named like "p|q" or "x (y)+"
still turned the link rule off: | becomes regex alternation and +
after a bracketed ) becomes a quantifier. Checked by globbing through
the plugin's getRootDirs under folder names with (, ), {, }, [, ], |,
+, !, @, * and ?, and by linting from a copy under "c (d)|e+/".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the delta review: an unmatched [ anywhere in the path, together
with a + that is now bracketed, made the glob match nothing ("a[b+c").
Bracketing [ as well fixed those and names such as "[a-z]" and
"[a+b]", and kept "Backup [old]", "x [1]", "a]b[c" and "a[]b" working.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Merge audit for head 53ce3c8.

  • Checks: all pass on this head (PR checks run 39464444040; Chromatic, Vercel and visual regression too). The Lint and format job ran the six new and renamed steps: Prettier and ESLint for app, website and calculator.
  • Reviews:
    • GPT-6.1 Sol, round 1 at ccc108c: REQUEST_CHANGES, three findings, fixed in 0183ac7.
    • GPT-6.1 Sol, round 2 at 0183ac7: APPROVE.
    • Opus 5.5 delta reviews of 9fb4375 and 53ce3c8: APPROVE.
    • Details are in the description's Review section.
  • Mergeable, not a draft, and no changes-requested review. Main moved by Point the Autumn Budget webinar banner at Zoom registration #1369 (one line in AutumnBudgetEventBanner.tsx). The merged tree has no conflicts, and that file passes prettier --check and eslint --max-warnings 0.
  • Why --admin: the protect-master ruleset requires one approving review, and the PR author can't approve their own PR. Nothing else blocks it.
  • Why a merge commit: the reformat commit e8d04db is listed by SHA in .git-blame-ignore-revs, so it has to keep that SHA.

@MaxGhenis
MaxGhenis merged commit 4d61480 into main Oct 20, 2026
10 checks passed
@MaxGhenis
MaxGhenis deleted the ci/lint-website-calculator-app branch October 20, 2026 18:01

This branch was successfully deployed

2 active deployments
Preview – policyengine-calculator-next — 53ce3c85 Deployed Oct 20, 2026 by vercel[bot]
Preview – policyengine-website — 53ce3c85 Deployed Oct 20, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant