chore: speed up validation and cut agent token usage - #435
Merged
Conversation
Turborepo rebuilt the whole monorepo before every lint run, and every task replayed its full logs on a cache hit. Both are expensive for agents working in this repository, and neither bought anything. - turbo.json: drop the build dependency from `lint`. The shared ESLint config uses typescript-eslint's `recommended` preset with no `parserOptions.project` or `projectService`, so no rule ever constructs a TypeScript program or reads a `.d.ts` — it cannot observe a stale or missing build. Linting the 29 non-playground packages from cold goes from 59.5s to 9.6s. - turbo.json: keep that dependency for `@rozenite/playground#lint` only, which runs `expo lint`; eslint-config-expo enables eslint-plugin-import, whose `import/no-unresolved` rule does resolve `@rozenite/*` imports to `dist`. - apps/playground: pass `--no-cache` to `expo lint`. Its ESLint cache lives under the gitignored `.expo/`, invisible to Turborepo, so a lint run made before the workspace packages were built kept replaying stale `import/no-unresolved` failures even after `dist/` existed. Turborepo already caches the task and the flag costs about 0.3s. - turbo.json: add `globalDependencies` for `tsconfig.base.json` and `eslint.config.mjs`. 14 package tsconfigs extend the former and 11 ESLint configs import the latter, but neither was a cache input, so editing either left every cached task stale. - turbo.json: exclude README.md and CHANGELOG.md from task inputs. Every changeset release rewrites all 31 changelogs, invalidating the whole graph. - turbo.json: quiet successful and replayed task logs via `outputLogs`. Failures still print in full. - package.json: add `:affected` script variants, plus `test:all`. - pnpm-workspace.yaml: set `loglevel: warn`. A cold install drops from 82 lines to 0. Warnings and errors still print, and the setting does not affect `pnpm run` output, so Turborepo task logs are untouched. - docs: add docs/agents/validation.md, and point AGENTS.md, CONTRIBUTING.md and the pull request guide at the `:affected` scripts.
This was referenced Aug 20, 2026
V3RON
added a commit
that referenced
this pull request
Aug 20, 2026
## Description `pnpm format:all` fails on `main` today. That matters more since #435, because CONTRIBUTING.md and the pull request guide now point contributors at `pnpm checks:affected`, and that script ends in `format:all`. - Reformats three files that predate the move to oxfmt. - Adds a `Check formatting` step to CI so it cannot drift again. - Ignores Claude Code's local state, which was breaking `format:all` independently. ## Related Issue None — repository tooling, follow-up to #435. ## Context **Why the three files were unformatted.** The diffs *unwrap* lines rather than wrapping them — a six-line union type collapses to one line, a markdown table's column padding widens. That is the signature of files formatted at Prettier's narrower print width and never reformatted when the repo moved to oxfmt. They are stale leftovers, not new drift. The only token-level changes are trailing commas appearing/disappearing as arguments collapse onto one line, and one leading union `|` — all semantically inert. Verified by checksumming both revisions with whitespace, commas and that pipe stripped, and by a full typecheck/lint/test pass on both affected packages. No behavioral change, so no version plan. **Why nothing caught it.** No workflow has ever run `pnpm format:all`; CI runs `turbo run typecheck build lint test --affected` only. The new step runs before the turbo step so it fails fast — oxfmt checks all 1246 files in under 400ms. One gap this does not close: the workflow has `paths-ignore: website/**`, so a website-only PR runs no CI at all and could still land unformatted `.mdx`. Whether website PRs should gate on CI is a separate decision. **Why only two `.claude/` paths are ignored.** `.claude/settings.local.json` and `.claude/worktrees/` were excluded only through per-machine files — the global git excludes file that Claude Code writes to, and `.git/info/exclude`. Git therefore ignored them but oxfmt did not, since oxfmt reads `.gitignore`/`.prettierignore` from the repo only. The result was that `pnpm format:all` failed for anyone using Claude Code, regardless of the three files above. Ignoring `.claude/` wholesale would be wrong: per the Claude Code settings documentation, `.claude/settings.json`, `agents/`, `skills/` and `commands/` are all intended to be committed and shared with the team. Only the two machine-local paths are ignored here. ## Testing - `pnpm format:all` — passes (1246 files, 383ms) - `npx turbo run typecheck lint test` — **118 tasks, all successful** - `npx turbo run typecheck lint test --filter=@rozenite/network-activity-plugin --filter=@rozenite/web` — 38 tasks, all successful - Confirmed the reformatting is semantically inert by checksum, as described above - CI workflow YAML re-parsed after editing; step order verified
V3RON
added a commit
that referenced
this pull request
Aug 20, 2026
## Description Caches Turborepo artifacts in CI, restoring on every run but writing only from pushes to `main`. The Validate job spends **319s of its 6m19s** inside `turbo run`. Nothing persists between runs today: `.turbo` is gitignored and only the pnpm store is cached, so every run rebuilds affected packages and their entire dependency chain from scratch. > Stacked on #436 — the base will retarget to `main` once that merges. Only the two workflow files are this PR's own change. ## Related Issue None — repository tooling, follow-up to #435. ## Context **Why saves are restricted to `main`.** GitHub scopes a pull request's cache to its merge ref, and that cache "can only be restored by re-runs of the pull request" — so a fork PR cannot reach `main`'s scope or another PR's regardless. Restricting saves closes the remaining case: a run writing a cache that a later re-run *of the same PR* would restore. That distinction matters more for Turborepo than for the pnpm store. A poisoned store still leaves every task to actually run. A poisoned Turborepo artifact makes tasks report a cache hit **without executing** and replays their stored logs — and #435's `outputLogs` now suppresses cache-hit logs, so the replay is quieter than it used to be. A fork PR could use that to forge a green check on a diff that would otherwise fail. Nothing detects this for us: pnpm documents `verifyStoreIntegrity` as catching accidental corruption, explicitly not deliberate tampering, and Turborepo's signature verification applies only to *remote* caches, not the filesystem cache `actions/cache` persists. Scope is the control that matters. Saving only from `main` means every cached artifact was produced by already-merged, reviewed code. PRs still restore `main`'s cache, which covers every dependency build they did not change; they only re-execute tasks for the packages they actually touch, which `--affected` already bounds. **Why the key rotates weekly with no fallback.** Turborepo never prunes `.turbo/cache`. The accumulated local cache in this repository is already **1.8GB across 10050 entries**. Because each `main` run restores before it saves, the saved artifact is the union of everything cached so far — so an unrotated key grows without limit until it hits the 10GB per-repository cache limit and evicts everything else, including the pnpm store cache. There is deliberately no `restore-keys` fallback to the previous period: falling back would carry the accumulation forward and defeat the reset. The cost is one cold run at the start of each week, which is no worse than today's behaviour, where every run is cold. **Why `release.yml` stays uncached.** It holds `contents: write` and `id-token: write` and publishes to npm. A restored artifact would be published with provenance attesting to something that was never built from this source. The job now carries a comment saying so, since this is exactly the kind of thing a later "optimization" would undo. ## Testing - Both workflow files re-parsed as YAML after editing; step order and `if:` conditions verified - `actions/cache` pinned to `55cc8345863c7cc4c66a329aec7e433d2d1c52a9`, confirmed via the API to be v6.1.0 in the first-party `actions/` org, matching this repository's pin-everything convention - Confirmed the `restore` and `save` sub-actions exist at that exact SHA - `npx turbo run typecheck lint test` — **118 tasks, all successful** - `pnpm format:all` — passes Cache behaviour itself can only be confirmed on a real run. Two things to watch: 1. **The path.** Turborepo resolves `.turbo/cache` at the repository root in a plain clone; I could only verify the resolution logic locally inside a git worktree, where it deliberately redirects to the main checkout. If the path were wrong the symptom is a cache that never hits, not a wrong result. 2. **Effect on runtime.** The baseline to beat is the 6m19s Validate run on #435. The first push to `main` after this merges is still cold — it populates the cache. Subsequent runs are the ones to measure.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Makes validation in this repository faster and much less verbose, primarily for agents working in it.
pnpm installis silent on success — a cold install drops from 82 lines to 0.:affectedscript variants so a two-file change no longer validates 38 packages.Related Issue
None — this is repository tooling rather than a user-facing change, and was not tracked by an existing issue.
Context
Why
lintno longer depends onbuild. The sharedeslint.config.mjsuses typescript-eslint'srecommendedpreset with noparserOptions.projectand noprojectService. Without those the parser never constructs a TypeScript program, so no rule can read a.d.ts. Lint therefore cannot observe a stale or missing build — verified by linting a file that imports a non-existent export from an unbuilt package, which exits 0. Building first bought nothing and cost a full monorepo build on every lint.Why
@rozenite/playgroundis the exception. It runsexpo lint, andeslint-config-expoenableseslint-plugin-import, whoseimport/no-unresolvedrule does resolve@rozenite/*imports todist. Unbuilt, it reports 26 false failures, so the build dependency is kept for that one task.Why
expo lintgets--no-cache. Its ESLint cache lives under the gitignored.expo/, which Turborepo can neither see nor invalidate. A lint run made before the workspace packages were built keeps replaying its staleimport/no-unresolvedfailures even afterdist/exists — the exact trap someone hits in a fresh worktree: lint fails, build, lint again, still fails. Turborepo already caches the task at a higher level, and the flag measures at ~0.3s.Cache-correctness fixes. 14 package
tsconfig.jsonfiles extendtsconfig.base.jsonand 11 ESLint configs import the rooteslint.config.mjs, but neither root file was a cache input — editing either left every cached task stale. They are nowglobalDependencies. Separately, every changeset release rewrites all 31CHANGELOG.mdfiles, which invalidated the entire task graph;README.mdandCHANGELOG.mdare now excluded from task inputs (verified that no build tooling reads them).Scope of the speedup. The lint improvement applies to standalone
pnpm lint:affected/lint:all. Commands that also runtypecheckare build-bound regardless, since typecheck genuinely needs.d.ts— the task graph fortypecheck lintis unchanged at 114 tasks, and CI'stypecheck build lint testis unchanged at 152. CI timings are therefore not expected to move.preferOfflinewas considered and deliberately rejected: it saves nothing on a lockfile install (20.9s vs 21.7s cold) and, once pnpm's metadata cache goes stale, resolves ranges against that cache instead of revalidating — reproduced withsemver@^7.0.0resolving to 7.6.0 rather than 7.8.5. The reasoning is recorded indocs/agents/validation.mdso it is not re-attempted.Testing
npx turbo run typecheck lint test— 118 tasks, all successfulnpx turbo run lint --forcefrom a fully cold state (alldist/, the Turborepo cache and the expo cache wiped) — 58/58 successful, including@rozenite/playgroundpnpm install --frozen-lockfile— silent (0 lines);loglevel: warnverified not to affectpnpm runoutput, with byte-identical stdout for a passing vitest run and a failing lint, including the error and exit statusloglevel: warnpnpm format:allclean for every file touched herepnpm release:plan— no version plan required (root config, docs, andapps/playgroundare excluded from versioning)Known pre-existing issues, not addressed here
pnpm format:allfails on three files that predate this branch (websocket-interceptor.ts,fuseboxReactDevToolsDispatcher.ts,compatibility.mdx). They look like leftovers from a Prettier-era print width.pnpm format:all, which is why that drift went unnoticed..turbo, so every run rebuilds affected packages and their dependency chains from scratch.