Promoting from staging in CI
Goal: every merge to main goes live on staging by itself, is checked with a real request,
and reaches production only after somebody approves it. The same commit is deployed to both, so
production gets exactly what staging was tested with.
GraphPilot has no promotion between services: a deployment builds from the files in front of it. So “promote” means deploying the same files from the same commit to a second service, and the approval lives in your CI. The pieces are on deploy from CI and staging and rollback; this recipe puts them into one workflow.
The running example is the service pair shop-api-staging and shop-api, deployed from one
repository that holds schema.graphql, gpilot.toml, and the client’s persisted query manifest
at dist/persisted-documents.json.
What you need
Section titled “What you need”- Both services, created as on staging and rollback.
- An organization API key as the repository secret
GPILOT_TOKEN, with the permissions to deploy and to push persisted queries to both services. See jobs. - A GitHub environment
called
productionwith required reviewers. That is the approval gate.
1. The workflow
Section titled “1. The workflow”name: GraphPilot
on: pull_request: push: branches: [main]
concurrency: group: graphpilot-shop-api cancel-in-progress: false
env: GPILOT_TOKEN: ${{ secrets.GPILOT_TOKEN }}
jobs: check: if: github.event_name == 'pull_request' runs-on: ubuntu-latest 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: Check the persisted query manifest run: gpilot pq push ./dist/persisted-documents.json --dry-run --service shop-api
staging: if: github.event_name == 'push' runs-on: ubuntu-latest 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 staging run: gpilot deploy --service shop-api-staging --yes - name: Push persisted queries run: gpilot pq push ./dist/persisted-documents.json --service shop-api-staging - name: Smoke test run: ./scripts/smoke.sh https://shop-api-staging.k7m2qp.graphpilot.cloud
production: needs: staging runs-on: ubuntu-latest environment: production 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 production run: gpilot deploy --service shop-api --yes - name: Push persisted queries run: gpilot pq push ./dist/persisted-documents.json --service shop-api - name: Smoke test run: ./scripts/smoke.sh https://shop-api.k7m2qp.graphpilot.cloudReplace k7m2qp with your organization’s hostname label.
checkruns on every pull request.--dry-runparses and verifies the manifest and writes nothing, so a broken manifest fails the pull request instead of the release.stagingdeploys on every merge.gpilot deploywaits for the deployment and exits0only when it completed, so a failed build stops the workflow before the smoke test. The exit codes say why.productionwaits forstaging, then for a reviewer of theproductionenvironment. It checks out the same commit, so it deploys the same files staging was tested with.- Persisted queries are pushed to each service before your client release, so no client ever
sends a hash the registry cannot resolve.
pq pushonly adds, so pushing a manifest twice is harmless. Release the client after theproductionjob, never before it.
2. The smoke test
Section titled “2. The smoke test”#!/usr/bin/env bashset -euo pipefailhost="$1"
body=$(curl -sS --fail-with-body "$host/graphql" \ -H 'content-type: application/json' \ -H "GP-Correlation-ID: smoke-$GITHUB_SHA" \ -d '{"query":"query Smoke { products(first: 1) { id name } }"}')
echo "$body" | jq -e '.errors == null and (.data.products | length) == 1'--fail-with-body fails on an HTTP error status, and jq -e fails on a GraphQL error, which
arrives with 200. Pick a query that exercises your origin and your schema together: a broken
origin fails it, and so does a schema whose new field your origin does not know yet.
The GP-Correlation-ID header labels the request. On Airliner (Business) and above, add a step
that waits for it in telemetry, which proves the request was recorded on the service you meant:
gpilot requests --service shop-api-staging --correlation-id "smoke-$GITHUB_SHA" \ --wait --timeout 60s -o json | jq -e '.[0].status == 200'3. What you see
Section titled “3. What you see”- In GitHub, a merge shows
staginggreen andproductionwaiting for review. Approving it runs the production deployment. - In the portal, each service’s Deployments list shows the new deployment with the API key’s name as who made it, and its findings. Name the key after the pipeline so the list reads well.
- In Insights, the smoke request is in each service’s request list with its correlation ID on the Overview tab of the request detail.
- From a terminal,
gpilot status --service shop-apilists the newest deployment ascompleted.
Pitfalls
Section titled “Pitfalls”- One key for both services is one key that can deploy production. If the approval must not be
bypassable, use two keys, and make the production key an environment secret of
productionrather than a repository secret. - The cache survives the deployment. A
HITright after the deploy is an entry stored before it. If the change made cached entries wrong, purge them in the production job; see purge after deploying. - Staging’s cache says nothing about production’s. The two services share nothing: cache, keys and telemetry are separate.
- Deploy the schema before the origin. If the same workflow releases your origin, run the GraphPilot deployment first; see deploy the schema before the origin.