Skip to content
SEO Optimizer is live, Audit on page SEO and AI-search readiness. Try it free
ログイン
CI/CD Integration

ガイド

CI/CD統合

すべてのスキャナーエンドポイントはAPIキーを受け付けるため、いずれもパイプラインのステップとして実行できます。このページでは、各ツールの出力の意味、それを合否ゲートに変換する方法、そして4つのCIプラットフォーム向けの実際に動作する例を紹介します。

GitHub Actionによるコードスキャン

CIでコードをスキャンする最短の方法です。ランナー上で@scanverra/cliを実行し(コードはランナーの外に出ません)、選んだ深刻度以上でジョブを失敗させ、SARIFをGitHubコードスキャンにアップロードします。

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
APIキーをリポジトリのシークレットSCANVERRA_API_KEYとして追加してください。Freeプランにはキーが1つ含まれ、アップロードしたスキャンは毎月のRepo Scanner回数に含まれます。以下のURLベースのチェックはREST APIを使います。

ツールごとのスコアリング基準

各エンドポイントが返す内容と、閾値をどこに設定すべきか。

ツール出力基準組み込みゲート
Webサイト監査0〜100の4つのスコア: performance、seo、accessibility、bestPracticesカテゴリ別スコア(0〜100)。基準:90以上が良好、50〜89が要改善、50未満が不良。組み込みなし — スコアごとに独自の閾値を設定してください。
セキュリティスキャンriskScore 0〜100(数値が高いほど安全)100から深刻度で重み付けされ、カテゴリごとに上限が設定された減点を引いた値。グレード帯: A+ 90以上 … F 30未満。組み込みなし — 独自の閾値を設定してください。
ブラウザ監査配列: jsErrors、brokenLinks、formIssues、imgIssues(それぞれseverity: "error" | "warning")総合スコアなし。問題数でゲートする、例えばseverity === "error"が1件でもあれば失敗とする。
リポジトリスキャナーscore 0〜100 + qualityGate: "passed" | "failed"100から深刻度で重み付けされ、カテゴリごとに上限が設定された減点を引いた値。組み込み済み — 重大な検出結果が1件でもある場合、またはscore が60未満の場合に自動的に失敗します。
SEOオプティマイザーscore 0〜100(オンページSEO + GEO/E-E-A-Tシグナル)オンページ、GEO、E-E-A-Tの検出結果について、深刻度で重み付けされ、カテゴリごとに上限が設定された減点を100から引いた値。組み込みなし — 独自の閾値を設定してください。

パイプラインを停止する

CIの仕組みはプラットフォームに関わらず同じです: 非ゼロで終了するステップはジョブを失敗させます。PRやマージリクエストのマージを実際にブロックするには、リポジトリホストのブランチ保護やマージリクエスト承認設定で、そのジョブを必須のステータスチェックとして追加する必要があります — 失敗したステップは単に赤く表示されるだけで、それ自体がマージを止めるわけではありません。

パイプラインの例

Audit、Security Scan、Browser Audit、Repo Scan、SEOオプティマイザーという同じ5つのチェックを、4つのプラットフォーム向けに記述した例です。以下から使用するCIシステムを選んでください。

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

プラットフォームごとに、APIキーをシークレットとして保存する場所:

  • GitHub Actions: リポジトリシークレット — Settings → Secrets and variables → Actions。
  • GitLab CI: マスクされたCI/CD変数 — Settings → CI/CD → Variables。
  • Azure Pipelines: シークレットパイプライン変数(鍵アイコン)、またはPipelines → Library配下のVariable Group。
  • Bitbucket Pipelines:リポジトリ設定 > Pipelines > リポジトリ変数で、Securedとして設定します。
  • Jenkins: Secret text credential — Manage Jenkins → Credentials — credentials()経由で参照します。エージェントイメージにはcurl、jq、bcのインストールも必要です。GitHubおよびAzureがホストするubuntu-latestランナーにはすでに含まれていますが、GitLabのalpineイメージには含まれていません(そのためapk addのステップが必要です)。

リポジトリスキャナーのステップを実行するには、事前にScanverraの設定でGitHub/Bitbucket/Azureアカウントを連携しておく必要があります — スキャンはリクエストごとに渡すトークンではなく、そこに保存されたトークンを使って実行されます。

アカウントを連携したくない場合は? @scanverra/cliはランナー上でチェックアウト済みのコードを直接スキャンし、必要なのはAPIキーのみで、GitHub、Bitbucket、Azureとの連携は不要です。

コードスキャン用のSARIF出力

SARIF(Static Analysis Results Interchange Format)は、コードスキャンツールが検出結果を交換するための標準JSON形式です。Scanverra CLIは--sarifでSARIF 2.1.0を出力するため、SARIFを読み込めるあらゆる場所に結果を表示できます。

  • GitHubコードスキャン:scanverra/scan-actionでsarif: trueを設定すると、検出結果がSecurityタブとプルリクエストの注釈に表示されます。
  • SonarQubeなどSARIFをインポートできるプラットフォームでも同じファイルを取り込めます。
  • BitbucketとGitLab:SARIFファイルをパイプラインのアーティファクトとして保存するか、独自のレポート用に--jsonを使ってください。
- 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 }
クォータに関する注意: FreeプランのAPIキーは無料のWebアカウントと同じクォータ制限を受けます — CIの実行も月間スキャン上限にカウントされ、PRごとにスキャンすると上限をすぐに使い切ってしまう可能性があります。Pro、Team、EnterpriseのAPIキーは従量制限がありません。制限と挙動を参照してください。

そのまま使えるqualityGateフィールドを返すのはリポジトリスキャナーのみです。他の4つのツールについては、事前に独自の閾値を決め、環境間で一貫性を保つようにしてください(例えば、ステージング用URLのセキュリティスコアを、本番向けに調整した閾値と比較しないでください)。