Skip to main content

GitHub Actions (budget check)

Fail your CI pipeline when a Webflow site's performance regresses below its configured budget. Site Health exposes a single-purpose /api/external/budget-check endpoint that returns pass: true or pass: false plus a violation list — perfect for a CI gate.

How it works​

Site Health evaluates the latest completed scan against the site's budget and alert rules:

  • Performance budget (budgetPerformance) — minimum acceptable Performance score
  • LCP budget (budgetLcp) — maximum acceptable average LCP in milliseconds
  • CLS budget (budgetCls) — maximum acceptable average CLS ratio
  • Alert rules — custom per-metric thresholds (performance, seo, accessibility, bestPractices) with lt / gt operators

If any threshold is violated, the endpoint returns pass: false and the violating metrics. If all pass, you get pass: true.

Step 1 — Create an API key​

In Site Health, go to Settings → API Keys → Create API Key and name it GitHub Actions. Copy the raw key once — you won't see it again.

Step 2 — Save it as a GitHub secret​

  1. In your GitHub repo: Settings → Secrets and variables → Actions → New repository secret.
  2. Name: SITE_HEALTH_API_KEY.
  3. Value: the raw key from Step 1.
  4. Click Add secret.

You'll also want your site ID (UUID). Find it in Site Health — open the site's detail page and copy the ID from the URL (/sites/<siteId>).

Step 3 — Add the workflow​

Create .github/workflows/performance-check.yml:

name: Performance Budget Check
on: [push, pull_request]

jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Check Site Health budget
run: |
response=$(curl -s -w "\n%{http_code}" -X POST \
https://sitehealth.octagramlabs.com/api/external/budget-check \
-H "Authorization: Bearer ${{ secrets.SITE_HEALTH_API_KEY }}" \
-H "Content-Type: application/json" \
-d '{"siteId": "YOUR_SITE_ID"}')
body=$(echo "$response" | head -n -1)
status=$(echo "$response" | tail -n 1)
echo "HTTP $status"
echo "$body" | jq .
pass=$(echo "$body" | jq -r '.pass')
if [ "$pass" != "true" ]; then
echo "::error::Performance budget check failed"
exit 1
fi

Replace YOUR_SITE_ID with your site's UUID. Commit and push — the next push will run the job.

Response format​

A passing response:

{
"pass": true,
"site": "studio-brava",
"scores": {
"health": 88,
"performance": 92,
"seo": 96,
"accessibility": 89,
"bestPractices": 100
},
"violations": [],
"scanId": "8b3e7c12-...",
"scannedAt": "2026-04-17T09:12:44.128Z"
}

A failing response:

{
"pass": false,
"site": "studio-brava",
"scores": { "health": 71, "performance": 68, "seo": 96, "accessibility": 89, "bestPractices": 100 },
"violations": [
{ "metric": "performance", "threshold": 85, "actual": 68, "operator": "min" },
{ "metric": "lcp", "threshold": 2500, "actual": 3180, "operator": "max" }
],
"scanId": "8b3e7c12-...",
"scannedAt": "2026-04-17T09:12:44.128Z"
}

Each violation contains:

  • metric — performance, seo, accessibility, bestPractices, lcp, or cls
  • threshold — the configured budget or rule value
  • actual — the measured value from the latest scan
  • operator — min (budget requires at least), max (budget requires at most), lt (rule triggers when less than), gt (rule triggers when greater than)

Tuning the check​

This endpoint reads the latest completed scan at the moment of the request — it does not trigger a new scan. For CI that deploys to Webflow:

  • Configure Webflow's site_publish webhook so Site Health rescans after every publish.
  • In your workflow, add a small delay (or a polling loop) after deploy so the webhook-triggered scan has time to complete before the budget check runs.

Configure budgets in Site Health​

Open the site's detail page → Settings → Budgets. Set:

  • Minimum Performance score (e.g., 85)
  • Maximum LCP in ms (e.g., 2500)
  • Maximum CLS ratio (e.g., 0.1)
  • Any additional alert rules (e.g., accessibility lt 90)

The budget-check endpoint uses these exact values.

To auto-scan on publish, add the Webflow publish webhook. To debug budget decisions interactively, use an MCP client — see Claude.ai connector or Claude Desktop and ask "why did studio-brava fail its budget?"

Troubleshooting​

401 "Missing Authorization header" or "Invalid API key." Check that the secret SITE_HEALTH_API_KEY is set at the repo (not environment) level, or the job references the right environment. Also verify the header is Authorization: Bearer ${{ secrets.SITE_HEALTH_API_KEY }} — the word Bearer is required.

404 "Site not found." The site ID is wrong or belongs to a different Site Health account. Confirm in the URL bar of the site's detail page.

200 response with pass: false and an empty violations array. The site has no completed scans yet. The endpoint returns {"error": "No completed scans", "pass": false} with status 200 so CI fails safely. Trigger a scan from Site Health (or wait for the webhook) before running the workflow.

422 Unprocessable Entity. The body didn't match the schema. Ensure you're sending {"siteId": "..."} as valid JSON with Content-Type: application/json and that the ID is a UUID.

The check passes locally but fails in CI (or vice versa). Both calls hit the same endpoint — the data is identical. If they disagree, a new scan completed between calls. Look at scanId in the response to confirm which scan each saw.