mirror of
https://github.com/millionco/react-doctor.git
synced 2026-09-14 20:00:24 +08:00
b612664043
Co-authored-by: Cursor <cursoragent@cursor.com>
193 lines
7.1 KiB
YAML
193 lines
7.1 KiB
YAML
name: "React Doctor"
|
|
description: "Scan React codebases for security, performance, and correctness issues"
|
|
branding:
|
|
icon: "activity"
|
|
color: "blue"
|
|
|
|
inputs:
|
|
directory:
|
|
description: "Project directory to scan"
|
|
default: "."
|
|
project:
|
|
description: "Workspace project(s) to scan (comma-separated)"
|
|
required: false
|
|
fail-on:
|
|
description: "Exit non-zero when diagnostics fire (error, warning, none)."
|
|
default: "error"
|
|
non-blocking:
|
|
description: "Advisory mode. Report findings but always exit 0 so neither the fail-on gate nor a scan error fails the workflow."
|
|
default: "false"
|
|
comment:
|
|
description: "Create or update a sticky pull request summary comment."
|
|
default: "true"
|
|
annotations:
|
|
description: "Emit diagnostics as GitHub Actions annotations."
|
|
default: "true"
|
|
node-version:
|
|
description: "Node.js version to use"
|
|
default: "22"
|
|
version:
|
|
description: "react-doctor npm version or package spec to run"
|
|
default: "latest"
|
|
|
|
outputs:
|
|
score:
|
|
description: "Health score (0-100). Empty when scoring was unavailable."
|
|
value: ${{ steps.render.outputs.score }}
|
|
total-issues:
|
|
description: "Total diagnostic count in the scan report."
|
|
value: ${{ steps.render.outputs.total-issues }}
|
|
error-count:
|
|
description: "Error diagnostic count in the scan report."
|
|
value: ${{ steps.render.outputs.error-count }}
|
|
warning-count:
|
|
description: "Warning diagnostic count in the scan report."
|
|
value: ${{ steps.render.outputs.warning-count }}
|
|
affected-files:
|
|
description: "Number of files with diagnostics."
|
|
value: ${{ steps.render.outputs.affected-files }}
|
|
|
|
runs:
|
|
using: "composite"
|
|
steps:
|
|
- uses: actions/setup-node@v5
|
|
with:
|
|
node-version: ${{ inputs.node-version }}
|
|
package-manager-cache: false
|
|
|
|
- id: pr-files
|
|
if: ${{ github.event_name == 'pull_request' }}
|
|
uses: actions/github-script@v8
|
|
env:
|
|
REACT_DOCTOR_CHANGED_FILES: ${{ runner.temp }}/react-doctor-changed-files.txt
|
|
with:
|
|
script: |
|
|
const fs = require("fs");
|
|
const outputPath = process.env.REACT_DOCTOR_CHANGED_FILES;
|
|
const pullRequest = context.payload.pull_request;
|
|
if (!pullRequest || !outputPath) return;
|
|
let files;
|
|
try {
|
|
files = await github.paginate(github.rest.pulls.listFiles, {
|
|
...context.repo,
|
|
pull_number: pullRequest.number,
|
|
per_page: 100,
|
|
});
|
|
} catch (error) {
|
|
core.warning(
|
|
`React Doctor could not list pull request files (${error.message}). ` +
|
|
"Scanning the full project instead. Grant the workflow `pull-requests: read` " +
|
|
"permission to scan only the files changed in the pull request.",
|
|
);
|
|
return;
|
|
}
|
|
const includedStatuses = new Set(["added", "modified", "renamed"]);
|
|
const changedFiles = files
|
|
.filter((file) => includedStatuses.has(file.status))
|
|
.map((file) => file.filename);
|
|
fs.writeFileSync(outputPath, changedFiles.join("\n") + (changedFiles.length ? "\n" : ""));
|
|
core.setOutput("path", outputPath);
|
|
|
|
- id: scan
|
|
shell: bash
|
|
env:
|
|
NO_COLOR: "1"
|
|
INPUT_DIRECTORY: ${{ inputs.directory }}
|
|
INPUT_PROJECT: ${{ inputs.project }}
|
|
INPUT_FAIL_ON: ${{ inputs.fail-on }}
|
|
INPUT_ANNOTATIONS: ${{ inputs.annotations }}
|
|
INPUT_VERSION: ${{ inputs.version }}
|
|
CHANGED_FILES_FROM: ${{ steps.pr-files.outputs.path }}
|
|
RUNNER_TEMP: ${{ runner.temp }}
|
|
GITHUB_RUN_ID: ${{ github.run_id }}
|
|
run: |
|
|
REPORT_FILE="${RUNNER_TEMP:-/tmp}/react-doctor-report-${GITHUB_RUN_ID:-$$}.json"
|
|
echo "report-file=$REPORT_FILE" >> "$GITHUB_OUTPUT"
|
|
|
|
FLAGS=("--json" "--json-compact" "--fail-on" "$INPUT_FAIL_ON")
|
|
if [ "$INPUT_ANNOTATIONS" = "true" ]; then FLAGS+=("--annotations"); fi
|
|
if [ -n "$INPUT_PROJECT" ]; then FLAGS+=("--project" "$INPUT_PROJECT"); fi
|
|
if [ -n "$CHANGED_FILES_FROM" ]; then FLAGS+=("--changed-files-from" "$CHANGED_FILES_FROM"); fi
|
|
|
|
if [[ "$INPUT_VERSION" = ./* || "$INPUT_VERSION" = ../* || "$INPUT_VERSION" = /* ]]; then
|
|
PACKAGE_SPEC="$INPUT_VERSION"
|
|
else
|
|
PACKAGE_SPEC="react-doctor@$INPUT_VERSION"
|
|
fi
|
|
|
|
set +e
|
|
npm exec --yes --package "$PACKAGE_SPEC" -- react-doctor "$INPUT_DIRECTORY" "${FLAGS[@]}" > "$REPORT_FILE"
|
|
SCAN_STATUS=$?
|
|
set -e
|
|
|
|
if ! node "$GITHUB_ACTION_PATH/scripts/ensure-json-report.mjs" "$REPORT_FILE" "$SCAN_STATUS"; then
|
|
SCAN_STATUS=1
|
|
fi
|
|
echo "exit-code=$SCAN_STATUS" >> "$GITHUB_OUTPUT"
|
|
|
|
- id: render
|
|
if: always()
|
|
shell: bash
|
|
env:
|
|
REPORT_FILE: ${{ steps.scan.outputs.report-file }}
|
|
COMMENT_FILE: ${{ runner.temp }}/react-doctor-comment.md
|
|
GITHUB_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
run: |
|
|
node "$GITHUB_ACTION_PATH/scripts/render-github-action-comment.mjs" "$REPORT_FILE" "$COMMENT_FILE"
|
|
echo "comment-file=$COMMENT_FILE" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Update sticky PR comment
|
|
if: ${{ always() && github.event_name == 'pull_request' && inputs.comment == 'true' }}
|
|
uses: actions/github-script@v8
|
|
env:
|
|
REACT_DOCTOR_COMMENT_FILE: ${{ steps.render.outputs.comment-file }}
|
|
with:
|
|
script: |
|
|
const fs = require("fs");
|
|
const commentPath = process.env.REACT_DOCTOR_COMMENT_FILE;
|
|
if (!commentPath || !fs.existsSync(commentPath)) return;
|
|
|
|
const body = fs.readFileSync(commentPath, "utf8").trim();
|
|
if (!body) return;
|
|
|
|
const marker = "<!-- react-doctor:summary -->";
|
|
try {
|
|
const { data: comments } = await github.rest.issues.listComments({
|
|
...context.repo,
|
|
issue_number: context.issue.number,
|
|
per_page: 100,
|
|
});
|
|
const previous = comments.find((comment) => comment.body?.startsWith(marker));
|
|
if (previous) {
|
|
await github.rest.issues.updateComment({
|
|
...context.repo,
|
|
comment_id: previous.id,
|
|
body,
|
|
});
|
|
} else {
|
|
await github.rest.issues.createComment({
|
|
...context.repo,
|
|
issue_number: context.issue.number,
|
|
body,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
core.warning(`React Doctor could not update the sticky PR comment: ${error.message}`);
|
|
}
|
|
|
|
- name: Fail if React Doctor found blocking issues
|
|
if: always()
|
|
shell: bash
|
|
env:
|
|
SCAN_STATUS: ${{ steps.scan.outputs.exit-code }}
|
|
INPUT_NON_BLOCKING: ${{ inputs.non-blocking }}
|
|
run: |
|
|
SCAN_STATUS="${SCAN_STATUS:-1}"
|
|
if [ "$INPUT_NON_BLOCKING" = "true" ]; then
|
|
if [ "$SCAN_STATUS" != "0" ]; then
|
|
echo "::notice title=React Doctor::Scan exited ${SCAN_STATUS}; non-blocking is enabled, so the job will not fail."
|
|
fi
|
|
exit 0
|
|
fi
|
|
exit "$SCAN_STATUS"
|