Run governed Agentic scenarios on every pull request. The action dispatches your chosen scenarios, polls until they finish, and fails the build on regressions.
In your GitHub repository settings → Secrets and variables → Actions, add:
ASSERTIVE_PROJECT_ID — your Assertive project UUIDASSERTIVE_CI_HMAC_SECRET — a shared random string (also saved in your Assertive backend)ASSERTIVE_SCENARIO_IDS — comma-separated scenario UUIDs (a repo variable is fine)Generate the HMAC secret with openssl rand -hex 32 and paste the same value in both places.
Save this as .github/workflows/assertive.yml:
name: Assertive QA
on:
pull_request:
workflow_dispatch:
jobs:
qa:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Assertive scenarios
uses: vancoregroup/assertive-action@v1
with:
endpoint: https://assertive.tech
project_id: ${{ secrets.ASSERTIVE_PROJECT_ID }}
scenario_ids: ${{ vars.ASSERTIVE_SCENARIO_IDS }}
hmac_secret: ${{ secrets.ASSERTIVE_CI_HMAC_SECRET }}
poll_timeout_seconds: "900"
The endpoint is HMAC-signed. Reject responses that come back with 401 — the signature or secret is off.
BODY='{"project_id":"<uuid>","scenario_ids":["<uuid>"],"triggered_by":"manual"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$ASSERTIVE_CI_HMAC_SECRET" | awk '{print $2}')
curl -X POST https://assertive.tech/api/public/hooks/ci-dispatch \
-H "Content-Type: application/json" \
-H "x-assertive-signature: $SIG" \
--data "$BODY"
The dispatch response returns { run_refs: [...] }. Poll each ref:
curl https://assertive.tech/api/public/runs/RUN-000042/status
# → { "run_ref": "RUN-000042", "status": "running", ... }Terminal statuses: passed, failed, cancelled.
For staging URLs behind a VPN, or when credentials must never leave your network, run the open-source Assertive Runner daemon. It pulls scenarios from Assertive, executes them locally with Stagehand against a real browser, and posts back only sanitized results.{{secret.KEY}} placeholders are resolved from a local env file — the cloud never sees the values.
Create a runner-scoped API key under Settings → API keys → Runner, then set mode: local:
name: Assertive QA (on-prem)
on:
pull_request:
workflow_dispatch:
jobs:
qa:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Assertive scenarios on this runner
uses: vancoregroup/assertive-action@v2
with:
mode: local
endpoint: https://assertive.tech
project_id: ${{ secrets.ASSERTIVE_PROJECT_ID }}
runner_key: ${{ secrets.ASSERTIVE_RUNNER_KEY }}
llm_key: ${{ secrets.LLM_API_KEY }}
# scenario_ids omitted = run all scenarios in the project
Or run the daemon directly on a host inside your network:
# .assertive.env — keep on the runner host, never commit
ASSERTIVE_API_KEY=ast_... # runner-scoped key (Settings → API keys → Runner)
ASSERTIVE_ENDPOINT=https://assertive.tech
ASSERTIVE_PROJECT_ID=<uuid>
LLM_API_KEY=... # your own key; prompts stay on this host
ASSERTIVE_INTERNAL_BASE=https://staging.internal.example.com # optional grounding
ASSERTIVE_CONCURRENCY=4 # parallel workers (1 = serial, max 8, clamped by project cap)
The daemon can also generate scenarios entirely on your host, for internal/staging sites the cloud cannot reach. It crawls the target with a local browser, builds a domain brief and scenario prompts using the same core as the cloud, calls your OpenAI-compatible LLM, and uploads only the synthesized scenario definitions for governance — no DOM, no credentials, no page content leaves the host.
# .assertive.env — on-prem generation
ASSERTIVE_API_KEY=ast_... # runner-scoped key
ASSERTIVE_ENDPOINT=https://assertive.tech
ASSERTIVE_PROJECT_ID=<uuid>
LLM_API_KEY=... # your own OpenAI-compatible key
LLM_BASE_URL=http://localhost:11434/v1 # optional: local Ollama / vLLM
LLM_MODEL=gpt-4o-mini # optional: model id
ASSERTIVE_GENERATE_URL=https://staging.internal.example.com # target to scan
ASSERTIVE_GENERATE_COUNT=6 # scenarios to synthesize
ASSERTIVE_GENERATE_RISK=balanced # balanced | high | broad
Then run assertive-runner generate. The generated scenarios are synced to the project and appear in Test Staging, pinned to the current Assertive version so future regenerations skip them unless you explicitly unpin.
While it is running, the daemon sends a heartbeat every 30 seconds so you can see which on-prem runners are online and what they are executing, live under Settings → Security → On-prem runners. Only operational metadata leaves your host: instance id, hostname, version, platform, status, and the run reference the cloud already issued — never page content, credentials or secrets.
# Optional heartbeat tuning
ASSERTIVE_RUNNER_LABEL="London build box" # friendly name in the health view
ASSERTIVE_HEARTBEAT_SECONDS=30 # 5-600, default 30
ASSERTIVE_DISABLE_HEARTBEAT=1 # opt out entirely (no liveness view)
A runner shows as online while beats arrive, stale after a couple of missed beats, and offline beyond that. Shutting the daemon down sends a final beat so it is marked offline immediately.
Every Runner release is published with an Ed25519-signed manifest at /api/public/runner/releases. The signing public key is compiled into the daemon, and each artifact carries a SHA-256 recorded inside the signed payload — so a tampered download fails even if the CDN or transport is compromised. Downgrades are refused and the binary swap is atomic, with automatic rollback if it is interrupted.
# Check the signed release channel (verifies signature + checksum)
assertive-runner update
# Install the verified update
assertive-runner update --apply
# Opt in to automatic updates on startup
ASSERTIVE_AUTO_UPDATE=1 assertive-runner run
# Air-gapped hosts: never phone home
ASSERTIVE_SKIP_UPDATE_CHECK=1 assertive-runner run
Auto-apply is opt-in: with ASSERTIVE_AUTO_UPDATE=1 the daemon installs verified updates on startup; otherwise it only tells you one exists. Set ASSERTIVE_SKIP_UPDATE_CHECK=1 for fully air-gapped hosts. Nothing self-modifies inside your perimeter unless you ask for it.
The daemon is a test executor, not a collector. Every outbound payload passes through the sanitizer, which redacts credential-shaped values, tokens and common PII patterns before anything is posted. This is the exact list.
Outbound endpoints are limited to /api/runner/scenarios, /api/runner/results, /api/runner/heartbeat, /api/runner/grounding (only when you set an internal base), /api/runner/scenarios/sync (only for on-prem generation) and the signed release manifest. Your own LLM endpoint is the only other destination, and you choose it. Every call is recorded in Runner activity so you can audit exactly what was delivered.
With parallel execution each worker runs in its own browser instance, so sessions, cookies and resolved secrets are never shared between concurrent scenarios.
Need a public share link for the run?
Toggle any run public and share /r/RUN-XXXXXX. See Pricing for parallel execution and SSO.