Quality gates in CI
Use the sitecmd CLI to fail a build when site health regresses. Examples for GitHub Actions, GitLab CI, CircleCI, and more.
A quality gate is a check that fails your build if your site is in worse shape than you’ll accept. SiteCMD’s CLI gives you three exit-code-driven gates: a free local Web Scan score gate, a free local Code Scan severity gate, and a connected Code Scan gate that compares a checkout with the site’s shared baseline.
The local sitecmd scan gate runs live-site checks. Local sitecmd audit runs Code Scan against the checkout. sitecmd gate runs the same source audit and asks the connected service which findings are new against its shared baseline. Core Web Vitals and the axe-core accessibility deep scan remain desktop-only because the release CLI does not bundle a browser engine.
This page is the recipe collection. For the underlying CLI behavior, see CLI reference.
The basic gate
sitecmd init https://staging.example.com --yes
sitecmd scan --fail-under 85
That’s the entire pattern. init writes a .sitecmd/config.json if one isn’t there. scan runs the checks. If the score is below 85, the command exits with code 1 and the CI job fails. If the score is 85 or above, it exits 0 and the job passes.
Most teams start here and iterate.
Free source-code gate
Audit the checkout directly and fail when Code Scan finds the selected severity or higher:
sitecmd audit . --format github --fail-on high
This requires no account, license, baseline, or secret. Exit code 0 passes, 1
means a finding met the severity floor, and 2 means the audit itself could not
run. Other output formats are summary, json, markdown, and review.
Connected shared-baseline gate
For a site using the connected service, sitecmd gate audits the current checkout with Code Scan and asks the service which findings are new against the shared baseline:
export SITECMD_CI_TOKEN='<the site-scoped CI token>'
export SITECMD_CONNECTION_PASSPHRASE='<the export passphrase>'
sitecmd gate --connection-export ./connection.json --threshold high
Create the encrypted connection export and CI token from Settings, then Connected, in the desktop app. The export contains no bearer credential. Store the token and passphrase in the CI system’s secret store; the encrypted export can travel with the workflow by whatever file or artifact mechanism your team uses.
The gate exits 0 when the checkout passes, 1 when new findings at or above the selected severity block the merge, and 2 when the gate could not run. Its candidate is evaluated and discarded, so a pull request gate never changes the baseline. Add --strict when detector or corpus drift should also block instead of producing an uncertain comparison.
This is distinct from sitecmd connected --submit, which binds code evidence to a deployment after it exists. See the CLI reference for connected submission, GitHub OIDC attestation, and every flag.
Picking a threshold
Some defaults that work for most projects:
- Marketing site, low-stakes:
--fail-under 75. Catches bad regressions, doesn’t block on polish. - Production product site:
--fail-under 85. Reasonable bar for a launched site you care about. - High-stakes (financial, health, anything regulated):
--fail-under 90and add--type security --fail-under 95as a second gate.
You can also pin a category-specific gate. sitecmd scan --type security --fail-under 95 only checks security category, and only fails if security drops below 95.
Multi-gate strategy
Run the same scan multiple times with different filters when you want different bars for different categories:
sitecmd scan --type security --fail-under 95
sitecmd scan --type accessibility --fail-under 80
sitecmd scan --fail-under 75
Each line is its own gate. Any one failing fails the build.
GitHub Actions
name: Site health
on:
push:
branches: [main]
pull_request:
jobs:
sitecmd:
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Install SiteCMD CLI
uses: brambleworks/SiteCMD/.github/actions/setup-sitecmd@<full-release-commit-sha>
with:
version: "<version>"
- name: Audit source
run: sitecmd audit . --format github --fail-on high
- name: Scan staging
run: |
sitecmd init https://staging.example.com --yes
sitecmd scan --fail-under 85
Replace the two placeholders with the version and full commit behind the signed
SiteCMD release tag. Do not use main: the setup action verifies that exact
release archive with SiteCMD’s updater signing key before it runs the binary.
The CLI’s exit code is what GitHub Actions reads. No extra adapter is needed.
GitLab CI
sitecmd:
image: ubuntu:24.04
variables:
SITECMD_VERSION: "<version>"
SITECMD_SOURCE_COMMIT: "<full-release-commit-sha>"
before_script:
- apt-get update && apt-get install -y curl ca-certificates minisign
- mkdir -p .sitecmd-setup "$CI_PROJECT_DIR/.sitecmd-bin"
- curl -fsSLo .sitecmd-setup/install.sh "https://raw.githubusercontent.com/brambleworks/SiteCMD/$SITECMD_SOURCE_COMMIT/.github/actions/setup-sitecmd/install.sh"
- curl -fsSLo .sitecmd-setup/updater-public-key.pub "https://raw.githubusercontent.com/brambleworks/SiteCMD/$SITECMD_SOURCE_COMMIT/.github/actions/setup-sitecmd/updater-public-key.pub"
- chmod +x .sitecmd-setup/install.sh
- SITECMD_INSTALL_DIR="$CI_PROJECT_DIR/.sitecmd-bin" .sitecmd-setup/install.sh
- export PATH="$CI_PROJECT_DIR/.sitecmd-bin:$PATH"
script:
- sitecmd audit . --format github --fail-on high
- sitecmd init https://staging.example.com --yes
- sitecmd scan --fail-under 85
Use the version and full source commit from the same release. The installer and trust root are fetched from that immutable commit, then verify the release archive before installing it.
CircleCI
jobs:
sitecmd:
docker:
- image: cimg/base:current
steps:
- checkout
- run:
name: Scan
command: |
export SITECMD_VERSION="<version>"
export SITECMD_SOURCE_COMMIT="<full-release-commit-sha>"
sudo apt-get update
sudo apt-get install -y minisign
mkdir -p .sitecmd-setup "$HOME/.local/bin"
curl -fsSLo .sitecmd-setup/install.sh "https://raw.githubusercontent.com/brambleworks/SiteCMD/$SITECMD_SOURCE_COMMIT/.github/actions/setup-sitecmd/install.sh"
curl -fsSLo .sitecmd-setup/updater-public-key.pub "https://raw.githubusercontent.com/brambleworks/SiteCMD/$SITECMD_SOURCE_COMMIT/.github/actions/setup-sitecmd/updater-public-key.pub"
chmod +x .sitecmd-setup/install.sh
SITECMD_INSTALL_DIR="$HOME/.local/bin" .sitecmd-setup/install.sh
export PATH="$HOME/.local/bin:$PATH"
sitecmd audit . --format github --fail-on high
sitecmd init https://staging.example.com --yes
sitecmd scan --fail-under 85
Pre-deploy vs. post-deploy
Two common shapes:
- Pre-deploy gate: scan staging before promoting to production. Fails the deploy if staging regressed.
- Post-deploy verification: scan production right after deploy. Surfaces regressions immediately, even if they only manifest with real production config (CDN headers, env vars, third-party scripts).
You typically want both, run independently.
deploy:
needs: [pre-deploy-scan]
# ...
pre-deploy-scan:
run: sitecmd scan --url https://staging.example.com --fail-under 85
post-deploy-scan:
needs: [deploy]
run: sitecmd scan --url https://example.com --fail-under 85
Comparing scans (regression-only gates)
If you want to fail on new regressions rather than absolute thresholds, use --diff:
sitecmd scan --diff
This compares against .sitecmd/last-scan.json from the previous run and exits 1 when a new critical issue appears, even with no --fail-under set (add --fail-under if you also want an absolute floor). Useful when you’re improving a site over time and don’t want to block on the legacy baseline.
You’ll need to persist .sitecmd/last-scan.json between CI runs for --diff to work. Most CI tools support cache or artifact-passing for this.
Performance
The CLI’s live-site checks measure performance without a browser: response time and TTFB, compression, caching headers, render-blocking resources, and image hygiene. They run fast enough for every PR scan.
Core Web Vitals and the axe-core accessibility deep scan need a real browser, which the standalone CLI binary ships without, so they don’t run in CI. Run them from the desktop app when you want those metrics. See CLI reference for what the CLI does and doesn’t cover.
Output for humans
By default, sitecmd scan prints a readable summary to stdout. CI logs are perfectly fine for the day-to-day. If you want structured output for a script or a custom report:
sitecmd scan --json > results.json
Then parse with jq or pipe into whatever you want.
Output for pull request comments
The fix prompts surface is designed for this. A scan must have run earlier in the job because fix reads the exported .sitecmd/ results. No license environment variables are required:
sitecmd fix --all > fixes.md
In GitHub Actions:
- name: Generate fix prompts
if: github.event_name == 'pull_request'
run: sitecmd fix --all > fixes.md
- name: Comment fixes on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require("fs");
const body = fs.readFileSync("fixes.md", "utf8");
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body
});
Fix prompts are part of the complete free local workbench. The command reads only the scan artifact in the CI checkout and writes to standard output.
When the gate fails
A failed gate doesn’t tell you what to fix. The CI log shows the headline (“Score 72, below threshold of 85”) but the details live in the scan JSON. Common pattern:
- CI fails the build with the score.
- Developer opens the SiteCMD desktop app (if they have it), or downloads
results.jsonfrom the CI artifacts. - Triage from there.
You can also wire the post-failure step to upload the scan JSON as an artifact:
- name: Upload scan results
if: failure()
uses: actions/upload-artifact@v4
with:
name: sitecmd-scan
path: .sitecmd/last-scan.json
What CI gates can’t do
- A local Web Scan cannot verify an unshipped fix. It runs against a URL, so if a pull request has not reached staging yet, the command is scanning the previous version of the site. The connected checkout gate can evaluate source findings before deployment, but not live behavior that does not exist at a URL yet.
- Local gates do not have shared integration history. A fresh CI checkout has no local analytics, deploy, or uptime history to correlate. Connected CI adds the hosted baseline through a separate site-scoped credential; it does not copy all desktop integration data into the runner.
- They can’t replace a real review. A passing gate means the site cleared the threshold, not that it’s perfect. Use gates as a floor, not a ceiling.