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

title

Yes

Any non-empty text, such as the service and version

triggeredAt

Yes

When the deployment started, in UTC ending in Z: 2026-10-20T15:04:05Z

status

No, but send it

success, failure, or pending. Defaults to pending, which Span doesn't count

environment

No

production, or any name containing prod or prd. Leave it out for production

services

No

List of service slugs, such as ["billing-api"]. Used to credit the deployment to a team

git.repoUrl

No

The repository URL, such as https://github.com/acme/billing-api

git.refName

No

The full commit SHA you deployed

completedAt

No

When the deployment finished, same format as triggeredAt

type

No

deploy, rollback, or restart. Defaults to deploy

pullRequests

No

PR numbers shipped in this deployment, such as [412, 415]. Used only once deploy-to-PR linking is on. Needs git.repoUrl

version, httpUrl, description, deployer, metadata

No

Extra context. metadata values must be text

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.

  1. Store the token in AWS Secrets Manager as span/dora-api-token and allow the CodeBuild service role to read it.

  2. 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.

  3. On the CodeBuild action, add an environment variable COMMIT_ID with 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.

  4. 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 and billing-api with 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 as pending and never counts. If you post at the start of a deployment, update it when it finishes with PATCH /api/dora/v1/deployments/<id> and {"status":"success"}.

  • Send an Idempotency-Key so retries don't double count. Without one, each POST creates 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 with DELETE /api/dora/v1/deployments/<id>.

  • Timestamps must be UTC and end in Z. 2026-10-20T15:04:05Z works, with or without milliseconds. 2026-10-20T15:04:05+00:00 and timestamps with no time zone are rejected with a 400 error. date -u +%Y-%m-%dT%H:%M:%SZ produces the right format.

  • Name your production environment so it contains prod or prd, or leave it out. A production environment called live or main won'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, pullRequests is 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, from pullRequests or from the PRs merged since your previous deployment, matched on git.refName. pullRequests also needs a GitHub, GitHub Enterprise Cloud (*.ghe.com), or Azure DevOps git.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-api service ships at 3:04 PM UTC and the pipeline sends status: success, environment: production, and services: ["billing-api"]. The Payments team owns billing-api, so the deployment adds 1 to both the organization total and the Payments team. If the same pipeline also posts its staging deployment with environment: staging, that one doesn't count anywhere.

Related articles

How to use the DORA API to send deployment activity to Span

Deployments / Week

Lead Time to Production in Span

Change Failure Rate Report

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.