FixRelay is a GitHub Action and Node CLI that turns SARIF security scanner output plus PR diff context into:
merge-risk-report.md: a human-readable merge-risk report with one embedded AI agent fix prompt.normalized-findings.json: normalized SARIF findings annotated with diff context.
In GitHub Actions, FixRelay can also post or update a single PR comment using
the marker <!-- fixrelay-comment:start -->.
FixRelay is not a scanner and does not call an LLM. It is deterministic glue around scanner output, git diff context, and a small risk-scoring model.
Runtime requirements:
- Node.js 20 or newer.
- No npm runtime dependencies.
- Semgrep installed only if you want FixRelay to run its built-in Semgrep audit.
From this repository:
npm linkThen, inside any git repository:
fixrelayBy default, the local CLI:
- Detects
origin/main, thenorigin/master. - Reviews
<base>...HEAD. - Runs built-in Semgrep when no
--sarifis provided and Semgrep is installed. - Prints a concise terminal summary.
- Writes
fixrelay-out/merge-risk-report.mdandfixrelay-out/normalized-findings.json.
Example output:
FixRelay: medium risk
Decision: warn
Findings: 2 PR-relevant / 7 total
Report: fixrelay-out/merge-risk-report.md
Findings JSON: fixrelay-out/normalized-findings.json
Use an explicit base when your branch does not track origin/main or
origin/master:
fixrelay --base origin/developUse --fail-on high when you want the local command to exit non-zero for
high-risk changes:
fixrelay --fail-on highDrop this into .github/workflows/fixrelay.yml:
name: FixRelay
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: write
issues: write
jobs:
fixrelay:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Semgrep
run: python -m pip install semgrep
- name: Run FixRelay
uses: willwang0202/FixRelay@v1
env:
GITHUB_TOKEN: ${{ github.token }}
with:
diff: origin/${{ github.base_ref }}...HEAD
fail-on: high
protected-paths: |
auth/
billing/
infra/
.github/workflows/This uses FixRelay's built-in Semgrep path. When no sarif input is provided
and run-semgrep is true, FixRelay runs:
semgrep scan --config auto --sarif --output <out-dir>/semgrep.sarif --no-git-ignoreFor a stable major version, use willwang0202/FixRelay@v1. For a fully pinned
release, use a patch tag such as willwang0202/FixRelay@v1.0.2 or a commit SHA.
If the check passes but no PR comment appears, verify that the workflow has
issues: write, pull-requests: write, GITHUB_TOKEN, and repository Actions
read/write workflow permissions.
If you already run Semgrep, CodeQL, Trivy, Snyk, or another SARIF-producing
scanner, pass the SARIF file explicitly. When sarif is set, FixRelay does not
run built-in Semgrep.
- name: Run Semgrep
run: semgrep scan --config auto --sarif --output semgrep.sarif || true
- name: Run FixRelay
uses: willwang0202/FixRelay@v1
env:
GITHUB_TOKEN: ${{ github.token }}
with:
sarif: semgrep.sarif
diff: origin/${{ github.base_ref }}...HEAD
fail-on: highMultiple SARIF files can be passed as newline- or comma-separated values.
If generated files or data files make the PR diff too noisy, build a filtered
diff file yourself and pass it with diff-file.
- name: Build FixRelay diff
run: |
git diff --unified=0 origin/${{ github.base_ref }}...HEAD -- \
. \
':!data/**' \
':!web/public/songs.json' \
> fixrelay.diff
- name: Run FixRelay
uses: willwang0202/FixRelay@v1
env:
GITHUB_TOKEN: ${{ github.token }}
with:
sarif: semgrep.sarif
diff-file: fixrelay.diff
fail-on: highdiff-file takes precedence over diff.
When FixRelay runs git diff itself, it allows up to 50 MB of diff output. If
the unified diff still exceeds the Node process buffer, FixRelay falls back to:
git diff --name-only <range>That keeps changed-file filtering working, but changed-line scoring is not available for that run.
These are defined in action.yml.
| Input | Default | Behavior |
|---|---|---|
run-semgrep |
true |
Runs built-in Semgrep only when sarif is empty. |
semgrep-config |
auto |
Value passed to Semgrep --config. |
sarif |
empty | Newline- or comma-separated SARIF files to load. |
diff |
empty | Git diff range such as origin/main...HEAD. On PR events, the action infers origin/<base>...HEAD if neither diff nor diff-file is set. |
diff-file |
empty | Path to a saved unified diff file. Takes precedence over diff. |
out-dir |
fixrelay-out |
Directory for generated artifacts. |
fail-on |
high |
Fails the action when risk reaches this threshold. Allowed: low, medium, high, critical, unknown, never. |
post-comment |
true |
Posts or updates the FixRelay PR comment when PR context exists. |
scope |
pr |
pr scores only findings in changed files. entire-repo scores all loaded findings. |
protected-paths |
auth/, billing/, infra/, .github/workflows/ |
Newline- or comma-separated protected path prefixes. |
These are written to GITHUB_OUTPUT.
| Output | Meaning |
|---|---|
risk |
Risk level: low, medium, high, critical, or unknown. |
decision |
Merge decision: allow, warn, or block. |
report |
Absolute path to merge-risk-report.md. |
findings |
Absolute path to normalized-findings.json. |
From this repository:
node bin/fixrelay.js review \
--sarif examples/semgrep.sarif \
--diff-file examples/pr.diff \
--out-dir fixrelay-out \
--fail-on neverfixrelay review is the local developer command, and bare fixrelay is an
alias for it.
Usage:
fixrelay [options]
fixrelay review [options]
fixrelay generate --sarif <file> [--diff <range>|--diff-file <file>] [options]
Review options:
--base <ref>: base ref for local review. Defaults toorigin/main, thenorigin/master.--sarif <file>: SARIF scanner output. Repeatable.--diff <range>: git diff range, such asorigin/main...HEAD.--diff-file <file>: saved unified diff file.--out-dir <dir>: artifact directory. Defaults tofixrelay-out.--fail-on <level>:low,medium,high,critical,unknown, ornever. CLI default isnever.--scope <scope>:prorentire-repo. Defaults topr.--pr-title <text>and--pr-body <text>: report context.--protected-path <path>: protected path prefix. Repeatable.--no-semgrep: skip built-in Semgrep when no--sarifis provided.--semgrep-config <cfg>: Semgrep--configvalue. Defaults toauto.
fixrelay generate is the lower-level command for scripts and CI jobs. It
prints the full JSON summary instead of the concise terminal summary.
Generate options:
--sarif <file>: SARIF scanner output. Repeatable.--diff <range>: git diff range, such asorigin/main...HEAD.--diff-file <file>: saved unified diff file.--out-dir <dir>: artifact directory. Defaults tofixrelay-out.--fail-on <level>:low,medium,high,critical,unknown, ornever. CLI default isnever.--scope <scope>:prorentire-repo. Defaults topr.--pr-title <text>and--pr-body <text>: report context.--protected-path <path>: protected path prefix. Repeatable.--no-semgrep: skip built-in Semgrep when no--sarifis provided.--semgrep-config <cfg>: Semgrep--configvalue. Defaults toauto.--post-comment: post the report withgh pr comment.
--post-comment requires GITHUB_TOKEN or GH_TOKEN, and the gh CLI must be
available.
FixRelay parses unified diff output and tracks:
- changed files
- changed lines
- test file changes
- CI workflow changes
- dependency manifest changes
scope: pr keeps only scanner findings whose file appears in the diff.
scope: entire-repo keeps every loaded scanner finding.
If no explicit diff or diff-file is provided outside GitHub PR context,
FixRelay tries git diff --unified=0 HEAD. If that produces no changed files
and scope is pr, FixRelay falls back to treating scanner finding files as
changed files. This keeps local no-diff runs useful, but PR workflows should
prefer an explicit diff or diff-file.
Risk scoring is deterministic.
Per finding:
- Low severity:
+10 - Medium severity:
+22 - High severity:
+35 - Critical severity:
+55 - Finding is in a changed file:
+15 - Finding is on a changed line:
+15 - Finding is in a protected path:
+15
Per PR:
- Findings exist and no test changes were detected:
+10 - CI/CD workflow files changed:
+12 - Dependency manifest or lockfile changed:
+8
Risk levels:
critical: score is at least100, or any finding is critical.high: score is at least70, or any finding is high.medium: score is at least35.low: anything lower.unknown: no scanner output was loaded because built-in Semgrep was missing or failed and no SARIF was provided.
Decisions:
low:allowmedium:warnhigh:blockcritical:blockunknown:warn
fail-on uses threshold comparison for low, medium, high, and
critical. unknown is special: it fails only when fail-on: unknown.
merge-risk-report.md includes:
- stable FixRelay comment markers
- merge risk heading
- decision text
- score or
N/Afor unknown risk - reasons
- up to three top findings
- recommended action
- one AI agent fix prompt for the first generated task
normalized-findings.json includes normalized SARIF fields plus:
is_in_diffis_on_changed_lineis_blocking
- FixRelay only loads SARIF. There is no generic JSON scanner input.
- Built-in Semgrep requires the
semgrepcommand to be installed. - SARIF parsing uses the first physical location for each result.
scope: prfilters by changed file, not by git blame. A finding in a changed file may have existed before the PR.- Large diff fallback keeps file-level context but loses changed-line context.
- The generated report embeds one agent prompt, based on the first task.
- FixRelay does not call an LLM and does not verify that a proposed fix is correct.
FixRelay reads local SARIF files and local git diff context. It writes local
artifacts. In GitHub Actions it talks to GitHub only when post-comment is
true and PR context exists.
It does not upload source code to a hosted model, train on code, or store code.
FixRelay is licensed under the Business Source License 1.1 (BUSL-1.1).
Internal use in your own repositories, CI/CD systems, and security workflows is
permitted. Providing FixRelay as a hosted or managed service to third parties
requires a commercial license.
Change Date: 2030-05-14. Versions convert to Apache 2.0 on that date or the
fourth anniversary of their first public distribution, whichever is earlier.
npm test
node bin/fixrelay.js generate \
--sarif examples/semgrep.sarif \
--diff-file examples/pr.diff \
--out-dir tmp/fixrelay-demo \
--fail-on never