Skip to Content
webagent docs
Guides

Run QA in CI in 5 minutes

Run your .webagent/tests.yaml on every pull request with the Lookover QA GitHub Action. The job passes or fails on the result and one comment on the pull request shows what broke.

Open as .md

The Lookover QA GitHub Action runs the test cases in your .webagent/tests.yaml in a cloud browser against a URL, usually a preview deploy. The job passes or fails on the result. One comment on the pull request shows what broke and is updated on every push. The job summary has the full report.

Beta

The Action is bundled JavaScript that runs on the node24 runtime, with no npm install. It is not published yet: the uses: line below works once the plugins repository is public and tagged v1. Until then, the Smoke tests in CI page shows the same idea with plain API calls, and those work today.

1. Set up

  1. Create an API key on the API keys page of the dashboard. The member role is enough. Choose the CI scope: a CI key can run tests and read results, and nothing else, so a leaked key cannot touch your team, billing or settings. Add an expiry if you want the key to stop on its own. See API keys. Save it as the repository secret WEBAGENT_API_KEY (Settings, Secrets and variables, Actions). The key never goes in a file.
  2. Commit a .webagent/tests.yaml. In Claude Code, /webagent:qa-init writes one from your code. Put a key: in it, for example key: checkout-flow. The key names the workflow in the dashboard and the pull request comment. See the tests file.
  3. Add the workflow file below.

2. The workflow

.github/workflows/lookover-qa.yml
name: Lookover QA
on:
  pull_request:
  push:
    branches: [main]

concurrency:                       # a new push cancels the old run (the action cancels the cloud run too)
  group: lookover-qa-${{ github.ref }}
  cancel-in-progress: true

permissions:
  contents: read
  pull-requests: write             # for the PR comment (without it the check still works, just no comment)

jobs:
  qa:
    name: Lookover QA              # the status check you can mark "required" later
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: lookover-ai/webagent-plugins/github-action@v1
        with:
          api-key: ${{ secrets.WEBAGENT_API_KEY }}
          url: https://staging.example.com   # the site to test

The URL must be public https. The cloud browser never opens localhost or a private address. See Test a local app.

sync turns on by itself for a push to the default branch: the file is saved as the workflow (by key), so the dashboard, its history and its schedule use the main definition. A pull request run never changes the saved workflow. It runs the file from the pull request ad hoc and groups under the same workflow in the dashboard. An edited file in a pull request cannot redefine what main tests. Set sync: true or sync: false to override.

A workflow synced from a repository shows as "Managed by owner/repo" in the dashboard. Edit .webagent/tests.yaml in the repository, or duplicate the workflow to edit it in the dashboard.

The tests file

.webagent/tests.yaml lists the cases to run. Each case has a title, plain-language steps and what must be true afterwards.

.webagent/tests.yaml
version: 1
name: Acme checkout
key: checkout-flow
url: https://staging.example.com
retries: 1
cases:
  - id: login-ok
    title: A user can sign in
    path: /login
    steps:
      - type "qa@example.com" into Email
      - type "<TEST_PASSWORD>" into Password
      - click "Sign in"
    checks: { text: Welcome, url: /dashboard }
    tags: [smoke, auth]
    priority: 1
  - id: login-wrong-password
    title: A wrong password is refused
    path: /login
    steps:
      - type "qa@example.com" into Email
      - type "nope" into Password
      - click "Sign in"
    expect: shows "Wrong email or password."
    required: false
FieldWhat it does
keySlug (lower-case letters, digits, - and _). Names the workflow and the pull request comment.
url or targetsThe site to test. Use url for one site, or targets with names and defaultTarget for several. Never both. The Action's url input wins over both.
engineOptional. An engine's short name or id. Without it the saved workflow's engine runs, then your project's default engine. A name that matches no engine in your project stops the job before any run starts (exit code 3).
retriesOptional, 0 to 2 extra tries for a failed case.
casesRequired, 1 to 100 cases.
cases[].stepsWhat a user does, one action per item. No selectors, no secrets. Without steps the case only loads the page.
cases[].expectWhat a user sees, in words: shows "X", address contains Y, does not show "Z". Any other wording is reported as "not checked".
cases[].checksThe exact form of expect: text, url, noText. When set, it wins over expect.
cases[].requiredDefault true. With false, a failure is a warning and never fails the job.
cases[].tags, priorityLabels for choosing a subset (tags input) and for order. Priority runs from 1 (most important) to 5.

