SEO checks in your deploy pipeline
Docket's CLI exits 2 when it finds a critical issue, so
docket audit https://staging.example.com is a complete deploy gate in one line and
nothing else has to be installed. Auditing 25 pages took a median of
24.9 seconds across 6 real sites
(17.0s to 36.1s, measured 2026-08-07), which is
short enough to sit on every pull request without anyone noticing the build got slower.
Read this first: your runners are probably Linux
Docket is Apple Silicon only. It runs on GitHub Actions'
macos-latest,
which is arm64, and it does not run on ubuntu-latest at all. If your pipeline is
Linux and you are not willing to add a macOS job to it, stop reading and use
Screaming Frog's
command line interface instead: it is available for Windows, Mac and Ubuntu Linux, it runs
headless, and for a Linux-only pipeline it is simply the right tool. That is a real advantage
and there is no version of this page where it is not.
Everything below assumes you are willing to run one macOS job. It costs more than a Linux one — see the arithmetic further down — and it is the whole of the catch.
Where the binary actually is
The CLI ships inside the app bundle, and until now this site told you it existed without telling you where. It is here:
/Applications/Docket.app/Contents/Resources/docket/docket
Put that directory on your PATH rather than symlinking the binary somewhere
else. The WebKit rendering helper lives beside it as a sibling, and keeping the directory
intact is what lets --render find it.
A workflow that works
name: SEO gate
on: [pull_request]
jobs:
seo:
runs-on: macos-latest # arm64. ubuntu-latest will not work.
steps:
- name: Install Docket
run: |
curl -sL -o docket.dmg https://github.com/mattkerr09/docket-site/releases/download/v0.1.0/Docket-0.1.0-arm64.dmg
hdiutil attach -nobrowse -quiet docket.dmg -mountpoint /Volumes/Docket
cp -R /Volumes/Docket/Docket.app /Applications/
echo "/Applications/Docket.app/Contents/Resources/docket" >> "$GITHUB_PATH"
- name: Audit staging
run: docket audit https://staging.example.com -n 100 --fail-on critical
No runtime to install, no pip install, no lockfile to resolve — the download is
17 MB and it is notarised by Apple, so nothing has to be talked past Gatekeeper. Process
overhead was 0.2s of the wall-clock in every run measured; effectively all
of the time is the crawl itself, at about 1.0 seconds per page.
The exit codes are a contract
A pipeline depends on these not moving, so they are part of the public interface more than anything printed is:
| Code | Meaning |
|---|---|
0 | The audit ran and found nothing at or above the threshold |
1 | Docket could not run. A defect in the tool, not in your site |
2 | The audit ran and the result is bad |
1 and 2 are deliberately distinct, because "your site is broken" and
"the tool is broken" demand opposite responses from whoever reads the log at six on a Friday,
and a gate that conflates them gets wrapped in || true within a month.
A staging URL that does not answer is 2, not 1: Docket ran
correctly, the site was not there, and that should stop a deploy. This distinction has teeth —
pointing Docket at a hostname that does not resolve used to report three critical issues, of
which two were invented. A DNS blip would have failed a build with a confident story about a
robots.txt file that did not exist. If you are wiring any audit tool into CI, run it
once against a hostname that does not resolve and read what it says. You will learn more in
that run than in ten against a healthy site.
Gate on what this deploy broke, not on what was already broken
--fail-on defaults to critical, deliberately: almost every real site
carries HIGH findings, and a default that fails on ordinary work gets disabled. But an absolute
threshold is the wrong question for a pipeline, and the reason is arithmetic. Every real site
has standing findings, so a bar tight enough to catch a regression fails every build, and one
loose enough to pass catches nothing. The deploy is only answerable for what it changed.
docket diff https://example.com https://staging.example.com --fail-on medium
That audits both on identical settings and fails only on findings that are new or worse than production. A check that was MEDIUM before and is HIGH now never appeared or disappeared — it got worse, which is exactly what the gate is for, and a naive new-versus-old set comparison misses it entirely. Improvements never fail a build, however many there are.
If the two crawls reach very different numbers of pages, Docket refuses to compare them and
exits 1 rather than 0. A build that goes green because the comparison
was impossible is worse than one that fails, because the team believes the gate ran.
Findings on the Security tab instead of in the log
An exit code tells a build to stop and nothing else, so whoever sees red opens the log and
reads text. -f sarif writes SARIF 2.1.0, the format GitHub, GitLab and Azure all
ingest, and the findings become listed alerts with their severity, their description and the
fix attached:
- name: Audit staging
run: docket audit https://staging.example.com -n 100 -f sarif > docket.sarif
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: docket.sarif
Be clear about what that gets you. SARIF was designed for static analysis, where a result points at a file and a line. Docket's findings are about URLs, and there is no general way to know which source file produced a given URL — a template, a CMS record and a static file all look identical from outside. So the alerts land on the Security tab, correctly titled and linked to the affected pages, and they do not annotate the lines of a pull request the way a linter's SARIF does. If inline diff annotations are what you are after, this will disappoint you.
Two details worth knowing. CRITICAL and HIGH both map to SARIF's error, because
SARIF has no rank above it — each finding keeps its real severity in properties.
And a finding's location is always a page of your own site: a few checks list a third-party URL
as where you go to fix something, and pointing an alert at Google's settings page would be
nonsense, so those travel as the rule's helpUri instead.
Findings in the test-report panel
-f junit writes JUnit XML, which is the report widget every CI system already
has — GitLab, Jenkins, Azure, Buildkite, CircleCI. The shape is one testcase per check, so the
panel reads like a test run — 93 tests, four failed — rather than handing
you a wall of text.
Two decisions in that shape matter more than the format. A check that ran and found nothing is a pass; a check that could not run is skipped, never passed. And if the crawl reached no pages at all, every check is skipped and none is green — a passing test report gets read as a guarantee, and an audit that read nothing has not earned one. Failures use the same threshold as the exit code, so a build cannot go green while showing red tests.
A GitHub Action, so you do not maintain the shell
The four lines above work and will keep working. If you would rather not own them:
- uses: mattkerr09/docket-site@v0.1.0
with:
url: https://staging.example.com
fail-on: critical
format: junit # or sarif, or text
It checks the runner first and fails with a sentence you can act on if it is not Apple
Silicon macOS, rather than letting you find out inside an hdiutil error. Full
inputs and exit codes are in
ACTION.md. It is
not published to the GitHub Marketplace — reference it by repository as shown.
What it costs to run
macOS minutes are the expensive ones. GitHub publishes $0.062 per minute for a standard macOS runner against $0.006 for Linux — about 10 times — and billed minutes round up. The measured audit is well under a minute, so a run bills as one: roughly 6 cents per gate, or $12.40 for two hundred pull requests in a month.
Worth saying plainly: that is a recurring cost on a tool sold as a one-time $149, and it is GitHub's, not ours. If it bothers you, gate on merges to main rather than every push, or run the job on a Mac you already own — Docket has no licence server and no seat count, so a self-hosted runner is free.
Three things not to do
Do not gate on the score. It is a weighted composite and it moves when the weighting changes. Gate on severities, which are defined per check and do not drift.
Do not run it against production on every push. Point it at staging. Docket backs off on 429 and 503 rather than hammering, but a crawl on every commit is still traffic your own analytics has to explain.
Do not turn on --render and leave it. Rendering runs each page
through WebKit and it is much slower than the numbers above, which were measured without it.
Turn it on for the pages that need it, or on a nightly job rather than a per-PR one.
Where this is thin
SARIF gets findings onto the Security tab but not onto the pull request diff, for the reason above — that one is structural rather than unfinished. `docket diff` is not wired into the action yet, so gating on what a deploy broke means running it as a plain step. Nothing is cached between runs, so every job re-downloads 17 MB. And the timings above are one machine on home broadband on a single day, across 6 sites — one of them swung fifteen seconds between two consecutive runs. Treat them as an order of magnitude, not a benchmark, and measure your own.
Common questions
Can I run an SEO audit in CI?
Yes, and it is the strongest use of one, because it catches a noindex before it ships rather than weeks later on a traffic graph. Docket's CLI exits 0 when clean, 2 when it finds something at or above your threshold, and 1 only when the tool itself could not run.
Does Docket run on GitHub Actions?
On macos-latest, which is arm64. Docket is Apple Silicon only, so it will not run on ubuntu-latest. If your pipeline is Linux-only, Screaming Frog's CLI runs on Windows, Mac and Ubuntu Linux and is the better fit.
How long does a Docket audit take in a pipeline?
A median of 24.9 seconds for 25 pages across 6 real sites measured on 2026-08-07, or roughly 1.0 seconds per page. Process startup was 0.2s, so almost all of it is the crawl. Rendering with --render is considerably slower and was not included.
Can Docket output SARIF for GitHub code scanning?
Yes — docket audit -f sarif writes SARIF 2.1.0, which you can hand to github/codeql-action/upload-sarif. The findings appear as alerts on the Security tab with their severity and fix. They do not annotate the pull request diff: SARIF locations are files and lines, and an SEO finding is about a URL, which cannot generally be mapped back to a source file.
Does Docket have a GitHub Action?
Yes — reference mattkerr09/docket-site in a `uses:` step. It installs Docket, runs the audit and exits with the same codes as the CLI, and it checks the runner first so a Linux job fails with a sentence you can act on rather than a confusing disk-image error. It is not on the GitHub Marketplace; reference it by repository.
Can Docket write JUnit output for my CI test panel?
Yes, with -f junit. One testcase per check, so the panel reads as a test run. A check that ran and found nothing passes; one that could not run is skipped, never passed; and if the crawl reached no pages, every check is skipped and none is green — a passing test report is read as a guarantee.
Should the build fail on the SEO score?
No. The score is a weighted composite and moves when the weighting changes. Gate on severities with --fail-on, or better, use docket diff to fail only on findings this deploy introduced or made worse — every real site carries standing findings, so an absolute threshold either fails every build or none of them.