SARIF & code scanning

Shipmoor Team
June 27, 2026
4 min read

SARIF is how Shipmoor findings become first-class citizens of your code-review surface: annotations on the PR diff, entries in the Security tab, and a history of what was found when. Shipmoor emits standard SARIF 2.1.0 from the same evidence as its human-readable and JSON output, so the three views never disagree about what the scan caught.

Producing SARIF

Add --sarif and point --output at a file:

shipmoor scan --diff origin/main...HEAD --sarif --output report.sarif

Every finding carries its rule ID (e.g. python.phantom_import), severity, location, message, and remediation text, plus a stable fingerprint, so code scanning can track a finding across pushes instead of re-opening it as new each time. Rule metadata links back to the rule’s documentation.

When --sarif writes to stdout instead of a file, stdout contains only the SARIF document (diagnostics go to stderr), so it pipes cleanly into another tool.

Suppressions stay in parity. A finding you’ve dismissed with an inline shipmoor:ignore directive is removed before the gate runs and before either machine format is written, so the SARIF omits exactly the same suppressed findings the JSON does. The editor, the JSON contract, and code scanning all agree on what was suppressed.

Uploading to GitHub code scanning

Hand the SARIF file to the official upload action:

      - name: Run Shipmoor
        run: |
          "$HOME/.shipmoor/bin/shipmoor" scan --diff origin/main...HEAD \
            --sarif --output report.sarif --fail-on high
      - name: Upload SARIF
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: report.sarif

Two details do the heavy lifting:

  • security-events: write: the job needs this permission for the upload to succeed. Declare it on the job alongside contents: read:
jobs:
  shipmoor:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      security-events: write
  • if: always(): the upload step must run even when the scan step failed the job. The runs you most want in the Security tab are exactly the ones where the gate fired, so you cannot let exit 1 short-circuit the upload.

The full workflow (and the composite-action form) is in GitHub Actions.

Exit codes: gate signal vs. error

The scan’s exit code and the SARIF are designed to be consumed together. A gate firing is not a tooling failure; the report is complete and worth uploading.

CodeWhat happenedIs the SARIF valid?
0Clean; no gate firedYes, complete (and empty of findings)
1Findings met the --fail-on gateYes, complete. This is the gate working, not an error.
2Usage or config errorNo report produced
3Scan failedNo report produced

So the correct CI shape is: let exit 1 fail the check (that is its job) while still uploading the report, and reserve “tooling broke” alerts for 2 and 3, where no report exists.

Uploading even when the gate fails

If you want SARIF in code scanning on every run, including the ones where a blocking finding fired the gate, make sure the failing exit code does not abort the job before the upload:

  • Simplest: keep --fail-on high on the scan step and mark the upload step if: always(), as above. The scan step still fails the job (so the check goes red), but the always-run upload step delivers the report first.
  • Decoupled: run the scan in a step that does not abort the job (capture its exit code), upload the SARIF, then fail the job yourself on the captured code:
      - name: Run Shipmoor (don't abort on a blocking finding)
        id: scan
        run: |
          "$HOME/.shipmoor/bin/shipmoor" scan --diff origin/main...HEAD \
            --sarif --output report.sarif --fail-on high \
            || echo "code=$?" >> "$GITHUB_OUTPUT"
      - name: Upload SARIF
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: report.sarif
      - name: Enforce the gate
        if: steps.scan.outputs.code == '1'
        run: exit 1

If you instead want code-scanning evidence with no blocking check at all, run with --fail-on none; you get full SARIF and a 0 exit on any successful scan.

Beyond GitHub

The SARIF is standard SARIF 2.1.0: GitLab, Azure DevOps, and most code-quality dashboards ingest it directly. For systems that prefer raw data, the JSON contract (shipmoor.scan.v1) carries everything the SARIF does and more (subtypes, change status, fingerprints), and, because both are built from one scan, omits the same suppressed findings.

Next

Last updated on June 27, 2026

Was this article helpful?

Your response is saved on this device.

Related Articles