Rules that save you a failed run:

  • Quote every value a step types: type "qa@example.com" into Email. A step that describes the value, such as type a valid email into Email, is typed as written.
  • Quote a whole YAML value that contains a colon and a space: expect: 'shows "Total: $19.00"'. Unquoted, the file does not load.
  • Never put a password, API key or token in the file. The text of steps is stored with the run, so use a throwaway test account.
  • A case marked signedIn: true is for the plugin only. A workflow run in CI skips it and says so in a warning.

3. Preview URLs

The url input wins over the target in tests.yaml. The Action waits up to 120 seconds for the URL to answer with 2xx or 3xx before it starts (wait-for-url), so a preview that is still building does not become a failed test.

A host that sends a deployment_status event, such as Vercel:

.github/workflows/lookover-qa.yml
name: Lookover QA
on: deployment_status

permissions:
  contents: read
  pull-requests: write

jobs:
  qa:
    name: Lookover QA
    if: github.event.deployment_status.state == 'success'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: lookover-ai/webagent-plugins/github-action@v1
        with:
          api-key: ${{ secrets.WEBAGENT_API_KEY }}
          url: ${{ github.event.deployment_status.environment_url || github.event.deployment_status.target_url }}

The comment goes to the pull request that owns the deployed commit. If the preview is behind a login page such as Vercel Deployment Protection, the cloud browser sees the login page, not your app. Use a staging URL, or turn protection off for the previews you test.

A host with a fixed preview address, such as Netlify: pull_request is enough.

Inside the job
      - uses: lookover-ai/webagent-plugins/github-action@v1
        with:
          api-key: ${{ secrets.WEBAGENT_API_KEY }}
          url: https://deploy-preview-${{ github.event.number }}--YOUR-SITE.netlify.app
          wait-for-url: 180   # the most the action waits

If the build takes longer than 180 seconds, start the job after the deploy.

Deploy gate: order the jobs, no special integration needed: deploy-preview, then qa (this Action), then promote with needs: [qa]. A red check stops promote.

4. What the job does

StateJobPull request comment
RunningIn progressLookover QA · RUNNING · testing <url>, replaced when the run ends
Pass (flaky passes included)Exit code 0PASS, counts and a case table. A case that passed on a retry reads PASS (flaky: passed on try 2 of 2)
A required case failedExit code 1FAIL, the failed step, expected against actual, the page, the console error, the failed request and a replay link
Could not test (the preview never answered, no capacity, timeout). Not an app failureExit code 2COULD NOT TEST, in plain words why
Setup problem (no key, key rejected, revoked or expired, bad file, bad input, engine: not found)Exit code 3NOT RUN · setup problem, and what to fix, for example "API key rejected: make a new one in API keys"
Fork pull request or Dependabot (GitHub gives no secrets)Exit code 0 with one noticeNone
No pull-requests: write permissionUnchangedOne notice in the log. The job summary still has the report
Job cancelled (a new push)The cloud run is cancelledUpdated to could not test (best effort)

Exit codes

Exit codeMeaning
0Pass. Also: could not test with on-error: neutral, any result with fail-on: never, and a fork pull request skip
1A required case failed: your app
2Could not test: not an app failure
3Setup problem: your configuration. fail-on: never does not hide these

Keep 1 and 2 apart. A 1 is a finding about your app. A 2 means the test could not finish, so nobody should hunt a bug that is not there. A busy platform at the start is retried twice before it counts as a 2.

