Skip to content

Deploy from CI

This guide sets up a job that deploys your service whenever main changes, first for GitHub Actions, then for GitLab CI. The same steps work in any CI system that can run a shell script: install the CLI, provide an API key, run gpilot deploy, and act on the exit code.

  • An organization API key with the permissions for the steps your job runs: deploying, and optionally purging and pushing persisted queries. Create it in the portal under your organization’s Settings → API keys. Which permissions each step needs is listed under jobs; grant only those. Name it after the pipeline: the portal shows the key’s name as who made each deployment.

  • The key as a CI secret named GPILOT_TOKEN: in GitHub under Settings → Secrets and variables → Actions, in GitLab as a masked CI/CD variable under Settings → CI/CD → Variables. The CLI reads that variable and uses it in place of a login.

  • Your service’s subdomain, the <subdomain> in https://<subdomain>.<label>.graphpilot.cloud.

  • gpilot.toml, and for a GraphQL API schema.graphql, in the directory the step runs in, or paths to them (--config, --schema). If your schema is generated, generate it in an earlier step; the Apollo Server, GraphQL Yoga and PostGraphile pages show how.

.github/workflows/graphpilot.yml
name: Deploy to GraphPilot
on:
push:
branches: [main]
# Deploy one push at a time, in order, rather than racing two deployments.
concurrency:
group: graphpilot-shop-api
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
env:
GPILOT_TOKEN: ${{ secrets.GPILOT_TOKEN }}
steps:
- uses: actions/checkout@v5
- name: Install the gpilot CLI
run: |
curl --proto '=https' --tlsv1.2 -fsSL https://get.graphpilot.io | sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Deploy
run: gpilot deploy --service shop-api --yes

Replace shop-api with your subdomain.

The same three steps as a GitLab job. resource_group plays the part of concurrency above: one deployment at a time, in order.

.gitlab-ci.yml
deploy-graphpilot:
stage: deploy
image: debian:bookworm-slim
rules:
- if: $CI_COMMIT_BRANCH == "main"
resource_group: graphpilot-shop-api
before_script:
- apt-get update && apt-get install -y --no-install-recommends curl ca-certificates
- curl --proto '=https' --tlsv1.2 -fsSL https://get.graphpilot.io | sh
- export PATH="$HOME/.local/bin:$PATH"
script:
- gpilot deploy --service shop-api --yes

GPILOT_TOKEN reaches the job as the CI/CD variable you created. GitLab sets CI=true as well.

The install script fetches the latest release every time it runs and puts gpilot in ~/.local/bin, which the workflow then adds to the PATH (see installation).

Installing the latest release on every run is deliberate. The CLI checks its version against a minimum that GraphPilot publishes with its releases, and a CLI below that minimum refuses to run and exits with code 6. A version that is merely behind prints a warning and carries on. A pinned or cached binary works until the next minimum passes it, and then your deployments stop.

If a release you depend on is ever refused before you can upgrade, set GPILOT_SKIP_VERSION_CHECK=1 on the step to skip the check for that run. Treat it as a temporary escape, not a setting to leave in place.

GitHub Actions and GitLab CI set CI=true, and the CLI switches to non-interactive mode when it sees it (see interactive vs. non-interactive). In that mode gpilot deploy proceeds without asking, so --yes is not strictly needed for it. Pass it anyway: it states the intent in the workflow. gpilot purge, below, refuses to run without it.

gpilot deploy (short for gpilot deployment create) uploads gpilot.toml and, for a GraphQL API, schema.graphql, starts a deployment, and waits for it to finish, 20 minutes at most by default (--timeout 45m to change that). When the build completes or fails, it prints the deployment’s diagnostics (schema and configuration): warnings on a success, the reasons on a failure. The same diagnostics are in the portal, which also lists the configuration keys that do nothing; the CLI does not print that list (see findings).

Without a schema file in the working directory and without --schema, the service is deployed as plain HTTP, with GraphQL features off, and the job log says No schema found: deploying as a plain HTTP service, GraphQL features are off. For a GraphQL API, make sure the step runs where schema.graphql is, or pass --schema: a missing file does not fail the step, it deploys the service without GraphQL. A --schema path that does not exist does fail it.

