CI guides

Recipes

Nightly scans, strict staging, Vercel and Netlify deploys, scripts, matrices and Slack.

Ready-made setups for common jobs. Each uses the CLI in GitHub Actions; the commands are the same in any CI, see the CI guides.

End-to-End on every push, Security nightly

A quick End-to-End scan on every push to main, and the fuller Security scan once a night, when nobody's waiting on it:

YAML
name: ValiqAI

on:
  push:
    branches: [main]
  schedule:
    - cron: '0 3 * * *'  # 03:00 UTC

jobs:
  valiqai:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - name: ValiqAI scan
        run: |
          if [ "${{ github.event_name }}" = "schedule" ]; then
            npx -y @valiqai/cli scan "$STAGING_URL" --type security --chatbot-consent --fail-on high --label nightly
          else
            npx -y @valiqai/cli scan "$STAGING_URL" --type e2e --fail-on blocker
          fi
        env:
          VALIQAI_API_KEY: ${{ secrets.VALIQAI_API_KEY }}
          STAGING_URL: https://staging.example.com

Add --chatbot-consent only if you're allowed to security-test the site's chatbot. What each scan costs is in Choose a scan.

Strict on staging, blockers only in production

Fail staging builds on anything high or worse, and production only on blockers. With both addresses in valiqai.json:

Terminal
$ valiqai scan --fail-on high                         # staging, the default environment$ valiqai scan --env production --fail-on blocker

Scan after a Vercel or Netlify deploy

ValiqAI scans sites on domains you've verified, so scan your own domain, not a *.vercel.app or *.netlify.app address, which you can't verify. Give your staging branch a domain of yours:

  • Vercel: assign a domain such as staging.example.com to the staging branch (Project › Settings › Domains).
  • Netlify: set a branch subdomain on your custom domain (Domain management › Branch subdomains).

Both report deployments to GitHub, so a workflow can wait for the deploy to finish:

YAML
name: Scan after deploy

on: deployment_status

jobs:
  valiqai:
    if: github.event.deployment_status.state == 'success' && github.event.deployment.environment == 'Preview'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npx -y @valiqai/cli scan https://staging.example.com --fail-on blocker
        env:
          VALIQAI_API_KEY: ${{ secrets.VALIQAI_API_KEY }}

Match the environment name to what your host reports. A deployment behind Vercel's or Netlify's password protection is refused before it starts (VQ711): scan one with protection off.

Post results to Slack

The simplest way is ValiqAI's Slack integration (Integrations › Slack), which posts scan results to a channel you pick.

To post your own message instead, point a webhook at a small function that checks the signature and forwards a line to a Slack incoming webhook:

Code
// An Express app; the same works as a serverless function.
import express from 'express';
import { verifyValiqaiSignature } from './verify.js'; // from the webhooks page

const app = express();

app.post('/hooks/valiqai', express.raw({ type: 'application/json' }), async (req, res) => {
  const raw = req.body.toString('utf8');
  if (!verifyValiqaiSignature(raw, req.get('X-ValiqAI-Signature') ?? '', process.env.VALIQAI_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }
  const { event, data } = JSON.parse(raw);
  const text =
    event === 'scan.completed'
      ? `ValiqAI: ${data.url} ${data.status}, score ${data.score}. https://www.valiqai.com/runs/${data.id}`
      : `ValiqAI: the scan of ${data.url} didn't finish (${data.error}).`;
  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ text }),
  });
  res.sendStatus(200);
});

app.listen(3000);

Subscribe the webhook to scan.completed and scan.failed, and answer within 10 seconds, or the delivery is tried again.

Use --json in a script

--json prints one result on stdout and the progress on stderr, so the result pipes cleanly:

Terminal
#!/usr/bin/env bash
set -uo pipefail

result=$(npx -y @valiqai/cli scan https://staging.example.com --json --fail-on none)
blockers=$(echo "$result" | jq '.counts.blocker')
echo "Score $(echo "$result" | jq '.score'), $blockers blocker(s): $(echo "$result" | jq -r '.runUrl')"

if [ "$blockers" -gt 0 ]; then
  echo "$result" | jq -r '.problems[] | select(.status == "open" and .severity == "blocker") | "- \(.title)"'
  exit 1
fi

--fail-on none keeps the exit code at 0 for a finished scan, so the script decides. The fields are in JSON output.

Scan several sites in a matrix

One job per site. A workspace runs one scan at a time, so the jobs take turns: each waits for the one before, up to --timeout. Running them one after another keeps that wait short:

YAML
jobs:
  valiqai:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      max-parallel: 1
      matrix:
        url:
          - https://staging.example.com
          - https://staging.shop.example.com
          - https://docs.example.com
    steps:
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npx -y @valiqai/cli scan "${{ matrix.url }}" --fail-on blocker --timeout 30m
        env:
          VALIQAI_API_KEY: ${{ secrets.VALIQAI_API_KEY }}

Every domain must be verified: by you for a personal key, or by anyone on the team for a team key.