A failed case that passes on a retry is flaky. It passes the job and is flagged. The retries input (0 to 2) sets the extra tries. Left empty, the API default applies, which is 1 for CI runs.

Make it a required check

GitHub only blocks a merge on checks you mark required (Settings, Branches). The status check is the job, so name it Lookover QA. To try it first, add fail-on: never. The job then reports but never fails because of a result. Run it that way for a week as a non-required check, then remove the line and require it. Setup problems still fail the job.

Replay links in the comment appear only when share-links allows it. Anyone with a link can watch the replay, so the default is off on public repositories and on for private ones. The dashboard link works for every member of your project.

5. Inputs and outputs

InputDefaultWhat it does
api-key(required)Your API key, from a secret. Empty on a fork pull request: the Action then skips with a notice
api-urlThe host built into the CLIAPI origin, without a path
urlThe site to test. Wins over the file's target
file.webagent/tests.yamlThe tests file. If this default is missing and workflow is set, the saved workflow of that key runs (needs url)
workflowkey in the fileWorkflow key. Also names the pull request comment
targetdefaultTarget of the fileA target name from the file
tagsAll casesComma-separated. Only cases with one of these tags run
case-idsAll casesComma-separated. Only these cases run
syncOn for a push to the default branch, else offSave the file as the workflow before the run
wait-for-url120Seconds, 1 to 180, to wait for the URL to answer. 0 or false skips the wait
timeout-minutes15Give up and cancel the run after this long
retriesThe API default (1 for CI runs)Extra tries for a failed case, 0 to 2
on-errorblockblock: could not test fails the job with exit code 2. neutral: it passes with a warning
fail-onfailfail: a failed required case fails the job. never: report only
commenttrueKeep one comment on the pull request, updated on every push
share-linksOn for private repositories, off for publicA 7-day replay link per failed case in the comment
github-token${{ github.token }}Token for the comment
OutputValue
verdictpass, fail, error (could not test), setup (not run: setup problem) or skipped (fork pull request without the secret)
run-idThe workflow run id. Empty when no run started
run-urlThe dashboard page of the run
flakyHow many cases passed only on a retry

Use an output in a later step. Give the Action step id: qa, then write if: steps.qa.outputs.verdict == 'fail'.

Fork pull requests

GitHub gives no secrets to a workflow started by a fork or by Dependabot, so there is no API key. The Action prints one notice, leaves the comment alone and exits with 0: nothing was tested and nothing failed. It runs when a maintainer pushes the branch to your repository. Do not switch to pull_request_target to get the secret. That runs untrusted code with your key.

Other CI systems

The Action carries the webagent command line tool and runs webagent test. The CLI is not published to npm yet, so the Action is the supported way today. For another CI system, the Smoke tests in CI page shows the API calls. The options of webagent test, for reference:

OptionWhat it does
--file <path>The tests file. Default .webagent/tests.yaml
--url <url>The site to test. Wins over the file's target
--target <name>A target of the file
--tags a,b, --case-ids a,bRun a subset
--workflow <key>The workflow key. With no file, runs the saved workflow of that key
--syncSave the workflow by key first, then run it. Use on pushes to the default branch only
--retries 0..2Extra tries for a failed case
--timeout-minutes 15On timeout or Ctrl-C the run is cancelled
--wait-for-url, --wait-for-url-timeout <s>Poll the URL until it answers 2xx or 3xx (up to 180 seconds, default 120) before starting
--on-error block|neutralCould not test exits with 2 (default) or 0
--junit <path>, --markdown <path>, --jsonWrite a JUnit file, a Markdown report, or print the run as JSON

It reads the API key from the environment variable WEBAGENT_API_KEY. The exit codes are the ones above.

Next steps

  • Write your first cases: QA testing and Recipes.
  • Read a failed report: Reading results.
  • Get a call when a run ends: the workflow_run.completed and workflow_run.failed webhook events.
  • Limit what a pipeline key can do, and let it expire: API keys.
  • Billing is by browser compute, and a retry runs the case again. See Billing.

On this page