Guides · Set up
Check every deploy
Add one step to your deploy pipeline, with examples for GitHub Actions, GitLab CI, Bitbucket Pipelines and a plain deploy script.
What the check does
Your pipeline tells us when a deploy has finished. 30 seconds later, once the new version has had time to roll out, we check the site: your homepage and key pages, robots.txt and your sitemaps, a few pages of your biggest page types, such as product pages or articles, and pages new in your sitemap since the last full crawl, with the pages they link to.
Your plan sets how many. Key pages: up to 10 on Free and 100 on Pro. Pages of your page types, and new pages: up to 10 of each on Free and 100 of each on Pro. Key pages never count against your monthly page allowance. The other pages do, and when the allowance runs out, the check goes on with your key pages only.
The job in your pipeline waits up to five minutes for our answer and prints what we found. It fails only when the deploy brought a new critical issue or error, or when the site stopped working: the homepage answers with a server error or "not found", or the site gives no answer while our own connection works. Issues the site already had, issues you've hidden or put in the backlog, and warnings never fail it.
Nothing on our side fails the job. If we can't be reached, take longer than five minutes or can't finish the check, the job says so and passes.
Deploys close together share one check. If another deploy arrives while the check is still waiting to start, the check looks at the newest deploy instead, and counts once. A deploy is also when we look again at issues someone marked as fixed.
Free checks 20 deploys a month. Pro checks every deploy. Once Free's 20 are used, a deploy is still accepted: the job says it wasn't checked and passes, every day we still check your key pages, and fixes waiting for an answer are still checked.
The site has to be verified first: see Show the site is yours. Until then, a call from your pipeline is accepted, and nothing is checked.
Step 1: create a deploy key
This part is done in the dashboard, by someone who can change the site's settings.
- Open the site, then Settings → Connections, and find Check after a deploy.
- Click Create a deploy key. Copy the key straight away: it is shown only once, because we keep only a scrambled copy of it.
- Hand it to your developer, who saves it as a secret named
SEO_HOOK_TOKENin the pipeline that deploys the site. It never goes in a file in the repository. - Click Copy the steps for your developer and send them along. They hold the GitHub Actions steps and the script for other pipelines, never the key.
Each site has its own key. If it's lost, click Replace the deploy key. The old one stops working at once, so the secret needs the new one too.
Step 2: add the step to your pipeline
This part is for your developer. The step runs once the deploy has succeeded, and sends us the key and the commit.
GitHub Actions
- In the repository that deploys the site, open Settings → Secrets and variables → Actions → New repository secret. Name it
SEO_HOOK_TOKENand paste the deploy key as its value. - In the workflow that deploys, in
.github/workflows/, add the job below underjobs:. Changeneeds: deployto the name of the job that deploys, so the check runs only after a deploy that succeeded.
# Your workflow, as it is today.
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: ./deploy.sh
# Add this job, after the job that deploys.
seo-uptime:
needs: deploy
runs-on: ubuntu-latest
timeout-minutes: 7
steps:
- name: Check the deploy with SEO Uptime
uses: codenyra/seo-uptime@v1
with:
token: ${{ secrets.SEO_HOOK_TOKEN }}
The job runs our GitHub Action, codenyra/seo-uptime,
a short public script you can read before you add it. It reads the commit from
GitHub, so there is nothing else to set. If your GitHub organization allows only the
actions it has chosen, run the script below as a step instead, with
SEO_HOOK_TOKEN: ${{ secrets.SEO_HOOK_TOKEN }} and
COMMIT_SHA: ${{ github.sha }} under its env:.
The script, for any other pipeline
The other examples run this script. Save it in the repository as
seo-uptime-check.sh. It is the same script the dashboard's steps carry.
It runs in plain sh and needs curl and jq, the deploy key in
SEO_HOOK_TOKEN and the commit in COMMIT_SHA.
set +e
answer=$(curl --silent --show-error --max-time 10 -X POST https://seouptime.com/hooks/deploy \
-H "Authorization: Bearer $SEO_HOOK_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"sha\":\"$COMMIT_SHA\"}") \
|| { echo "SEO Uptime could not be reached (ignored)"; exit 0; }
printf '%s\n' "$answer" | jq -r '.status' 2>/dev/null || { echo "SEO Uptime gave an unexpected answer (ignored)"; exit 0; }
status_url=$(printf '%s\n' "$answer" | jq -r '.status_url // empty' 2>/dev/null)
[ -n "$status_url" ] || exit 0
deadline=$(($(date +%s) + 300))
while [ "$(date +%s)" -lt "$deadline" ]; do
sleep 10
result=$(curl --silent --max-time 5 -H "Authorization: Bearer $SEO_HOOK_TOKEN" "$status_url")
[ "$(printf '%s\n' "$result" | jq -r '.state' 2>/dev/null)" = done ] || continue
printf '%s\n' "$result" | jq -r '.summary, (.new_defects[] | " \(.severity): \(.name), \(.url) (\(.rule))"), "Details: \(.url)"'
[ "$(printf '%s\n' "$result" | jq -r '.result')" = fail ] && exit 1
exit 0
done
echo "SEO Uptime is still checking after 5 minutes (ignored)."GitLab CI
Add the key under Settings → CI/CD → Variables, with the key
SEO_HOOK_TOKEN, the deploy key as its value, and masked. If you also
protect it, only pipelines on protected branches get it, which is fine when you deploy
from one. Then add a job in a stage after your deploy.
# Your stages, with check added after deploy.
stages:
- build
- test
- deploy
- check
seo-uptime:
stage: check
image: alpine:3.22
variables:
COMMIT_SHA: $CI_COMMIT_SHA
before_script:
- apk add --no-cache curl jq
script:
- sh seo-uptime-check.sh
rules:
# The same rules as your deploy job, so it runs only after a deploy.
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCHBitbucket Pipelines
Add the key under Repository settings → Repository variables, named
SEO_HOOK_TOKEN, with Secured ticked. Then add a step after the
step that deploys.
pipelines:
branches:
main:
# Your deploy step, as it is today.
- step:
name: Deploy
script:
- ./deploy.sh
# Add this step after it.
- step:
name: Check the deploy with SEO Uptime
image: atlassian/default-image:4
script:
- apt-get update && apt-get install -y curl jq
- COMMIT_SHA=$BITBUCKET_COMMIT sh seo-uptime-check.shA deploy script on your server
If the deploy is a script, such as a Laravel Forge deploy script, a shell script or a Makefile target, run the check as its last line, in the folder the deploy checked out. The server needs curl and jq. Keep the key out of the script itself, for example in a file only the deploy user can read.
# The last lines of your deploy script, once the new version is live.
export SEO_HOOK_TOKEN="$(cat "$HOME/.seo-hook-token")"
COMMIT_SHA="$(git rev-parse HEAD)" sh seo-uptime-check.sh
When the check finds a new critical issue or error, the script exits with 1, so your
deploy tool shows the deploy as failed, though the new version is already live. Add
|| true to the last line if you'd rather it didn't.
Just a quick call
If your platform lets you run one command after a deploy and nothing more, send the
call on its own. The key goes in the Authorization header, as here, or as
token in the JSON body. The sha is optional, but without it
the dashboard can't say which commit was checked.
curl -s -X POST https://seouptime.com/hooks/deploy -H "Authorization: Bearer $SEO_HOOK_TOKEN" -H "Content-Type: application/json" -d "{\"sha\":\"$COMMIT_SHA\"}"
We answer at once with HTTP 202 and a status_url, which the script asks
with the same key until the check is done. Without that wait, the check runs and its
alerts go out as usual, but your pipeline never sees the result: nothing in its log,
and the job never fails. On GitHub Actions, wait: false under the
action's with: does the same.
Step 3: see that it worked
After the next deploy, the job's log shows what the check found and a link to it, usually within two minutes. From the script, a deploy that broke a key page reads like this:
deploy check queued
New: 1 critical problem on 24 pages checked after deploy 4f9c2e1.
critical: The homepage or a critical page is noindex, https://www.example.com/pricing (NOINDEX_CRITICAL_URL)
Details: https://seouptime.com/…/crawls/4182A deploy that broke nothing prints "Nothing new on 24 pages checked after deploy 4f9c2e1." The GitHub Action prints the same, shows each new issue as an annotation on the run, and adds a table to the run's summary.
In the dashboard, the check appears in the site's History as a Deploy check. Under Settings → Connections, Check after a deploy then says when it was last called and for which commit. On Free it also says how many of the month's deploy checks are used.
If it doesn't work
Each of these leaves the job green. The job's log says which one it was.
"unknown token" (HTTP 401)
The secret doesn't match the site's deploy key: a typo, a key replaced since, or a secret the job can't see, such as a protected GitLab variable on a branch that isn't protected. The key can't be shown again, so replace it and save the new one.
Too many calls (HTTP 429)
One key may call us 60 times an hour from one address. Past that, calls are refused until the hour is up, and nothing is checked. The script then says "SEO Uptime gave an unexpected answer (ignored)".
"still checking after 5 minutes"
The job stopped waiting. The check carries on, and its result is in the site's History.
"This month's 20 deploy checks are used"
On Free, the deploys after that aren't checked until the next calendar month. Every day we still check your key pages.
"the site is not verified, so nothing was started"
Verify the site under Settings → Setup → Ownership. The job says the same kind of thing when the site's verification has lapsed, or when the site is past what the team's plan covers.
"could not be reached" or "gave an unexpected answer"
From the script, the first can also mean curl isn't installed, and the second that jq isn't. Without jq the call still reaches us and the check runs, but the job can't read the answer. Install both in the job's image.