Skip to content
SEO Optimizer is live, Audit on page SEO and AI-search readiness. Try it free
Sign in
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: high
Add your API key as the SCANVERRA_API_KEY repository secret. The Free plan includes one key; each uploaded scan counts toward your monthly Repo Scanner scans. The URL-based checks below use the REST API.

Scoring basis per tool

What each endpoint returns, and what to threshold on.

ToolOutputBasisBuilt-in gate
Website Audit4 scores 0-100: performance, seo, accessibility, bestPracticesCategory scores from 0-100. Convention: ≥90 good, 50-89 needs work, <50 poor.None built in — pick your own threshold per score.
Security ScanriskScore 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 AuditArrays: jsErrors, brokenLinks, formIssues, imgIssues (each severity: "error" | "warning")No aggregate score.Gate on issue counts, e.g. any severity === "error".
Repo Scannerscore 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 Optimizerscore 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; }
          true

Where 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 needs curl, jq, and bc installed; GitHub-hosted and Azure-hosted ubuntu-latest runners already include them, GitLab's alpine image doesn't (hence the apk add step).

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.

Prefer not to connect an account? @scanverra/cli scans the checked-out code directly in the runner and needs only an API key, no GitHub/Bitbucket/Azure connection.

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 }
Quota note: a Free-plan API key is quota-limited the same as a Free web account — CI runs still count against your monthly scan limit and can exhaust it fast if you scan on every PR. Pro, Team, and Enterprise API keys are unmetered. See Limits & Behavior.

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).