> ## Documentation Index
> Fetch the complete documentation index at: https://anaconda.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# CI/CD integration

export const GCell = ({children, className}) => <div className={`grid-table-cell ${className || ""}`} role="cell">
    {children}
  </div>;

export const GTH = ({children, className}) => <div className={`grid-table-th ${className || ""}`} role="columnheader">
    {children}
  </div>;

export const GRow = ({children}) => <div className="grid-table-row" role="row">{children}</div>;

export const GBody = ({children}) => <div className="grid-table-body" role="rowgroup">{children}</div>;

export const GHead = ({children}) => <div className="grid-table-head" role="rowgroup">{children}</div>;

export const GTable = ({children, className, cols}) => <div className={`grid-table not-prose overflow-hidden rounded-2xl ${className || ""}`} style={{
  "--grid-table-cols": cols
}} role="table">
    {children}
  </div>;

export const Comments = ({children}) => {
  return <div class="my-4 px-5 py-4 overflow-hidden rounded-2xl flex gap-3 border border-zinc-500/20 bg-zinc-50/50 dark:border-zinc-500/30 dark:bg-zinc-500/10" data-callout-type="comments">
      <div class="w-4">
        <svg width="14" height="14" viewBox="0 0 640 640" fill="currentColor" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" aria-label="Comments">
            <path d="M320 112C434.9 112 528 205.1 528 320C528 434.9 434.9 528 320 528C205.1 528 112 434.9 112 320C112 205.1 205.1 112 320 112zM320 576C461.4 576 576 461.4 576 320C576 178.6 461.4 64 320 64C178.6 64 64 178.6 64 320C64 461.4 178.6 576 320 576zM280 400C266.7 400 256 410.7 256 424C256 437.3 266.7 448 280 448L360 448C373.3 448 384 437.3 384 424C384 410.7 373.3 400 360 400L352 400L352 312C352 298.7 341.3 288 328 288L280 288C266.7 288 256 298.7 256 312C256 325.3 266.7 336 280 336L304 336L304 400L280 400zM320 256C337.7 256 352 241.7 352 224C352 206.3 337.7 192 320 192C302.3 192 288 206.3 288 224C288 241.7 302.3 256 320 256z" />
        </svg>
      </div>
      <div class="text-sm prose min-w-0 w-full">
        {children}
      </div>
    </div>;
};

