Skip to main content

Pipelines and the deployment gate

Most changes are deployments. An approval only works as a control when a deployment can't happen without it, so the pipeline asks the desk before it deploys, and tells it how it went afterwards.

Record the change → Ask the gate → Start the change → Deploy → Finish the change
Ask the gate → Stop: the gate says no, so the job fails and nothing is deployed
StepCallWhat it does
1. Record the changePOST /api/v1/x/changes, or POST /api/v1/x/changes/from-template for a standard changeThe change, with the commit or build as externalRef. Usually done ahead of the run, when the release is planned.
2. Ask the gateGET /api/v1/x/changes/CHG-1042/gateMay it deploy now? Answers allowed, and the reasons when not.
3. Start the changePOST /api/v1/x/changes/CHG-1042/startMarks it In progress, inside its window.
4. DeployYour own deployment.
5. Finish the changePOST /api/v1/x/changes/CHG-1042/finishThe outcome: successful, successful_with_issues, failed_backed_out or failed_not_backed_out, and a note.

[!IMPORTANT] The pipeline's API token needs the changes:read and changes:write scopes, and the token's owner must own or carry out the change (or hold Manage changes). Keep the token in the CI system's secrets, never in the repository. See API tokens.

What a run looks like​

When the change is approved and its window has started, the gate lets the deployment through:

Deploy Pulseboard Web · main · run #482
✓ Check out the code · 2s
✓ Build and test · 1m 12s
✓ Ask the change gate · 1s
GET /api/v1/x/changes/CHG-1042/gate
{"change":"CHG-1042","status":"scheduled","allowed":true,"reasons":[]}
✓ Start the change · 1s
✓ Deploy · 48s
✓ Finish the change · 1s
CHG-1042 finished: successful

When it isn't, the job stops at the gate, and the deployment never runs:

Deploy Pulseboard Web · main · run #483
✓ Check out the code · 2s
✓ Build and test · 1m 9s
✗ Ask the change gate · 1s
GET /api/v1/x/changes/CHG-1042/gate
{"change":"CHG-1042","status":"scheduled","allowed":false,
"reasons":["Its window starts 2026-10-30 18:00 UTC"]}
Error: Process completed with exit code 1.
– Start the change
– Deploy
– Finish the change

The gate's answer​

{ "change": "CHG-1042", "status": "scheduled", "allowed": false, "reasons": ["Not every approval is in place"] }

allowed is true while the change is in progress, or when it may start now. Otherwise reasons says why, in words you can show in the pipeline's log:

ReasonWhat to do
It is … , not scheduledIt isn't approved and scheduled yet (a draft, waiting for approval, rejected …).
Not every approval is in placeWait for the approvers its policy asks.
It changed after it was approvedSomeone edited it after approval; it needs approving again.
Its template isn't approved nowA standard change whose template is suspended, past its review date or changed.
It has no window / Its window starts … / Its window is overDeploy inside the window, or reschedule the change.
A freeze is on: …Wait for the freeze to end; only emergency changes run in one.

GitHub Actions​

A deploy job that asks the gate first, starts the change, deploys, and finishes it with the outcome. Set DESK_TOKEN as a repository secret and the change's key as CHANGE:

jobs:
deploy:
runs-on: ubuntu-latest
env:
DESK_URL: https://desk.example.com
DESK_TOKEN: ${{ secrets.DESK_TOKEN }}
CHANGE: CHG-1042
steps:
- uses: actions/checkout@v4

- name: Ask the change gate
run: |
answer=$(curl -sf -H "Authorization: Bearer $DESK_TOKEN" "$DESK_URL/api/v1/x/changes/$CHANGE/gate")
echo "$answer"
test "$(echo "$answer" | jq -r .allowed)" = "true"

- name: Start the change
id: start
run: curl -sf -X POST -H "Authorization: Bearer $DESK_TOKEN" "$DESK_URL/api/v1/x/changes/$CHANGE/start"

- name: Deploy
run: ./deploy.sh

- name: Finish the change
if: always() && steps.start.outcome == 'success'
run: |
outcome=${{ job.status == 'success' && 'successful' || 'failed_backed_out' }}
curl -sf -X POST -H "Authorization: Bearer $DESK_TOKEN" -H "Content-Type: application/json" \
-d "{\"outcome\":\"$outcome\",\"note\":\"Run $GITHUB_RUN_ID\"}" \
"$DESK_URL/api/v1/x/changes/$CHANGE/finish"

[!TIP] A job that fails the gate never reaches Deploy. The last step runs whenever the change was started, so a failed deployment is recorded too; it reports failed_backed_out, so change it to failed_not_backed_out if your deployment doesn't roll itself back. When the gate stopped the run, there is nothing to finish: the change was never started.

GitLab CI​

The same calls in the deploy job: the gate in before_script, so a refusal stops the job before script runs.

deploy:
stage: deploy
variables:
CHANGE: CHG-1042
before_script:
- answer=$(curl -sf -H "Authorization: Bearer $DESK_TOKEN" "$DESK_URL/api/v1/x/changes/$CHANGE/gate")
- echo "$answer"
- test "$(echo "$answer" | jq -r .allowed)" = "true"
- curl -sf -X POST -H "Authorization: Bearer $DESK_TOKEN" "$DESK_URL/api/v1/x/changes/$CHANGE/start"
script:
- ./deploy.sh

Tracing a release​

Dev tasks from Software Development can be linked to the change that releases them, so a code change can be traced from the request to the release.