blinkof.ai Run a free test →
Guide

Run a blink test on every deploy

Testing once tells you the app was fine once. Wire Blinkof.ai into your CI and every deploy gets the same pass a real user would give it — and the build fails before a new critical reaches anyone.

By Blinkof.ai·6 min read


Why test on every deploy

Vibe coding makes the next change as fast as the first. That's the point — and the risk. The signup that worked last week breaks when you tweak the form; a refactor re-exposes a key; a new page ships without security headers. A one-time audit can't see any of that. The fix isn't more discipline, it's making the check automatic: run it on every deploy, and let a real failure stop the release instead of a user finding it.

Blinkof.ai's API does exactly the free scan you already know — sign up like a customer, throw hostile input at your forms, read your shipped bundles and database rules, walk your money flows, check your privacy promises — and hands CI a score and a count of what's critical. This guide wires it into GitHub Actions; the same three calls work in any CI.

One rule that makes it work: scan the new version, not the old one

Blinkof tests your live URL over the network — it only ever sees whatever is serving there the moment it runs. So the whole thing hinges on timing: scan only once the new build is live. Kick off the scan the instant you push while the deploy is still building, and you've just graded the previous version. There are two timing-correct patterns, and they give you two different results:

  • A) A safety net on production — scan after the deploy goes live; a red build means "a regression just shipped — roll back." Catch-and-recover.
  • B) A gate before merge — deploy the PR to a verified staging URL, scan that, and block the merge on a new critical, with the verdict posted on the PR. Catch-before-users.

1. Get a token (and verify your domain)

On a Pro or Studio plan, open your dashboard, create an API token under "Blink test on every deploy," and add it to your repo as BLINKOF_TOKEN (Settings → Secrets and variables → Actions). Deep API scans only run against a host you've verified you own — so verify your production (and staging) domain in the dashboard first.

2A. Production safety net (post-deploy)

This keys off your host's deployment event, which reports success only once the new build is actually live — so there's nothing to wait for. Vercel, Cloudflare Pages, Netlify and friends all emit it.

name: Blink test (production)
on:
  deployment_status