Anaconda Platform integrates with CI/CD systems, so your teams can test and deploy flows automatically as part of their existing delivery pipelines. For more informational background on deploying ML/AI systems to production, read [How To Organize Continuous Delivery of ML/AI Systems](https://www.anaconda.com/blog/continuous-delivery-of-ml-ai).

## GitOps for Anaconda Platform

The following diagram illustrates a typical CI/CD pattern:

<Frame>
  <img src="https://mintcdn.com/anaconda-29683c67/TXgc_5FN8574WLab/images/platform/migrated/cicd-overview.png?fit=max&auto=format&n=TXgc_5FN8574WLab&q=85&s=e4ef5f8c602dab978551069a5de4b613" alt="Overview of a GitOps workflow with Anaconda Platform, from local development through pull request, automated testing, review, and deployment" width="1456" height="925" data-path="images/platform/migrated/cicd-overview.png" />
</Frame>

<Steps>
  <Step title="Experiment">
    The user experiments and prototypes code on a cloud workstation or locally on a laptop. The user can test their code at scale quickly and autonomously on the platform's cluster.
  </Step>

  <Step title="Pull request">
    When the code works adequately, the user commits it and opens a pull request. They authenticate with the CI/CD system using their personal credentials.
  </Step>

  <Step title="Run tests">
    The CI/CD system, such as GitHub Actions or CircleCI, launches a test suite automatically when a pull request is opened. The CI/CD system submits workloads to the platform, authenticating as a machine user.
  </Step>

  <Step title="Review and approve">
    After tests pass, a human reviewer reviews the pull request. The reviewer can tag the pull request as approved, and [a corresponding Metaflow tag can be applied to test runs](https://www.anaconda.com/blog/five-ways-to-use-the-new-metaflow-tags) as well, signaling a successful PR.
  </Step>

  <Step title="Deploy">
    After the PR is approved, the CI/CD system deploys the flow either as a new production version or as [a new `@project` variant](https://docs.metaflow.org/production/coordinating-larger-metaflow-projects), running concurrently with the production version so its performance can be evaluated live.
  </Step>
</Steps>

## Supported CI/CD platforms

Anaconda Platform supports all major CI/CD platforms through OIDC-based authentication:

* **GitHub Actions**: Native OIDC support
* **GitLab CI/CD**: Native OIDC support via `id_tokens`
* **Azure DevOps**: Azure AD federation
* **CircleCI**: OIDC token support

Each platform uses a similar pattern:

1. Configure a machine user on the platform. See [Programmatic access via machine users](/docs/platform/guides/security/programmatic-access-via-machine-users) for per-provider claim fields.
2. Set up OIDC authentication in your CI/CD config (YAML examples below).
3. Use `obproject-deploy` to deploy your project.

## Verifying your deploy

`obproject-deploy` tags each workflow it deploys with lineage metadata: the commit hash being deployed, and an identifier for the CI run that triggered the deploy when running in CI. To confirm a deployment succeeded, open the workflow in the UI: select the project, then **Workflows**, and then select the workflow. The workflow details show the tags applied to its runs:

<Frame>
  <img src="https://mintcdn.com/anaconda-29683c67/VD0yQ0tXYWIdTsBU/images/platform/plat_cicd_workflow_tags.png?fit=max&auto=format&n=VD0yQ0tXYWIdTsBU&q=85&s=65585ca9c888edae94b7fb80818d219d" alt="The workflow detail view showing the commit-hash tag the workflow applies to runs, above a table of successful runs" width="1866" height="1082" data-path="images/platform/plat_cicd_workflow_tags.png" />
</Frame>

You can also see the tags in the deploy output, which prints a line such as `Tagging deployments with: commit-hash:15ecbcb5ecf0f21295bf7085a6b9a568f92f85ab`.

If you do not see the tags, the deploy did not complete. Check the CI job's logs for the `obproject-deploy` step and verify the machine user authenticated successfully against `auth.<YOUR_DEPLOYMENT_URL>`.

## Machine user naming convention

The YAML examples below derive the machine user name from your project name:

```sh theme={null}
PROJECT_NAME=$(yq .project obproject.toml)
CICD_USER="${PROJECT_NAME//_/-}-cicd"
```

Two implications:

* The machine user must be named with hyphens rather than underscores. For example, `my-project-cicd`, not `my_project-cicd`. The underscore-to-hyphen normalization in the YAML exists so a project named `my_project` resolves to a machine user named `my-project-cicd`.
* To use a different name, such as a team-shared machine user across multiple projects, set `cicd_user` in `obproject.toml`:

```toml theme={null}
project = "my_project"
cicd_user = "team-shared-cicd"
```

The example YAML files read this value with `yq ".cicd_user // \"$DEFAULT\""`, so an explicit value takes precedence over the derived default.

## The `outerbounds service-principal-configure` flag reference

The auth command takes a different flag per provider:

<GTable cols="18% 32% 50%">
  <GHead>
    <GRow>
      <GTH>Provider</GTH>
      <GTH>Flag</GTH>
      <GTH>Notes</GTH>
    </GRow>
  </GHead>

  <GBody>
    <GRow>
      <GCell>GitHub Actions</GCell>
      <GCell>`--github-actions`</GCell>
      <GCell>Reads `$ACTIONS_ID_TOKEN_REQUEST_TOKEN` / `$ACTIONS_ID_TOKEN_REQUEST_URL` automatically</GCell>
    </GRow>

    <GRow>
      <GCell>GitLab CI</GCell>
      <GCell>`--jwt-token "$OUTERBOUNDS_ID_TOKEN"`</GCell>
      <GCell>Token issued by GitLab's `id_tokens:` keyword</GCell>
    </GRow>

    <GRow>
      <GCell>CircleCI</GCell>
      <GCell>`--jwt-token "$CIRCLE_OIDC_TOKEN_V2"`</GCell>
      <GCell>Requires a CircleCI context (can be empty) on the workflow</GCell>
    </GRow>

    <GRow>
      <GCell>Azure DevOps</GCell>
      <GCell>`--jwt-token "$idToken"`</GCell>
      <GCell>`$idToken` is exposed by the `AzureCLI@2` task when `addSpnToEnvironment: true`</GCell>
    </GRow>

    <GRow>
      <GCell>IAM-backed</GCell>
      <GCell>(none; IAM credentials in the environment)</GCell>
      <GCell>See [Identity by IAM](/docs/platform/guides/security/programmatic-access-via-machine-users#identity-by-iam)</GCell>
    </GRow>
  </GBody>
</GTable>

In all cases, the command also takes `--name <SERVICE_PRINCIPAL_NAME>`, `--deployment-domain <DEPLOYMENT_DOMAIN>`, and `--perimeter <PERIMETER>`. To read these values from `obproject.toml` instead of passing them explicitly, use `--from-obproject-toml`.

## Using Anaconda Platform with GitHub Actions

The [GitHub Actions on OBP demo repository](https://github.com/outerbounds/github-actions-on-obp-demo/) walks through the key workflows in practice.

### Example GitHub Actions workflow

```yaml expandable theme={null}
name: Deploy Project
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
      with:
        fetch-depth: 0

    - name: Set up Python
      uses: actions/setup-python@v5
      with:
        python-version: "3.12"

    - name: Install dependencies
      run: pip install outerbounds ob-project-utils pyyaml

    - name: Configure the machine user
      run: |
        PROJECT_NAME=$(yq .project obproject.toml)
        PLATFORM=$(yq .platform obproject.toml)
        CICD_USER="${PROJECT_NAME//_/-}-cicd"
        outerbounds service-principal-configure \
          --name $CICD_USER \
          --deployment-domain $PLATFORM \
          --perimeter default \
          --github-actions

    - name: Deploy Project
      run: obproject-deploy
```

## Using Anaconda Platform with GitLab CI/CD

GitLab CI/CD supports OIDC authentication via the `id_tokens` keyword. Use the example below as a starting template.

<Warning>
  The `aud` value in `id_tokens` must match your platform URL. GitLab evaluates `id_tokens` at pipeline creation time, before any scripts run, so the value cannot be read dynamically from `obproject.toml`. Update this value when setting up a new project.
</Warning>

### Example `.gitlab-ci.yml`

```yaml expandable theme={null}
stages:
  - deploy

deploy:
  stage: deploy
  image: python:3.12
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == "main"

  id_tokens:
    OUTERBOUNDS_ID_TOKEN:
      aud: <YOUR_PLATFORM_URL>

  script:
    - wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
    - chmod +x /usr/local/bin/yq
    - pip install outerbounds ob-project-utils pyyaml

    - PROJECT_NAME=$(yq .project obproject.toml)
    - PLATFORM=$(yq .platform obproject.toml)
    - CICD_USER="${PROJECT_NAME//_/-}-cicd"
    - |
      outerbounds service-principal-configure \
        --name $CICD_USER \
        --deployment-domain $PLATFORM \
        --perimeter default \
        --jwt-token $OUTERBOUNDS_ID_TOKEN

    - obproject-deploy

  artifacts:
    paths:
      - deployment_summary.md
    when: always
```

<Comments>
  Replace \<YOUR\_PLATFORM\_URL> with your deployment's URL.
</Comments>

## Using Anaconda Platform with Azure DevOps

Azure DevOps supports OIDC through Azure AD federation.

### Example `azure-pipelines.yml`

```yaml expandable theme={null}
trigger:
  - main

pr:
  - main

pool:
  vmImage: ubuntu-latest

steps:
- checkout: self

- task: AzureCLI@2
  displayName: 'Configure the machine user'
  inputs:
    azureSubscription: '<YOUR_SERVICE_CONNECTION>'
    addSpnToEnvironment: true
    scriptType: bash
    scriptLocation: inlineScript
    inlineScript: |
      wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
      chmod +x /usr/local/bin/yq

      PROJECT_NAME=$(yq .project obproject.toml)
      PLATFORM=$(yq .platform obproject.toml)
      CICD_USER="${PROJECT_NAME//_/-}-cicd"

      pip install outerbounds ob-project-utils pyyaml
      outerbounds service-principal-configure \
        --name $CICD_USER \
        --deployment-domain $PLATFORM \
        --perimeter default \
        --jwt-token $idToken

- script: obproject-deploy
  displayName: 'Deploy Project'
  env:
    SYSTEM_ACCESSTOKEN: $(System.AccessToken)
```

<Comments>
  Replace \<YOUR\_SERVICE\_CONNECTION> with the name of your Azure service connection.
</Comments>

## Using Anaconda Platform with CircleCI

CircleCI supports OIDC tokens for secure authentication.

### Example `.circleci/config.yml`

```yaml expandable theme={null}
version: 2.1

jobs:
  deploy:
    docker:
      - image: cimg/python:3.12
    steps:
      - checkout
      - run:
          name: Install dependencies
          command: |
            wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
            chmod +x /usr/local/bin/yq
            pip install outerbounds ob-project-utils pyyaml
      - run:
          name: Configure the machine user
          command: |
            PROJECT_NAME=$(yq .project obproject.toml)
            PLATFORM=$(yq .platform obproject.toml)
            CICD_USER="${PROJECT_NAME//_/-}-cicd"
            outerbounds service-principal-configure \
              --name $CICD_USER \
              --deployment-domain $PLATFORM \
              --perimeter default \
              --jwt-token $CIRCLE_OIDC_TOKEN_V2
      - run:
          name: Deploy Project
          command: obproject-deploy

workflows:
  deploy:
    jobs:
      - deploy:
          context: [<YOUR_OIDC_CONTEXT>]
          filters:
            branches:
              only: [main]
```

<Comments>
  Replace \<YOUR\_OIDC\_CONTEXT> with the name of a CircleCI context with OIDC enabled. The context can be empty.
</Comments>

If you need help setting up GitOps in your environment, contact Anaconda support.
