---
title: Use in CI
description: Run dualmark verify in your pipeline to catch AEO regressions before they ship.
---

`dualmark verify` exits non-zero when a required check fails, making it a natural fit for any CI
pipeline. Add it to your pull-request workflow and block merges the moment conformance drops.

## GitHub Action (recommended)

The fastest way to add AEO conformance to your pipeline -- no Bun setup, no manual parsing.

```yaml
- uses: dodopayments/dualmark-verify-action@v1.0.0
  with:
    url: ${{ vars.STAGING_URL }}
    level: standard
```

### Features

| Feature | Description |
|---|---|
| **Zero-config** | Action installs Bun, runs the CLI, parses results -- all in one step |
| **Level gating** | Set `level: basic`, `standard`, or `advanced` -- fails if below threshold |
| **Regression detection** | Set `fail-on-regression: true` -- compares against the previous run's artifact |
| **PR comments** | Set `comment-on-pr: true` -- posts a sticky score table that updates on re-push |
| **Artifact upload** | `aeo.json` saved as `aeo-report` (configurable via `artifact-name`) -- download in later steps or other jobs |

### Full example

```yaml
name: AEO Conformance

on:
  pull_request:
  push:
    branches: [main]

jobs:
  aeo:
    runs-on: ubuntu-latest
    permissions:
      contents: read # checkout
      pull-requests: write # comment-on-pr
      actions: read # fail-on-regression reads the previous run's artifact
    steps:
      - uses: actions/checkout@v4
      - uses: dodopayments/dualmark-verify-action@v1.0.0
        with:
          url: https://staging.example.com/blog/your-post
          level: standard
          fail-on-regression: true
          comment-on-pr: true
```

<Callout type="warn">
  Each input maps to a permission scope: `comment-on-pr` needs `pull-requests: write`, and
  `fail-on-regression` needs `actions: read` (to read the previous run's artifact). Omitting a scope
  surfaces as a confusing API permission error at runtime.
</Callout>

<Callout type="info">
  The action automatically uploads `aeo.json` as a build artifact (default: `aeo-report`).
  If you are verifying multiple URLs in the same workflow, pass a unique `artifact-name` to each step to prevent collisions.
  Use `actions/download-artifact@v4` in downstream jobs.
</Callout>

### Advanced: Verifying multiple URLs

If you verify multiple URLs, give each step a unique `artifact-name`. Otherwise, the second run overwrites the first one's artifact. The default name is `aeo-report`.

```yaml
jobs:
  aeo-multi:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: dodopayments/dualmark-verify-action@v1.0.0
        with:
          url: https://staging.example.com/pricing
          artifact-name: aeo-report-pricing

      - uses: dodopayments/dualmark-verify-action@v1.0.0
        with:
          url: https://staging.example.com/docs
          artifact-name: aeo-report-docs
```

See the [Action README](https://github.com/dodopayments/dualmark-verify-action) for full input reference and configuration options.

## GitHub Actions (CLI)

If you prefer to run the CLI directly instead of using the action, add it to your workflow manually:

<Steps>

<Step>

## Add the workflow file

```yaml title=".github/workflows/aeo.yml"
name: AEO Conformance

on:
  pull_request:
  push:
    branches: [main]

jobs:
  aeo:
    runs-on: ubuntu-latest
    steps:
      - name: Setup Bun
        uses: oven-sh/setup-bun@v2
        with:
          bun-version: latest
      - name: Verify AEO conformance
        run: bunx @dualmark/cli verify ${{ vars.STAGING_URL }} --timeout 15000
```

Replace `vars.STAGING_URL` with the URL of your staging deployment, or hard-code a page URL
directly (e.g. `https://staging.example.com/blog/your-post`).

</Step>

<Step>

## Parse results in downstream steps

Use `--json` to get a machine-readable report you can forward to a dashboard or annotation step:

```yaml title=".github/workflows/aeo.yml"
- name: Verify AEO conformance
  run: bunx @dualmark/cli verify https://staging.example.com/blog/your-post --json > aeo.json

- name: Upload report
  uses: actions/upload-artifact@v4
  with:
    name: aeo-report
    path: aeo.json
```

<Callout type="info">
  Exit codes are the same with `--json` -- non-zero on required-check failure. The JSON report can be
  consumed by dashboards, Slack bots, or custom annotation scripts.
</Callout>

</Step>

</Steps>

## GitLab CI

```yaml title=".gitlab-ci.yml"
aeo-conformance:
  image: oven/bun:latest
  script:
    - bunx @dualmark/cli verify https://staging.example.com/blog/your-post --timeout 15000
  only:
    - merge_requests
    - main
```

For JSON output in GitLab, pipe to a file and expose it as an artifact:

```yaml title=".gitlab-ci.yml"
aeo-conformance:
  image: oven/bun:latest
  script:
    - bunx @dualmark/cli verify https://staging.example.com/blog/your-post --json > aeo.json
  artifacts:
    paths:
      - aeo.json
    when: always
```

## Docker / self-hosted runners

If your runner has Docker available but not Bun, use the official Bun image directly:

```bash
docker run --rm oven/bun:latest \
  bunx @dualmark/cli verify https://example.com/blog/your-post
```

Or in a `docker-compose`-based pipeline:

```yaml
services:
  aeo:
    image: oven/bun:latest
    command: bunx @dualmark/cli verify https://example.com/blog/your-post
```

## Next steps

<Cards>
  <Card title="The verify CLI" href="/docs/conformance/cli">
    Full flag reference, exit codes, and programmatic API.
  </Card>
  <Card title="Scoring & levels" href="/docs/conformance/scoring">
    What each check is worth. Basic / Standard / Advanced thresholds explained.
  </Card>
</Cards>
