CI/CD ingestion
Mint a token that only records deployments, report a deployment in one call after the fact or as it starts and ends, keep retries harmless with the run id as the idempotency key, and let GitHub deployments report themselves.
By the end of this page your pipeline will report every deployment it makes: the version, the environment, the commit and the branch, as it starts and as it ends, with a credential that can do nothing else. What the record looks like from inside the application is on CI and deployments.
Mint an ingestion token
Open Account Settings → API Tokens and press Create Token. Name it after the pipeline,
confine it to the project the pipeline deploys, and tick ci:deploy and nothing else. The ability
satisfies the deployment writes alone: the token can record and finish deployments, and every
read, every other write and every other project is refused. The deployments it records are written
as you, so mint it as the release manager or as a service account the team keeps for the purpose.
Put the plaintext in the pipeline's secret store; it is shown once. API tokens
covers the page in full.
You will also need the project's id and the environment's id, both in the address bar of their
pages and in the GET /v1/projects/{project}/environments answer; keep them beside the token as
pipeline variables.
Report a deployment in one call
A pipeline that reports after the deployment is done sends one request:
curl -sS -X POST "$BIFROST_API/v1/projects/$BIFROST_PROJECT/deployments" \
-H "Authorization: Bearer $BIFROST_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $PIPELINE_RUN_ID" \
-d '{
"environment_id": "'"$BIFROST_ENVIRONMENT"'",
"version": "1.4.0",
"commit_sha": "'"$COMMIT_SHA"'",
"branch": "main",
"pipeline_url": "'"$PIPELINE_URL"'"
}'The status is succeeded when left out; send "status": "failed" from the failure branch of the
job and the record is kept as a failure. The answer is 201 with the deployment, and the
environment now carries the version as its Current Version.
Report as the deployment starts and ends
A pipeline that reports as it goes opens the deployment first, deploys, then finishes it whichever way the job ended:
DEPLOYMENT_ID=$(curl -sS -X POST "$BIFROST_API/v1/projects/$BIFROST_PROJECT/deployments" \
-H "Authorization: Bearer $BIFROST_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $PIPELINE_RUN_ID-start" \
-d '{"environment_id": "'"$BIFROST_ENVIRONMENT"'", "version": "1.4.0", "status": "running", "commit_sha": "'"$COMMIT_SHA"'", "branch": "main", "pipeline_url": "'"$PIPELINE_URL"'"}' \
| jq -r '.data.id')curl -sS -X PATCH "$BIFROST_API/v1/deployments/$DEPLOYMENT_ID" \
-H "Authorization: Bearer $BIFROST_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $PIPELINE_RUN_ID-finish" \
-d '{"status": "succeeded"}'Send failed or cancelled instead when the job ended that way; a finish step that always runs,
reading the job's own outcome, is the shape the ready-made snippets use. A deployment finished once
is not moved again, so a second finish, from a retried job, answers 422 and changes nothing.
Keep retries harmless
Every write needs an Idempotency-Key. Use the pipeline's run id, with a suffix per step when a
run makes more than one call: a retried step with the same key and the same body receives the
first answer again, so a flaky network records nothing twice, while the same key with a different
body is refused. The rules are on the idempotency page.
What the record carries
| Field | Required | What to send |
|---|---|---|
environment_id | yes | The environment deployed into; one of the project's. |
version | yes | What was deployed, up to 60 characters: a tag, a semantic version or a build number. |
status | no | succeeded when left out; running to open a deployment you will finish, queued or failed. |
commit_sha | no | The commit deployed, 7 to 40 hex characters. |
branch | no | The branch or tag deployed from. |
pipeline_url | no | The run's own page, so a reader can go from the record to the log. |
notes | no | Anything worth keeping with the record, up to 5,000 characters. |
started_at | no | When it started, ISO 8601; now when left out and the status is past queued. |
finished_at | no | When it ended, ISO 8601; now when left out and the status is final. |
The full contract, the finish request and every refusal are on the Deployments reference.
What is refused
| Answer | Why |
|---|---|
403 FORBIDDEN | The token carries neither ci:deploy nor deployment:record, its owner's role lacks the permission, or it is a guest's. |
404 RESOURCE_NOT_FOUND | The environment is not the project's, or the deployment to finish is not one the token's project holds. |
422 VALIDATION_FAILED | A field is malformed, the status is not one allowed here, or the deployment ended already. |
400 IDEMPOTENCY_KEY_MISSING | A write came without an Idempotency-Key. |
Reads are not for pipelines
An ingestion token reads nothing, by design: a credential that lives in a pipeline's secret store should not be able to list a project's work. Read the environments and the deployments with a staff token, or over MCP.
Or let GitHub deployments report themselves
A GitHub workflow that creates deployments through GitHub's own Deployments API, on a repository picked from the team's App installation, needs no step: the App hears each deployment and each of its statuses and records them on the environment whose name matches. The matching and the version rules are on CI and deployments.
Ready-made snippets
GitHub Actions
A composite action you copy into the repository that reports as the job starts and as it ends.
GitLab CI and other pipelines
A job that reports from GitLab CI, and the two calls for any other runner.
Related
- CI and deployments: what the record looks like inside the application and who is told.
- Deployments API: the full contract.
- API tokens: minting and confining the token.
- Idempotency: how retries are made harmless.
deployment.*webhook events: what goes out as a deployment moves.
How is this guide?
Last updated on