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>
53 lines
2.1 KiB
JavaScript
53 lines
2.1 KiB
JavaScript
import fs from "node:fs";
|
|
import { pathToFileURL } from "node:url";
|
|
|
|
/**
|
|
* Map repo-root-relative changed-file paths (from `git diff --name-only` or the
|
|
* GitHub `pulls.listFiles` API) to the SCAN-relative paths the CLI's
|
|
* `--changed-files-from` expects. The CLI resolves changed-file entries relative
|
|
* to the scanned `directory` (its diff detection runs `git diff --relative`), so
|
|
* strip the `directory` prefix and drop files outside it — otherwise a
|
|
* subdirectory scan (`directory: UI`) doubles up to `UI/UI/src/...`, misses every
|
|
* base read, and reports pre-existing issues as newly introduced.
|
|
*
|
|
* Shared by the action's local-`git diff` path (the cheap default) and its
|
|
* GitHub-API fallback so the two derive the same set from one implementation.
|
|
*
|
|
* @param {ReadonlyArray<string>} files repo-root-relative changed-file paths
|
|
* @param {string | undefined} directory the scanned `directory` input
|
|
* @returns {string[]} scan-relative paths
|
|
*/
|
|
export const normalizeChangedFiles = (files, directory) => {
|
|
const directoryPrefix = String(directory ?? ".")
|
|
.replace(/^\.\/?/, "")
|
|
.replace(/\/$/, "");
|
|
return files
|
|
.map((file) => String(file).trim())
|
|
.filter(Boolean)
|
|
.flatMap((filename) => {
|
|
if (!directoryPrefix) return [filename];
|
|
const scopedPrefix = `${directoryPrefix}/`;
|
|
return filename.startsWith(scopedPrefix) ? [filename.slice(scopedPrefix.length)] : [];
|
|
});
|
|
};
|
|
|
|
// CLI: read newline-separated repo-root paths on stdin (from `git diff
|
|
// --name-only`), write the scan-relative set to argv[3] (or stdout). argv[2] is
|
|
// the scanned directory.
|
|
const main = () => {
|
|
const directory = process.argv[2];
|
|
const rawInput = fs.readFileSync(0, "utf8");
|
|
const normalized = normalizeChangedFiles(rawInput.split("\n"), directory);
|
|
const rendered = normalized.length > 0 ? `${normalized.join("\n")}\n` : "";
|
|
const outputPath = process.argv[3];
|
|
if (outputPath) {
|
|
fs.writeFileSync(outputPath, rendered);
|
|
} else {
|
|
process.stdout.write(rendered);
|
|
}
|
|
};
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
main();
|
|
}
|