jobs:
  blink:
    if: github.event.deployment_status.state == 'success'
    runs-on: ubuntu-latest
    steps:
      - name: Blinkof.ai scan
        env:
          BLINKOF_TOKEN: ${{ secrets.BLINKOF_TOKEN }}
          APP_URL: ${{ github.event.deployment_status.environment_url || 'https://your-app.com' }}
        run: |
          set -euo pipefail
          res=$(curl -sf -X POST https://blinkof.ai/api/v1/scans \
            -H "Authorization: Bearer $BLINKOF_TOKEN" -H "Content-Type: application/json" \
            -d "{\"url\":\"$APP_URL\"}")
          id=$(echo "$res" | jq -r '.id')
          echo "Report: https://blinkof.ai/report/$id"
          for _ in $(seq 1 60); do
            sleep 5
            out=$(curl -sf https://blinkof.ai/api/v1/scans/"$id" -H "Authorization: Bearer $BLINKOF_TOKEN")
            [ "$(echo "$out" | jq -r '.status')" = "complete" ] && break
          done
          echo "$out" | jq '{score, grade, counts, new}'
          crit=$(echo "$out" | jq -r '.counts.critical')
          if [ "$crit" != "0" ]; then
            echo "::error::$crit critical(s) live in production — roll back."
            exit 1
          fi

The deploy already happened, so this is an alarm, not a gate — a red run is your cue to roll back or hotfix.

2B. Gate the merge (pre-merge, on a verified staging URL)

To stop a regression before it ships, deploy the PR to a stable staging domain you've verified, wait until it's serving that commit, scan it, then block the merge and post the result on the PR. (Ephemeral per-deploy preview URLs won't work — a deep scan needs a verified host, and those random subdomains can't be verified. Use one stable staging domain.)

name: Blink test (PR gate)
on:
  pull_request:
    branches: [main]

permissions:
  contents: read
  pull-requests: write

jobs:
  blink:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Deploy this PR to staging
        run: ./deploy-staging.sh   # however you ship — e.g. wrangler deploy --env staging

      - name: Blink test staging
        id: blink
        env:
          BLINKOF_TOKEN: ${{ secrets.BLINKOF_TOKEN }}
          APP_URL: https://staging.your-app.com   # must be verified in Blinkof
        run: |
          set -euo pipefail
          # Wait until staging is serving THIS commit before scanning.
          # (Expose the build SHA at /version, or swap in your own liveness check.)
          for _ in $(seq 1 60); do
            [ "$(curl -sf "$APP_URL/version" || true)" = "$GITHUB_SHA" ] && break
            sleep 5
          done
          res=$(curl -sf -X POST https://blinkof.ai/api/v1/scans \
            -H "Authorization: Bearer $BLINKOF_TOKEN" -H "Content-Type: application/json" \
            -d "{\"url\":\"$APP_URL\"}")
          id=$(echo "$res" | jq -r '.id')
          for _ in $(seq 1 60); do
            sleep 5
            out=$(curl -sf https://blinkof.ai/api/v1/scans/"$id" -H "Authorization: Bearer $BLINKOF_TOKEN")
            [ "$(echo "$out" | jq -r '.status')" = "complete" ] && break
          done
          {
            echo "score=$(echo "$out" | jq -r '.score')"
            echo "crit=$(echo "$out" | jq -r '.counts.critical')"
            echo "new=$(echo "$out" | jq -r '.new // 0')"
            echo "report=https://blinkof.ai/report/$id"
          } >> "$GITHUB_OUTPUT"

      - name: Post the result on the PR
        if: always()
        uses: actions/github-script@v7
        env:
          SCORE: ${{ steps.blink.outputs.score }}
          CRIT: ${{ steps.blink.outputs.crit }}
          NEW: ${{ steps.blink.outputs.new }}
          REPORT: ${{ steps.blink.outputs.report }}
        with:
          script: |
            const { SCORE, CRIT, NEW, REPORT } = process.env;
            const body = `**Blink test** — score **${SCORE}/100** · ${CRIT} critical · ${NEW} new since last scan\n\n[Full report →](${REPORT})`;
            const cs = await github.rest.issues.listComments({ ...context.repo, issue_number: context.issue.number });
            const mine = cs.data.find(c => c.body.startsWith('**Blink test**'));
            if (mine) await github.rest.issues.updateComment({ ...context.repo, comment_id: mine.id, body });
            else await github.rest.issues.createComment({ ...context.repo, issue_number: context.issue.number, body });

      - name: Block merge on a new critical
        run: '[ "${{ steps.blink.outputs.crit }}" = "0" ] || { echo "::error::New criticals — fix before merge."; exit 1; }'

Make this job a required status check (Settings → Branches) and a failing blink test blocks the merge — with the score, the count of what's new, and a link to the full report sitting right on the PR.

3. Tune what fails the build

The example fails on any critical. Pick the bar that fits where you are:

  • Strictest — fail on anything new since the last scan: change the check to new=$(echo "$out" | jq -r '.new // 0'); [ "$new" = "0" ]. Great once you're at a clean baseline.
  • Balanced — fail on criticals only (the default above). Lets you ship with known low-priority items while blocking the dangerous ones.
  • Gentlest — never fail; just print the score and report link so it shows up in the run log. Drop the exit 1 branch.

Every run also updates the app on your dashboard, so you get the score-over-time graph and the "fixed / new since last scan" diff for free — CI just becomes another thing that triggers it.

The API, briefly

Two endpoints, Bearer-authed with your token:

  • POST /api/v1/scans with {"url":"…"}{ id, status, report_url, result_url }. Queues a deep scan.
  • GET /api/v1/scans/:id{ status, score, grade, counts, fixed, new, report_url } once complete.

It's just HTTP, so the same pattern drops into GitLab CI, CircleCI, a deploy hook, or a one-line cron — anywhere you can run curl.

Whatever you ship with

Vercel, Cloudflare, Netlify, Render, a VPS — Blinkof doesn't care how you deploy, because it tests the live URL. Just keep the one rule that makes any of this meaningful: scan only after the new build is live there, on a domain you've verified.

Try it once by hand first

Paste your URL and see the report you'll be gating on — free, in under a minute.

Run a free blink test →