Deploy from GitHub Actions and other CI

llms.txt

If your tests run in a continuous integration (CI) system such as GitHub Actions, GitLab CI or Bitbucket Pipelines, the natural next step is to deploy only when they pass. SrvYard gives you two ways to start a deployment from there: the deploy hook, a secret address you request with one curl command, and the API, which needs an access token but also lets you wait for the deployment to finish and read its result.

This article shows both, with a complete GitHub Actions workflow for each, explains what the answers mean, how to see in SrvYard what CI did, and how to rotate the secret if it leaks. Whatever you choose, SrvYard deploys the branch configured on the site: CI says when, not what.

Before you begin

  • A site that deploys by hand first. Run Deploy now once and read the log (see Write and test a deploy script).
  • A secret store in your CI system. The hook address and the API token are credentials. Keep them in the pipeline's secrets, never in the repository.
  • For the API route, a token with the right scope. Create it as shown in Create an API token and pick scopes: the scope for starting and reading deployments is sites:manage-deploys (plus sites:view to read them).
  • Do not combine with push to deploy unless you want both. If push to deploy is on, every push already deploys; the CI step would add a second deployment.

1. Use the deploy hook

Open the site's Deployments tab. The Deploy hook card holds the address.

The Deploy hook card
The deploy hook address is a secret: anyone who has it can start a deployment.
  1. The address. It has the form https://your-srvyard-host/hooks/deploy/<token>. The card says "Request this URL (GET or POST) from CI or a script to start a deployment. Add ?branch=main to ignore other branches." Only owners and admins see it.
  2. Copy. Copies the address for pasting into your CI secrets.
  3. Create a new URL. Replaces the token (step 4).

The same address is on Settings > Deployments, under Deploy hook URL, with a Regenerate URL button.

Request it with either method:

curl -fsS "$DEPLOY_HOOK_URL?branch=main"

The answers, always JSON:

Status Body Meaning
202 {"status":"queued","deployment":"<id>"} A deployment was queued. If one was already waiting, its id is returned instead and no second one is made.
202 {"status":"ignored","reason":"The branch does not match the site's branch."} You passed ?branch= and it is not the site's branch. Nothing was queued. This is not an error.
404 {"message":"Not found."} The token is wrong or was replaced.
409 {"status":"rejected","message":"..."} The site cannot deploy now: it is not ready, its server is not ready, it has no repository or its deploy script is empty.
429 More than 30 requests in a minute from one address.

curl -f turns 4xx and 5xx answers into a non-zero exit status, so a wrong token fails the pipeline step. The hook returns as soon as the deployment is queued; it does not wait for the result. If CI must know whether the deployment worked, use the API (step 3).

Note

The hook takes only an optional branch. It does not choose a commit or another branch: the deployment fetches the tip of the site's branch when it starts. See Deploy now, redeploy and switch branches.

A GitHub Actions workflow

name: Deploy
on:
  push:
    branches: [main]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: composer install --no-interaction --prefer-dist
      - run: php artisan test
  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - name: Start the SrvYard deployment
        run: curl -fsS "${{ secrets.SRVYARD_DEPLOY_HOOK }}?branch=main"

Add SRVYARD_DEPLOY_HOOK under Settings > Secrets and variables > Actions of the repository. GitHub hides the value in logs.

2. Check what CI did

Every deployment records how it started.

The history filtered to API deployments
Deployments started from CI show the trigger API or Deploy hook.
  1. A row. In the history, the Trigger column says Deploy hook for the address above and API for a request with a token. The Trigger filter above the table (its entries are named Hook and Api) lists only those, as in the screenshot. The commit, branch and author are the ones SrvYard fetched when the deployment started.
The details of an API deployment
The details page records how the deployment was started.
  1. Triggered by. API or Deploy hook. For the API the audit trail also records the token's name, so you can tell which pipeline started it.

3. Use the API and wait for the result

The API needs a token but answers properly: it returns the deployment, and you can poll it until it finishes.

  1. Start the deployment (the site id is the UUID in the site's address):
curl -fsS -X POST "https://your-srvyard-host/api/v1/sites/$SITE_ID/deployments" \
  -H "Authorization: Bearer $SRVYARD_TOKEN" \
  -H "Idempotency-Key: $GITHUB_SHA"

The answer is 202 Accepted with the deployment under data (its id, status, trigger and the commit once known) and a status_url. An Idempotency-Key makes a repeated request with the same key and body return the first answer instead of queueing another deployment.

  1. Poll until it ends. The statuses are queued, running, finished, failed and cancelled:
until status=$(curl -fsS "https://your-srvyard-host/api/v1/deployments/$ID" \
    -H "Authorization: Bearer $SRVYARD_TOKEN" | jq -r '.data.status'); \
    [ "$status" = finished ] || [ "$status" = failed ] || [ "$status" = cancelled ]; do
  sleep 10
done
[ "$status" = finished ]

The last line makes the script exit with an error unless the deployment finished. A deployment whose health check fails ends as failed although the release is live.

  1. Read the log with GET /api/v1/deployments/<id>/log when it failed, and print it in CI so nobody has to open SrvYard.

A workflow that starts the deployment, waits and fails the job on a bad result:

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - name: Deploy and wait
        env:
          SRVYARD_TOKEN: ${{ secrets.SRVYARD_TOKEN }}
          SITE_ID: ${{ vars.SRVYARD_SITE_ID }}
        run: |
          ID=$(curl -fsS -X POST "https://your-srvyard-host/api/v1/sites/$SITE_ID/deployments" \
            -H "Authorization: Bearer $SRVYARD_TOKEN" | jq -r '.data.id')
          until status=$(curl -fsS "https://your-srvyard-host/api/v1/deployments/$ID" \
              -H "Authorization: Bearer $SRVYARD_TOKEN" | jq -r '.data.status'); \
              [ "$status" = finished ] || [ "$status" = failed ] || [ "$status" = cancelled ]; do
            sleep 10
          done
          echo "Deployment $ID: $status"
          [ "$status" = finished ]

The API allows 60 requests a minute per token. Polling every ten seconds stays far below that. The conventions (errors, pagination, rate limit headers) are in Conventions: authentication, pagination, errors, rate limits.

4. Rotate the secret

If the hook address was ever pasted somewhere it should not be (a log, a ticket, a public repository), replace it.

The Create a new deploy hook URL confirmation
The old address stops working as soon as you confirm.
  1. Create new URL. The current address stops working immediately and requests to it answer 404. Update the secret in every pipeline that uses it. The new address is shown on the card.

For an API token, delete it and create a new one under your account's API tokens. See Rotate tokens and review the sign-in log.

Troubleshooting

  • The step fails with curl: (22) ... 404. The address is wrong or was regenerated. Copy the current one from the Deploy hook card.
  • The step succeeds but nothing deploys. The answer was 202 with "status":"ignored": your ?branch= differs from the site's branch. Fix the branch in the URL or in the site's repository settings.
  • 409 rejected. Read the message: the deploy script may be empty, the repository missing or the server not ready.
  • Two deployments for one push. Push to deploy and the CI step are both on. Turn one off.
  • The workflow finishes before the deployment does. The hook only queues the deployment. Use the API and poll, as above.
  • 403 missing_scope. The token lacks sites:manage-deploys. Create a new token with the scope.
  • 429. Too many requests. Wait for the time in the Retry-After header.
Last updated 2026-10-247 min readNeed help? Contact support