> For the complete documentation index, see [llms.txt](https://docs.drizz.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.drizz.dev/automate-and-integrate/ci-cd.md).

# CI/CD

Any CI/CD system that can make an authenticated HTTP request can start a Drizz run: authenticate, upload the build, trigger the plan.

|                   |                                                                           |
| ----------------- | ------------------------------------------------------------------------- |
| **Platforms**     | GitHub Actions · Jenkins · GitLab CI · Bitbucket Pipelines · Azure DevOps |
| **Requirement**   | Anything that can make an authenticated HTTP request                      |
| **Env vars**      | `DRIZZ_BASE_URL`, `DRIZZ_API_KEY`                                         |
| **Pipeline does** | authenticate → upload → trigger                                           |
| **Watch out**     | The trigger step succeeds even if every test later fails                  |

## Prerequisites

* Client ID, client secret, auth host and audience, issued by Drizz
* API base URL for your organization
* Test plan ID, from the web app
* A CI secret store holding all five values
* `curl` and `jq` available on the build agent

## What a pipeline can and cannot do

| Consequence                                                     | Detail                                       |
| --------------------------------------------------------------- | -------------------------------------------- |
| The trigger step goes green as soon as the run is **triggered** | It reports the trigger, not the test outcome |
| A build cannot be failed on a Drizz test failure                | No run status is available to the pipeline   |
| Results are read by a person                                    | Open the report in the Drizz web app         |

## Copy this

```bash
#!/usr/bin/env bash
set -euo pipefail

# Secrets come from your CI secret store, never from the repository
: "${DRIZZ_BASE_URL:?}" "${DRIZZ_AUTH_DOMAIN:?}" "${DRIZZ_CLIENT_ID:?}"
: "${DRIZZ_CLIENT_SECRET:?}" "${DRIZZ_AUDIENCE:?}"

# 1 — fresh token for this run
DRIZZ_API_KEY=$(curl -sf -X POST "https://${DRIZZ_AUTH_DOMAIN}/oauth/token" \
  -H "Content-Type: application/json" \
  -d "{\"client_id\":\"${DRIZZ_CLIENT_ID}\",\"client_secret\":\"${DRIZZ_CLIENT_SECRET}\",\"audience\":\"${DRIZZ_AUDIENCE}\",\"grant_type\":\"client_credentials\"}" \
  | jq -r .access_token)

# 2 — upload the build
curl -sf -X POST "${DRIZZ_BASE_URL}/apk/upload" \
  -H "x-api-key: ${DRIZZ_API_KEY}" \
  -F "file=@app-release.apk" | tee upload.json

# 3 — trigger the plan against the version just uploaded
VERSION=$(jq -r .details.version_name upload.json)
curl -sf -X POST "${DRIZZ_BASE_URL}/testplan/run" \
  -H "x-api-key: ${DRIZZ_API_KEY}" \
  -H "Content-Type: application/json" \
  -d "{\"test_plan_id\":\"ADuF4ViN\",\"apks\":{\"com.shopease.android\":\"${VERSION}\"}}" \
  | tee trigger.json

# The run has started. Open the report in the Drizz web app for the outcome.
echo "execution_id: $(jq -r .execution_details.execution_id trigger.json)"
```

## Pipeline steps

1. Request an access token.
2. Upload the build — only when the app version changed.
3. Trigger one test plan, or a batch.
4. Record the `execution_id` in the build log.
5. Archive the trigger response as a build artifact.
6. Open the report in the Drizz web app to read the outcome. This step is manual.

## Supported platforms

The integration is identical on every platform. Only the surrounding syntax changes.

| Platform                | Trigger points                         |
| ----------------------- | -------------------------------------- |
| **GitHub Actions**      | Pull requests, merges, manual dispatch |
| **Jenkins**             | Scripted or declarative pipelines      |
| **GitLab CI**           | Branch pipelines, scheduled jobs       |
| **Bitbucket Pipelines** | Build or release stages                |
| **Azure DevOps**        | Release pipelines                      |

## Platform examples

Both examples trigger a run and finish. Each passes whether the tests pass or fail. The step that would wait for the result is marked in the file and deliberately not written, because there is no endpoint to call.

{% tabs %}
{% tab title="GitHub Actions" %}

```yaml
name: Drizz regression

on:
  workflow_dispatch:
  push:
    branches: [main]

jobs:
  drizz:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build the app
        run: ./gradlew assembleRelease

      - name: Authenticate with Drizz
        id: auth
        env:
          DRIZZ_AUTH_DOMAIN: ${{ secrets.DRIZZ_AUTH_DOMAIN }}
          DRIZZ_CLIENT_ID: ${{ secrets.DRIZZ_CLIENT_ID }}
          DRIZZ_CLIENT_SECRET: ${{ secrets.DRIZZ_CLIENT_SECRET }}
          DRIZZ_AUDIENCE: ${{ secrets.DRIZZ_AUDIENCE }}
        run: |
          TOKEN=$(curl -sf -X POST "https://${DRIZZ_AUTH_DOMAIN}/oauth/token" \
            -H "Content-Type: application/json" \
            -d "{\"client_id\":\"${DRIZZ_CLIENT_ID}\",\"client_secret\":\"${DRIZZ_CLIENT_SECRET}\",\"audience\":\"${DRIZZ_AUDIENCE}\",\"grant_type\":\"client_credentials\"}" \
            | jq -r .access_token)
          echo "::add-mask::$TOKEN"
          echo "token=$TOKEN" >> "$GITHUB_OUTPUT"

      - name: Upload the build
        env:
          DRIZZ_BASE_URL: ${{ secrets.DRIZZ_BASE_URL }}
          DRIZZ_API_KEY: ${{ steps.auth.outputs.token }}
        run: |
          curl -sf -X POST "${DRIZZ_BASE_URL}/apk/upload" \
            -H "x-api-key: ${DRIZZ_API_KEY}" \
            -F "file=@app/build/outputs/apk/release/app-release.apk" \
            | tee upload.json

      - name: Trigger the test plan
        env:
          DRIZZ_BASE_URL: ${{ secrets.DRIZZ_BASE_URL }}
          DRIZZ_API_KEY: ${{ steps.auth.outputs.token }}
        run: |
          VERSION=$(jq -r .details.version_name upload.json)
          curl -sf -X POST "${DRIZZ_BASE_URL}/testplan/run" \
            -H "x-api-key: ${DRIZZ_API_KEY}" \
            -H "Content-Type: application/json" \
            -d "{\"test_plan_id\":\"ADuF4ViN\",\"apks\":{\"com.shopease.android\":\"${VERSION}\"}}" \
            | tee trigger.json
          echo "Started: $(jq -r .execution_details.execution_id trigger.json)"

      # BLOCKED — a "wait for the run and fail on failure" step goes here.
      # It needs a run-status endpoint, which does not exist yet.
      # Until then this workflow reports that a run started, not whether it passed.

      - name: Keep the trigger record
        uses: actions/upload-artifact@v4
        with:
          name: drizz-trigger
          path: |
            upload.json
            trigger.json
```

{% endtab %}

{% tab title="Jenkins" %}

```groovy
pipeline {
  agent any

  environment {
    DRIZZ_BASE_URL   = credentials('drizz-base-url')
    DRIZZ_AUTH       = credentials('drizz-auth-domain')
    DRIZZ_AUDIENCE   = credentials('drizz-audience')
    DRIZZ_CLIENT     = credentials('drizz-client-id')
    DRIZZ_SECRET     = credentials('drizz-client-secret')
    TEST_PLAN_ID     = 'ADuF4ViN'
    PACKAGE_NAME     = 'com.shopease.android'
  }

  stages {
    stage('Build') {
      steps {
        sh './gradlew assembleRelease'
      }
    }

    stage('Authenticate') {
      steps {
        script {
          env.DRIZZ_API_KEY = sh(
            returnStdout: true,
            script: '''
              curl -sf -X POST "https://${DRIZZ_AUTH}/oauth/token" \
                -H "Content-Type: application/json" \
                -d "{\\"client_id\\":\\"${DRIZZ_CLIENT}\\",\\"client_secret\\":\\"${DRIZZ_SECRET}\\",\\"audience\\":\\"${DRIZZ_AUDIENCE}\\",\\"grant_type\\":\\"client_credentials\\"}" \
                | jq -r .access_token
            '''
          ).trim()
        }
      }
    }

    stage('Upload') {
      steps {
        sh '''
          curl -sf -X POST "${DRIZZ_BASE_URL}/apk/upload" \
            -H "x-api-key: ${DRIZZ_API_KEY}" \
            -F "file=@app/build/outputs/apk/release/app-release.apk" \
            | tee upload.json
        '''
      }
    }

    stage('Trigger') {
      steps {
        sh '''
          VERSION=$(jq -r .details.version_name upload.json)
          curl -sf -X POST "${DRIZZ_BASE_URL}/testplan/run" \
            -H "x-api-key: ${DRIZZ_API_KEY}" \
            -H "Content-Type: application/json" \
            -d "{\\"test_plan_id\\":\\"${TEST_PLAN_ID}\\",\\"apks\\":{\\"${PACKAGE_NAME}\\":\\"${VERSION}\\"}}" \
            | tee trigger.json
        '''
      }
    }

    // BLOCKED — a "Wait for result" stage goes here once a run-status
    // endpoint exists. There is nothing to poll today.
  }

  post {
    always {
      archiveArtifacts artifacts: 'upload.json,trigger.json', allowEmptyArchive: true
      echo 'Drizz run triggered. Open the report in the Drizz web app for the outcome.'
    }
  }
}
```

{% endtab %}
{% endtabs %}

## Secrets

| Value                                                   | Where it belongs                                                       |
| ------------------------------------------------------- | ---------------------------------------------------------------------- |
| Client ID, client secret, audience, auth host, base URL | CI secret store. Never committed                                       |
| Access token                                            | Run-scoped variable, masked in logs — `::add-mask::` in GitHub Actions |

## Common mistakes

| What you did                                     | What happens                                   |
| ------------------------------------------------ | ---------------------------------------------- |
| Treated a green trigger step as a green test run | It only means the run started                  |
| Put the Drizz step in front of a release gate    | The gate lets everything through               |
| Uploaded the build on every commit               | Slow pipelines, and 409s on unchanged versions |
| Cached the token between pipeline runs           | It expires after 24 hours                      |
| Echoed the token in a build log                  | Anyone with log access can trigger your runs   |
| Looped single triggers over 30 plans             | HTTP 429. Use the batch endpoint               |

## Next

* [API overview](/automate-and-integrate/api-overview.md) — the base URL and the auth model
* [Trigger a run](/automate-and-integrate/trigger-a-run.md) — single and batch triggers
* [Errors & limits](/automate-and-integrate/errors-and-limits.md) — rate limits and retries

***

*Last updated: 6 August 2026*
