Send deployments to Span from GitHub Actions or AWS CodePipeline
Last updated: October 27, 2026
Add one step to your deploy pipeline and Span counts every successful production deployment, with no manual work after setup. This article shows the step for GitHub Actions and AWS CodePipeline.
Why it matters
Span doesn't read deployments from GitHub Actions or AWS CodePipeline on its own. Your pipeline has to tell Span each time it ships. Once it does, Span fills in Deployments, Deployments / week, and Change failure rate, and, once Span turns on deploy-to-PR linking for your organization, lead time from first commit to production.
The step is a single HTTPS call. You set it up once per pipeline, and every deployment after that reaches Span automatically.
How it works
After a production deployment finishes, your pipeline sends one POST to the DORA API. Span stores it as a deployment record right away and includes it in your metrics on the next refresh.
POST https://span.app/api/dora/v1/deployments with the header Authorization: Bearer <your Span token>.
Field | Required | What to send |
|---|---|---|
| Yes | Any non-empty text, such as the service and version |
| Yes | When the deployment started, in UTC ending in |
| No, but send it |
|
| No |
|
| No | List of service slugs, such as |
| No | The repository URL, such as |
| No | The full commit SHA you deployed |
| No | When the deployment finished, same format as |
| No |
|
| No | PR numbers shipped in this deployment, such as |
| No | Extra context. |
What counts. Span counts a deployment when status is success and the environment is blank or contains prod or prd, in any letter case. production, prd-us, and pre-prod all count. staging, qa, and live don't. Rollbacks and restarts count like any other deployment.
Which team gets credit. Span credits each deployment to the teams that own the services you list in services. A deployment with no service, or a service with no owner, shows in your organization total but not under any team.
Setup
1. Create a token and store it as a secret. In Span, go to Settings → Service Accounts, open the menu on the service account's row, choose Manage Token, then Generate. Only admins who can manage service accounts see this page, so ask your Span admin if it's missing. A service account token keeps working when people change roles, which a personal token doesn't. Store it in your CI as a secret named SPAN_API_TOKEN. Never paste it into a workflow file.
2. Give each service an owning team. Span credits a deployment to the team that owns its service. Create each service once, with its owner, before your first pipeline run. slug is the value your pipeline will send in services: lowercase, starting with a letter, using letters, numbers, and single - or _ separators. The team slug must match an existing Span team.
curl --fail-with-body -sS -X POST https://span.app/api/dora/v1/services \
-H "Authorization: Bearer $SPAN_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Billing API","slug":"billing-api","owners":[{"type":"team","slug":"payments"}]}'
If the service already exists, this returns an error. Find its id with GET /api/dora/v1/services, then set the owner with PATCH /api/dora/v1/services/<id> and {"owners":[{"type":"team","slug":"payments"}]}. The API is the only way to set owners.
3. Send a test deployment. Run this once from your terminal. A successful call returns the new deployment, including its id.
curl --fail-with-body -sS -X POST https://span.app/api/dora/v1/deployments \
-H "Authorization: Bearer $SPAN_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"Test deploy","triggeredAt":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'","status":"success","environment":"production","services":["billing-api"]}'
Check that it appears under Catalog → Deployments, then remove it so it doesn't count: curl -X DELETE https://span.app/api/dora/v1/deployments/<id> -H "Authorization: Bearer $SPAN_API_TOKEN".
4a. GitHub Actions. Add the first step at the start of your production deploy job and the last step at the end. Replace billing-api with your service slug.
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Record deploy start
run: echo "DEPLOY_STARTED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_ENV"
# ... your existing deploy steps ...
- name: Send deployment to Span
if: success()
env:
SPAN_API_TOKEN: ${{ secrets.SPAN_API_TOKEN }}
run: |
payload=$(jq -n \
--arg title "billing-api ${GITHUB_SHA::7}" \
--arg started "$DEPLOY_STARTED_AT" \
--arg completed "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg repo "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" \
--arg sha "$GITHUB_SHA" \
--arg run "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
'{title: $title, triggeredAt: $started, completedAt: $completed,
status: "success", environment: "production",
services: ["billing-api"], httpUrl: $run,
git: {repoUrl: $repo, refName: $sha}}')
curl --fail-with-body -sS --retry 3 -X POST \
https://span.app/api/dora/v1/deployments \
-H "Authorization: Bearer ${SPAN_API_TOKEN}" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: gha-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" \
-d "$payload"
To keep a record of failed deployments, copy the last step, set if: failure(), and send status: "failure". Failed deployments show in the Catalog but don't change any metric.
4b. AWS CodePipeline. Add a stage after your production deploy stage with one AWS CodeBuild action. CodePipeline only runs it when the deploy stage succeeds.
Store the token in AWS Secrets Manager as
span/dora-api-tokenand allow the CodeBuild service role to read it.On your pipeline's source action, set the variable namespace to
SourceVariables. This works for git sources: CodeCommit, or GitHub and Bitbucket through a connection. An S3 or ECR source has no commit ID.On the CodeBuild action, add an environment variable
COMMIT_IDwith the value#{SourceVariables.CommitId}. CodePipeline fills in#{...}values only in action settings like this one, never inside a buildspec. If you skip this step, the buildspec falls back to$CODEBUILD_RESOLVED_SOURCE_VERSION.Give the CodeBuild project this buildspec. It uses only
curl, which every CodeBuild image includes, and stops with a clear message if it can't find a full commit SHA. Replace the repo URL andbilling-apiwith your own.
version: 0.2
env:
secrets-manager:
SPAN_API_TOKEN: span/dora-api-token
phases:
build:
commands:
- |
COMMIT_ID="${COMMIT_ID:-$CODEBUILD_RESOLVED_SOURCE_VERSION}"
case "$COMMIT_ID" in
""|*[!0-9a-f]*) echo "No commit SHA found. Map COMMIT_ID to #{SourceVariables.CommitId} on the CodeBuild action."; exit 1 ;;
esac
[ ${#COMMIT_ID} -eq 40 ] || { echo "COMMIT_ID isn't a full commit SHA: $COMMIT_ID"; exit 1; }
- NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
- SHORT_SHA=$(echo "$COMMIT_ID" | cut -c1-7)
- |
code=$(curl -sS -o span-response.json -w "%{http_code}" -X POST \
https://span.app/api/dora/v1/deployments \
-H "Authorization: Bearer ${SPAN_API_TOKEN}" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: codebuild-${CODEBUILD_BUILD_ID}" \
-d "{\"title\":\"billing-api ${SHORT_SHA}\",\"triggeredAt\":\"${NOW}\",\"completedAt\":\"${NOW}\",\"status\":\"success\",\"environment\":\"production\",\"services\":[\"billing-api\"],\"git\":{\"repoUrl\":\"https://github.com/acme/billing-api\",\"refName\":\"${COMMIT_ID}\"}}")
cat span-response.json
[ "$code" -lt 300 ]
This action runs after the deployment, so triggeredAt is the finish time rather than the start time. That shifts a deployment's date only when it runs across midnight UTC.
Things to know
Send
status: "success"every time. Without it the deployment is saved aspendingand never counts. If you post at the start of a deployment, update it when it finishes withPATCH /api/dora/v1/deployments/<id>and{"status":"success"}.Send an
Idempotency-Keyso retries don't double count. Without one, eachPOSTcreates a new deployment, so a resent call after a timeout counts twice. With one, Span processes the first successful call and replays its response for any repeat with the same key within 24 hours. The examples above build the key from the run or build ID, so retries of the call are safe while a re-run of the whole job records a new deployment. If you do end up with an extra, delete it withDELETE /api/dora/v1/deployments/<id>.Timestamps must be UTC and end in
Z.2026-10-20T15:04:05Zworks, with or without milliseconds.2026-10-20T15:04:05+00:00and timestamps with no time zone are rejected with a 400 error.date -u +%Y-%m-%dT%H:%M:%SZproduces the right format.Name your production environment so it contains
prodorprd, or leave it out. A production environment calledliveormainwon't count.Use the same service slug every time. A slug Span hasn't seen before creates a new service with no owner, so a typo sends deployments to the organization total only. Assign owners once with the DORA API services endpoint.
Send the full commit SHA in
git.refName. When deploy-to-PR linking is on, Span matches it against PR merge commits to work out which PRs a deployment shipped. Both examples above send the full SHA.Lead time for changes and PR linking are off by default. Span turns them on for your organization on request. Until then,
pullRequestsis stored but not used, and the DORA page shows PR cycle time where lead time would be. Once it's on, Span links each deployment to the PRs in it, frompullRequestsor from the PRs merged since your previous deployment, matched ongit.refName.pullRequestsalso needs a GitHub, GitHub Enterprise Cloud (*.ghe.com), or Azure DevOpsgit.repoUrl, or the call returns a 400 error.Turning on PR linking changes which team gets credit. With it off, deployments go to the teams that own their services. With it on, deployments go to the teams of the linked PRs' authors, and a deployment with no linked PRs counts in the organization total only. Expect team numbers to shift when you switch.
Span doesn't read deployments from your CI on its own. A deployment reaches Span only when your pipeline sends it. Deployments from before setup won't appear unless you post them.
Worked example. Your
billing-apiservice ships at 3:04 PM UTC and the pipeline sendsstatus: success,environment: production, andservices: ["billing-api"]. The Payments team ownsbilling-api, so the deployment adds 1 to both the organization total and the Payments team. If the same pipeline also posts its staging deployment withenvironment: staging, that one doesn't count anywhere.
Related articles
How to use the DORA API to send deployment activity to Span
Lead Time to Production in Span
Need help?
Contact your Span account team or reach out to support@span.app. For a failed call, include the response body and the time you sent it so we can trace it.