mirror of
https://github.com/millionco/react-doctor.git
synced 2026-09-14 20:00:24 +08:00
2cadd3fe2c
* 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>
67 lines
2.1 KiB
TypeScript
67 lines
2.1 KiB
TypeScript
import { describe, expect, it } from "vite-plus/test";
|
|
import { classifyVersionSpec } from "../../../scripts/resolve-package-spec.mjs";
|
|
|
|
// The action keys its install cache on the resolved react-doctor version, so the
|
|
// version→spec classification (cacheable registry range vs non-cacheable local
|
|
// path) is load-bearing for the cache being sound. Lock the branch logic.
|
|
describe("classifyVersionSpec", () => {
|
|
it("treats `latest` as a cacheable registry range", () => {
|
|
expect(classifyVersionSpec("latest")).toEqual({
|
|
cacheable: true,
|
|
spec: "react-doctor@latest",
|
|
registryRange: "latest",
|
|
});
|
|
});
|
|
|
|
it("treats an exact version as a cacheable registry spec", () => {
|
|
expect(classifyVersionSpec("2.1.0")).toEqual({
|
|
cacheable: true,
|
|
spec: "react-doctor@2.1.0",
|
|
registryRange: "2.1.0",
|
|
});
|
|
});
|
|
|
|
it("treats a semver range as cacheable", () => {
|
|
expect(classifyVersionSpec("^2")).toEqual({
|
|
cacheable: true,
|
|
spec: "react-doctor@^2",
|
|
registryRange: "^2",
|
|
});
|
|
});
|
|
|
|
it("treats a relative local path as a NON-cacheable passthrough spec (self-test path)", () => {
|
|
expect(classifyVersionSpec("./packages/react-doctor")).toEqual({
|
|
cacheable: false,
|
|
spec: "./packages/react-doctor",
|
|
registryRange: undefined,
|
|
});
|
|
expect(classifyVersionSpec("../foo")).toEqual({
|
|
cacheable: false,
|
|
spec: "../foo",
|
|
registryRange: undefined,
|
|
});
|
|
});
|
|
|
|
it("treats an absolute local path as non-cacheable", () => {
|
|
expect(classifyVersionSpec("/tmp/react-doctor")).toEqual({
|
|
cacheable: false,
|
|
spec: "/tmp/react-doctor",
|
|
registryRange: undefined,
|
|
});
|
|
});
|
|
|
|
it("defaults an empty / undefined version to `latest`", () => {
|
|
const expected = { cacheable: true, spec: "react-doctor@latest", registryRange: "latest" };
|
|
expect(classifyVersionSpec("")).toEqual(expected);
|
|
expect(classifyVersionSpec(undefined)).toEqual(expected);
|
|
});
|
|
|
|
it("trims surrounding whitespace", () => {
|
|
expect(classifyVersionSpec(" latest ")).toEqual({
|
|
cacheable: true,
|
|
spec: "react-doctor@latest",
|
|
registryRange: "latest",
|
|
});
|
|
});
|
|
});
|