CI/CD Integration
Guides
CI/CD Integration
Every scanner endpoint accepts an API key, so any of them can run as a pipeline step. This page covers what each tool's output means, how to turn it into a pass/fail gate, and working examples for four CI platforms.
Code scanning with the GitHub Action
The quickest way to scan code in CI. It runs @scanverra/cli on the runner (your code never leaves it), fails the job at the severity you choose, and uploads SARIF to GitHub code scanning.
on: [push, pull_request]
jobs:
scanverra:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v4
- uses: scanverra/scan-action@v1
with:
api-key: ${{ secrets.SCANVERRA_API_KEY }}
fail-on: highScoring basis per tool
What each endpoint returns, and what to threshold on.
| Tool | Output | Basis | Built-in gate |
|---|---|---|---|
| Website Audit | 4 scores 0-100: performance, seo, accessibility, bestPractices | Category scores from 0-100. Convention: ≥90 good, 50-89 needs work, <50 poor. | None built in — pick your own threshold per score. |
| Security Scan | riskScore 0-100 (higher = safer) | 100 minus severity-weighted, category-capped deductions. Grade bands: A+ ≥90 … F <30. | None built in — pick your own threshold. |
| Browser Audit | Arrays: jsErrors, brokenLinks, formIssues, imgIssues (each severity: "error" | "warning") | No aggregate score. | Gate on issue counts, e.g. any severity === "error". |
| Repo Scanner | score 0-100 + qualityGate: "passed" | "failed" | 100 minus severity-weighted deductions, capped per category. | Built in — auto-fails on any critical finding or score < 60. |
| SEO Optimizer | score 0-100 (on-page SEO + GEO/E-E-A-T signals) | 100 minus severity-weighted, category-capped deductions across on-page, GEO, and E-E-A-T findings. | None built in — pick your own threshold. |
Stopping the pipeline
CI mechanics are the same regardless of platform: a step that exits non-zero fails the job. To actually block a PR or merge request from merging, add that job as a required/status check in your repo host's branch protection or merge-request approval settings — a failed step alone just shows red, it doesn't stop a merge on its own.
Example pipelines
The same five checks — Audit, Security Scan, Browser Audit, Repo Scan, SEO Optimizer — written for four platforms. Pick your CI system below.
name: Scanverra checks
on: [pull_request]
jobs:
scanverra:
runs-on: ubuntu-latest
env:
API_KEY: ${{ secrets.SCANVERRA_API_KEY }}
TARGET_URL: https://staging.example.com # your deployed preview URL
steps:
- name: Website Audit (perf/SEO/a11y)
run: |
RES=$(curl -sf -X POST https://www.scanverra.com/api/audit \
-H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
-d "{\"url\":\"$TARGET_URL\"}")
echo "$RES" | jq .
PERF=$(echo "$RES" | jq '.performanceScore')
A11Y=$(echo "$RES" | jq '.accessibilityScore')
if (( $(echo "$PERF < 50" | bc -l) )) || (( $(echo "$A11Y < 90" | bc -l) )); then
echo "::error::Audit failed thresholds (perf=$PERF, a11y=$A11Y)"
exit 1
fi
- name: Security Scan
run: |
SCAN_ID=$(curl -sf -X POST https://www.scanverra.com/api/security-scan \
-H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
-d "{\"url\":\"$TARGET_URL\"}" | jq -r .scanId)
for i in $(seq 1 30); do
RESULT=$(curl -sf "https://www.scanverra.com/api/security-scan?id=$SCAN_ID" -H "X-API-Key: $API_KEY")
STATUS=$(echo "$RESULT" | jq -r .status)
[ "$STATUS" != "running" ] && break
sleep 10
done
RISK=$(echo "$RESULT" | jq '.riskScore')
echo "riskScore=$RISK"
[ "$STATUS" = "failed" ] && { echo "::error::Security scan failed to complete"; exit 1; }
[ "$RISK" -lt 70 ] && { echo "::error::Security risk score too low ($RISK)"; exit 1; }
true
- name: Browser Audit (JS errors, broken links)
run: |
TEST_ID=$(curl -sf -X POST https://www.scanverra.com/api/browser-test \
-H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
-d "{\"url\":\"$TARGET_URL\"}" | jq -r .testId)
for i in $(seq 1 30); do
RESULT=$(curl -sf "https://www.scanverra.com/api/browser-test?id=$TEST_ID" -H "X-API-Key: $API_KEY")
STATUS=$(echo "$RESULT" | jq -r .status)
[ "$STATUS" != "running" ] && break
sleep 10
done
ERRORS=$(echo "$RESULT" | jq '[.jsErrors[]? | select(.severity=="error")] | length')
[ "$STATUS" = "failed" ] && { echo "::error::Browser audit failed to complete"; exit 1; }
[ "$ERRORS" -gt 0 ] && { echo "::error::$ERRORS JS errors found"; exit 1; }
true
- name: Repo Scan (SAST, secrets, deps, quality gate)
run: |
SCAN_ID=$(curl -sf -X POST https://www.scanverra.com/api/repo/scan \
-H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
-d '{"owner":"your-org","repo":"your-repo","branch":"${{ github.head_ref }}"}' | jq -r .scanId)
for i in $(seq 1 30); do
RESULT=$(curl -sf "https://www.scanverra.com/api/repo/result?id=$SCAN_ID" -H "X-API-Key: $API_KEY")
STATUS=$(echo "$RESULT" | jq -r .status)
[ "$STATUS" != "running" ] && break
sleep 10
done
echo "$RESULT" | jq '{score, qualityGate}'
GATE=$(echo "$RESULT" | jq -r .qualityGate)
[ "$GATE" = "failed" ] && { echo "::error::Repo quality gate failed"; exit 1; }
true
- name: SEO Optimizer (on-page, GEO, E-E-A-T)
run: |
SCAN_ID=$(curl -sf -X POST https://www.scanverra.com/api/seo-optimizer \
-H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
-d "{\"url\":\"$TARGET_URL\"}" | jq -r .scanId)
for i in $(seq 1 30); do
RESULT=$(curl -sf "https://www.scanverra.com/api/seo-optimizer?id=$SCAN_ID" -H "X-API-Key: $API_KEY")
STATUS=$(echo "$RESULT" | jq -r .status)
[ "$STATUS" != "running" ] && break
sleep 10
done
SCORE=$(echo "$RESULT" | jq '.score')
echo "seoScore=$SCORE"
[ "$STATUS" = "failed" ] && { echo "::error::SEO scan failed to complete"; exit 1; }
[ "$SCORE" -lt 70 ] && { echo "::error::SEO score too low ($SCORE)"; exit 1; }
trueWhere to store the API key as a secret, per platform:
- GitHub Actions: repo secret — Settings → Secrets and variables → Actions.
- GitLab CI: masked CI/CD variable — Settings → CI/CD → Variables.
- Azure Pipelines: secret pipeline variable (lock icon) or a Variable Group under Pipelines → Library.
- Bitbucket Pipelines: Repository settings > Pipelines > Repository variables, marked as secured.
- Jenkins: Secret text credential — Manage Jenkins → Credentials — referenced via
credentials(). The agent image also needscurl,jq, andbcinstalled; GitHub-hosted and Azure-hostedubuntu-latestrunners already include them, GitLab'salpineimage doesn't (hence theapk addstep).
The Repo Scanner step needs your GitHub/Bitbucket/Azure account connected in Scanverra settings beforehand — it scans through the token stored there, not one passed per-request.
SARIF output for code scanning
SARIF (Static Analysis Results Interchange Format) is the standard JSON format code scanning tools use to exchange findings. The Scanverra CLI writes SARIF 2.1.0 with --sarif, so results show up wherever SARIF is read.
- GitHub code scanning: set sarif: true on scanverra/scan-action and findings appear in the Security tab and as pull request annotations.
- SonarQube and other platforms that import SARIF can ingest the same file.
- Bitbucket and GitLab: keep the SARIF file as a pipeline artifact, or use --json for your own reporting step.
- uses: actions/checkout@v4
- uses: scanverra/scan-action@v1
with:
api-key: ${{ secrets.SCANVERRA_API_KEY }}
sarif: true # uploads the report to GitHub code scanning
# The job needs: permissions: { security-events: write }Only the Repo Scanner returns a ready-made qualityGate field. For the other four tools, decide your own thresholds up front and keep them consistent across environments (e.g. don't compare a staging URL's security score against a threshold tuned for production).