Files
millionco__react-doctor/action.yml
T
2026-05-31 03:04:46 -04:00

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"