--detach returns as soon as the deployment has started. The job then succeeds without knowing whether the deployment did, so use it only if something else watches the outcome.

The job fails whenever gpilot exits with anything but 0. The codes are stable, so a script can branch on them. These are the ones a deploy step returns; exit codes has the full list.

CodeMeaning in a deploy step
0The deployment completed. Warnings, if any, were printed and do not fail the step.
1The deployment failed, was canceled from somewhere else, or the wait timed out. Read the printed diagnostics.
2A flag or value the CLI does not know.
3The API key is missing, revoked, or rejected (HTTP 401).
4The GraphPilot API could not be reached or returned an error, including a permission the key lacks: a valid key without deployment:edit exits 4, not 3.
6This CLI is older than the minimum version. Reinstall to get the latest.
7The outcome is unknown: the progress stream ended before the deployment reported one, and checking it until --timeout ran out did not settle it. The deployment is still open, or every check failed; check with gpilot status.

--timeout covers the whole wait, watching and checking together. When the stream ends early, the CLI keeps checking the deployment until it ends or --timeout runs out: a deployment that completed still exits 0, one that failed exits 1.

A 1 after the deployment started does not mean the service is down: a deployment that fails never goes live, and the previous version keeps serving (see deployments).

Permission cache:purge

Deploying does not clear the cache (see the cache survives a deployment). Entries stored before the deployment keep the lifetime and partitioning they were stored with. If a change makes those entries wrong, purge after the deploy step:

- name: Purge the cache
run: gpilot purge --all --yes --service shop-api

In GitLab, add the same command as another line under script.

Only do this when the change calls for it. A full purge sends the next request for every operation to your origin. Purging through the API covers narrower options.

If your clients send persisted queries, register the operations from your client build before that build is released:

- name: Push persisted queries
run: gpilot pq push ./dist/persisted-documents.json --service shop-api

gpilot pq push only adds: entries already registered are left alone, and nothing is removed. Add --dry-run to check a manifest without registering anything.

A deploy step that exits 0 says the new version is live, not that it answers the way you expect. Send one request through the service and fail the job if it does not come back clean:

- name: Smoke test
run: |
body=$(curl -sS --fail-with-body "https://shop-api.k7m2qp.graphpilot.cloud/graphql" \
-H 'content-type: application/json' \
-H "GP-Correlation-ID: smoke-$GITHUB_SHA" \
-d '{"query":"query Smoke { products(first: 1) { id } }"}')
echo "$body" | jq -e '.errors == null and (.data.products | length) == 1'

--fail-with-body fails the step on an HTTP error status, and the jq -e check fails it on a GraphQL error, which arrives with 200. The GP-Correlation-ID header labels the request, so on Airliner (Business) and above gpilot requests --service shop-api --correlation-id "smoke-$GITHUB_SHA" --wait --timeout 60s finds exactly this request in its telemetry. To deploy to staging first and promote the same commit after an approval, see the recipe promoting from staging in CI.

Push a change to main and watch the job. A green job with exit code 0 means the new version is live. Confirm it from your machine:

Terminal window
gpilot status --service shop-api

The newest deployment is completed, lowercase in both the table and -o json. In the portal, the service’s Deployments list shows the same deployment with its findings. Then send a request through the service and check that it still answers as before: a gp-cache: HIT on a request that was cached before the deployment is expected, because the cache survives a deployment.

  • A pinned or cached CLI stops working the day the minimum version passes it, with exit code 6. Install the latest release in every run.
  • --detach turns every started deployment into a green job, whether it went live or not.
  • gpilot purge without --yes is refused in CI.
  • Persisted queries pushed after the client ships leave the first users with hashes the registry cannot resolve. Push before you release the client.
  • A schema deployed after the origin leaves every query that selects a new field uncached until it lands (CACHE_SKIPPED_UNRESOLVED_SELECTION). Run the GraphPilot deploy step before the step that releases your origin; see deploy the schema before the origin.
  • One service is one environment. To rehearse a change, deploy it to a second service first; see staging and rollback.