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
| Step | Call | What it does |
|---|---|---|
| 1. Record the change | POST /api/v1/x/changes, or POST /api/v1/x/changes/from-template for a standard change | The change, with the commit or build as externalRef. Usually done ahead of the run, when the release is planned. |
| 2. Ask the gate | GET /api/v1/x/changes/CHG-1042/gate | May it deploy now? Answers allowed, and the reasons when not. |
| 3. Start the change | POST /api/v1/x/changes/CHG-1042/start | Marks it In progress, inside its window. |
| 4. Deploy | Your own deployment. | |
| 5. Finish the change | POST /api/v1/x/changes/CHG-1042/finish | The 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:readandchanges:writescopes, 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:
| Reason | What to do |
|---|---|
| It is … , not scheduled | It isn't approved and scheduled yet (a draft, waiting for approval, rejected …). |
| Not every approval is in place | Wait for the approvers its policy asks. |
| It changed after it was approved | Someone edited it after approval; it needs approving again. |
| Its template isn't approved now | A standard change whose template is suspended, past its review date or changed. |
| It has no window / Its window starts … / Its window is over | Deploy 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 tofailed_not_backed_outif 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.