* fix(ci): harden workflows against cache poisoning and document unsigned tags
- Restrict TURBO_TOKEN/TURBO_TEAM to main-branch pushes only across
ci.yml, code-quality.yml, and react-doctor.yml. First-party PR branches
no longer have cache write access (preventing cache poisoning attacks).
PRs run with a cold cache.
- Add SECURITY comments in action-version-bump.yml explaining that tags
are unsigned and recommending SHA pinning for hardened CI.
- Update install-github-workflow.ts template to include security note
about @v2 floating major tag and SHA pinning recommendation.
Closes#1685
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
* fix(ci): make pull request cache read-only
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
Co-authored-by: Aiden Bai <aiden@million.dev>
The 0.7.3 failure was IDENTITY_TOKEN_READ_ERROR mid parallel publish,
not missing registry-url. Adding registry-url writes an empty
_authToken line that blocks Trusted Publishing OIDC
(actions/setup-node#1551). Keep force_publish dispatch to finish
partial releases.
setup-node was missing registry-url on the main publish job, so trusted
publishing failed mid-release (react-doctor@0.7.3 / eslint-plugin). Match
publish-dev, and add workflow_dispatch force_publish to finish partial releases.
* feat(action,core): CI speedups — install cache, persistent scan caches, local diff scope (plans 09–11)
The GitHub Action's dominant cost on a PR run is the uncached install (~15s of
an ~18s step), not the scan. These three plans target the CI experience:
Plan 09 — cache the install (biggest CI win). A resolve-version step pins the
concrete published version (so the cache key is stable even for `latest`;
scripts/resolve-package-spec.mjs), an actions/cache step restores the install
keyed on version+node+os+arch (no fuzzy fallback — native ABI safety), and the
scan installs into the cached `--prefix` only on a miss. A non-cacheable
local-path spec keeps the npx path. ~15s install → ~1-2s restore on a hit.
Plan 11 — derive PR changed files locally + lock the diff fast path. The base
step now runs `git diff --name-only --diff-filter=AMR <base>...HEAD` (faster, no
API rate limit, works on forks), falling back to the GitHub API only when the
base isn't reachable; both share scripts/normalize-changed-files.mjs. A
regression test locks that diff mode skips dead-code + supply-chain (the
fast-path guarantee). A clearer degraded-mode warning points at `fetch-depth: 0`.
Plan 10 — persist scan caches across CI runs. `REACT_DOCTOR_CACHE_DIR` lets the
action point the engine's caches at a stable `${runner.temp}` path an
actions/cache step persists, so the per-file content-addressed lint cache (#900)
restores across commits — a PR re-lints only its changed files. A new
supply-chain per-PURL on-disk cache (24h TTL, fail-open, NO_CACHE-bypassed) skips
the Socket network for unchanged deps.
Unit/integration tests cover all three (version classification, changed-file
normalization, diff-fast-path skip, cache-dir override, supply-chain cache
hit/bypass); changeset for the npm-facing surface (REACT_DOCTOR_CACHE_DIR +
supply-chain cache). Action releases (tags) + the self-test cacheable-install
job (a workflow-file change needing `workflow` push scope) + R3 sparse-checkout
docs + plan-09's `--print-cache-key` follow-up are noted for a workflow-scoped
push / dogfooding pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(action): self-test the cacheable install path (plan 09)
Adds an `action-cacheable-install` job that runs the action with a published
`version:` (latest) so CI exercises the resolve-version + actions/cache +
prefix-install branch (the local-path job covers the npx branch). Advisory
(`blocking: none`). Split out from the main plan-09 commit because pushing a
`.github/workflows/` change requires `workflow` token scope.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cli): migrate mutable @main action refs to @v2 (action change → migration)
Since plans 09-11 change the action, register a once-per-repo project migration
(the framework's "action updates" path) that pins a mutable `@main` / `@master`
React Doctor action reference in `.github/workflows/*.yml` to the recommended
floating major `@v2`.
An unpinned `@main` runs whatever the action's HEAD points to with the
workflow's write permissions — a supply-chain risk (#299) — and the rewrite also
moves the workflow onto the install- and scan-cached release. Only mutable refs
are rewritten; pinned tags / SHAs are deliberate and untouched, a different
action on `@main` is ignored, and only the ref changes (owner, comments, and the
`version:` input are preserved). Runs once per repo like the legacy-config
migration and logs the change for review/commit (or revert if intentionally
tracking main). No-op (stays pending) when there's no mutable ref.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(action): update the action contract test for the plans 09-11 restructure
github-action.test.ts asserts the literal content of action.yml's steps; plans
09/11 moved that content into shared scripts, so two assertions broke:
- the inline `directoryPrefix` prefix-stripping is now in the shared
normalize-changed-files.mjs (used by both the local-diff base step and the API
fallback) — assert the wiring instead, with the behavior locked by
normalize-changed-files.test.ts.
- the inline `PACKAGE_SPEC="react-doctor@$INPUT_VERSION"` derivation moved to the
resolve-version step (resolve-package-spec.mjs) — assert it's read from that
step's output.
Adds a test for the new contract (resolve-version + the toolchain/scan
actions/cache steps + the cached prefix-install). Behavior unchanged; the
contract test now matches the reworked action.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: order the diagnostics dump by rule priority
Users report that `npx react-doctor` prints a large, undifferentiated dump and
they don't know what to tackle first. The score API now returns an intrinsic
priority/tier per rule; surface it so the dump leads with what's most worth
fixing instead of ordering purely by severity.
- core: `ScoreResult` carries the API's `rules: {ruleKey: {priority, tier}}`
payload (new `RulePriority`/`RuleTier` types); `parseScoreResult` captures it.
- CLI: rule-groups and categories sort by `effectivePriority` (API priority
desc). Unranked rules get a severity-based midpoint; with no score at all
(`--no-score` / API unreachable) ordering degrades to the previous
error-before-warning + count order, so there is no regression offline.
No priority table is duplicated here — the engine remains the single source of
truth and the values arrive over the score API.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): improve error handling for command execution failures
* refactor(core): validate the score API response with effect/Schema
Replace the hand-rolled type guards in `parseScoreResult`/`parseRulePriorities`
with `effect/Schema` (the schema lib already used across @react-doctor/core, see
schemas.ts) — `Schema.Struct` + `Schema.Literals` + `Schema.Record` decoded via
`decodeUnknownOption`. A malformed payload decodes to null and falls back to the
severity ordering, same as before. Result types are now `readonly` to match the
decoded shape.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
setup-node's registry-url writes an .npmrc with _authToken placeholder
that short-circuits the OIDC exchange. Also need npm 11.5.1+ for
trusted publishing support.