CI
Run Dino in CI: the GitHub Action, pinning, credentials, failing a build, partial coverage, reading the result, breaking changes, GitHub code scanning, and the same pattern for any CI system.
Run Dino after the API you're verifying is deployed: it tests the running API, not your source code. It returns an exit code your pipeline branches on and a DinoResult document you can keep.
GitHub Actions
name: API verification
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
jobs:
dino:
runs-on: ubuntu-latest
steps:
- uses: Dino-HQ/dino/.github/actions/scan@v1
with:
api-url: ${{ secrets.API_URL }}
api-token: ${{ secrets.API_TOKEN }} # optional
cli-version: 1.4.0
fail-on-high: true- Pin the CLI.
@v1takes backward-compatible fixes to the action and never a breaking change (that would bev2).cli-versionpins the CLI, so CLI upgrades are deliberate. Don't use@main, which moves with every change. For a hardened workflow, pin the action to a full commit SHA instead of@v1. - Credentials.
api-tokenis the action's input for a bearer token. Pass it from a repository secret; the action sends it asAuthorization: Bearer …and never writes it to disk. - The result. The report is uploaded as the
dino-scan-reportartifact, and the step'sexit-codeoutput carries Dino's exit code.
| Input | Default | What it does |
|---|---|---|
api-url | required | The endpoint to verify |
protocol | graphql | graphql or rest |
spec-url | OpenAPI URL or path, required for rest | |
api-token | Bearer token for the target API | |
fail-on-high | false | Fail (exit 3) on any HIGH or CRITICAL finding |
accept-partial | false | Treat partial coverage as success instead of exit 6 |
fail-on-breaking | false | Also fail (exit 3) on breaking schema changes; see Breaking changes |
format | markdown | Report format: markdown, json, or sarif for GitHub code scanning |
upload-sarif | true | With sarif: upload to code scanning (set false to upload it yourself) |
github-token | github.token | With sarif: reads the previous code scanning analysis and its state |
cli-version | latest | CLI version to install |
Failing a build
The exit code is the signal. Nothing needs to parse the report.
| Exit | Meaning in CI |
|---|---|
0 | Clean, or findings below your gate: pass |
3 | A gate failed (--fail-on-high, --fail-on-breaking): do not ship |
6 | Partial coverage: some checks couldn't complete, so this is not a pass |
4 | Transient (network, rate limit): retry |
2, 5 | Usage or config error: fix the input |
70 | Dino crashed: report it |
Partial coverage means Dino couldn't verify everything it planned, for example because introspection was limited or a check couldn't run. The verdict is honest about it: verdict.coverage is "partial" and verdict.reasons says why. Decide explicitly whether that's acceptable. accept-partial (or --accept-partial) turns 6 into 0; it never hides a failed gate. The full table and precedence: Machine contracts.
Breaking changes
fail-on-breaking runs dino diff, which compares the API with the snapshot saved by the previous run. On a fresh CI runner there is no previous run, so the first build only saves a baseline. Keep the .dino/snapshots folder between runs with a cache:
- uses: actions/cache@v4
with:
path: .dino/snapshots
key: dino-snapshots-${{ github.ref_name }}-${{ github.run_id }}
restore-keys: dino-snapshots-${{ github.ref_name }}-
- uses: Dino-HQ/dino/.github/actions/scan@v1
with:
api-url: ${{ secrets.API_URL }}
cli-version: 1.4.0
fail-on-breaking: trueGitHub code scanning
With format: sarif, Dino's findings become alerts in the repository's Security tab, next to your other code scanning tools. Security findings (access control, data leaks, CORS, missing rate limits and similar) are security alerts with a severity; contract and deprecation findings are quality alerts. SARIF needs CLI 1.3.0 or later.
name: API verification (code scanning)
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
security-events: write # upload SARIF, read the previous Dino analysis
actions: read # read the previous run's Dino state (needed on private repositories)
concurrency:
group: dino-sarif-${{ github.ref }}
cancel-in-progress: false
jobs:
dino:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # a committed spec or .dino.yml gives alerts a stable file
- uses: Dino-HQ/dino/.github/actions/scan@v1
with:
api-url: ${{ secrets.API_URL }}
cli-version: 1.4.0
format: sarif- Untested is never fixed. GitHub closes every alert missing from an upload. Dino reconciles each upload with the previous one: an alert closes only when Dino re-tested that exact operation with the same test plan. A partial run uploads what it verified and carries every alert it didn't test forward, unchanged.
- When Dino can't tell, it uploads nothing. If the previous analysis or its state can't be read, or another Dino upload landed during the scan, no SARIF is written, existing alerts stay as they are, and the step keeps the scan's exit code. A run that reached nothing (unreachable target, every tool failed, nothing in scope) never uploads. The
sarif-writtenoutput says which happened. - One writer per ref. Every workflow that uploads Dino SARIF must use the same
concurrencygroup,dino-sarif-${{ github.ref }}. Two uploads at once could each start from the same previous analysis and close an alert neither re-tested. - State. The action keeps the state the next run needs as a workflow artifact (
dino-sarif-state-…). Keep artifact retention longer than the gap between scans. If it expires, Dino writes no SARIF until you run one complete scan with--sarif-rebaseline, which starts over deliberately. - Where alerts point. Each alert is attached to a file git tracks: your local OpenAPI spec or SDL, else
.dino.yml, else the workflow file. A scan narrowed with--toolsor--modulesuploads under its own category, so it never closes a full scan's alerts. - Outside GitHub Actions,
dino scan --format sarifwrites SARIF only for a complete run, for other SARIF consumers. Exit codes, refusals and the identifiers are in Machine contracts.
Any other CI
The same three steps work anywhere with a shell: install a pinned version, run the scan, branch on the exit code.
bash -o pipefail -c 'curl -fsSL https://usedino.dev/install.sh | sh -s 1.4.0'
export PATH="$HOME/.local/bin:$PATH"
dino scan --endpoint "$API_URL" --fail-on-high --format json > dino-result.json
code=$? # capture it before any other command runs
echo "Dino exit code: $code"
exit "$code"- Credentials. Keep secrets in your CI's secret store and pass them at run time:
--token "$API_TOKEN"or--header "X-Api-Key: $API_KEY"on the command line, or commit a.dino.ymlwhoseauthnames the environment variables to read (valueEnv,clientIdEnv,clientSecretEnv). Dino never writes a secret to the file. See Configuration. - The result.
dino-result.jsonis theDinoResultdocument: keep it as a build artifact, or read the verdict withjq '.verdict'. Errors (exit2,4,5,70) put a JSON envelope on the last line of stderr instead. See Machine contracts. - No install. With Node.js 22+ on the runner you can skip installing:
npx -y @dino-hq/cli@1.4.0 scan …. - Breaking changes. Run
dino diff --endpoint "$API_URL" --fail-on-breakingbeforedino scanand keep.dino/snapshotsbetween runs with your CI's cache.diffcompares with the latest saved snapshot andscansaves a new one, so adiffafter thescanwould compare the API with itself. The GitHub Action already runs them in this order.