---
title: Pipelines
description: Reference for every Azure DevOps pipeline shipped by Forge templates — what each one deploys, what triggers it, which shared template it extends, and the PR path filters that decide which validation runs on a pull request.
---

# Pipelines

Every Forge repository gets its pipelines from the templates that scaffolded it. Service
templates compose **feature templates**, and each feature contributes YAML files under `.azdo/`
plus the metadata that registers them in Azure DevOps and wires up branch policies. The same
files can also be generated from an Aspire AppHost.

This page is the source of truth for **what each pipeline does, what triggers it, and which one
to run**. Pipeline names below use the scaffold metadata tokens `{project_id}` and
`{front_end_project_id}`; your repository substitutes its own identifiers at scaffold time. For
diagnosing a pipeline that is failing, see
[Pipeline Troubleshooting](../guides/development/pipeline-troubleshooting.md).

---

## 🧭 Which pipeline do I run?

**Run the deploy pipeline for the service you are deploying.** Each service pipeline deploys
everything that service owns — the API pipeline deploys your application *and* its full auth
configuration in a single run.

| Deploying…            | Run                      | Deploys                                            |
| --------------------- | ------------------------ | -------------------------------------------------- |
| An API                | `{project_id}-api`       | App infrastructure, container, and all auth stages |
| A front end           | `{front_end_project_id}` | Web infrastructure and static site                 |
| An event subscription | `{project_id}-sub`       | Subscription infrastructure and function app       |
| An event service      | `{project_id}`           | Event service infrastructure and container         |
| A docs site           | `{project_id}-docs`      | Published documentation site (manual)              |
| A package or module   | `{project_id}`           | Published package artifact                         |

### Shortcut pipelines

| Pipeline            | Use when                                                                    |
| ------------------- | --------------------------------------------------------------------------- |
| `{project_id}-auth` | You changed only `infra/auth` and want it applied without a full API deploy |

!!! tip "You rarely need the auth pipeline"
    The API pipeline already runs the auth stages — `auth_ext_okta_client`, `auth_ext_app`,
    `auth_ext_user`, and `auth_corp`. The standalone auth pipeline exists purely as a faster
    path for auth-only changes. See
    [Deployment Workflow](../guides/security/configuration/deployment-workflow.md).

### PR validation

Validation is chosen automatically by branch policy path filters — you do not pick it.

| You changed…                                                         | Validation that runs        |
| -------------------------------------------------------------------- | --------------------------- |
| `src/**`, `infra/api/**`, `*.sln`, generated OpenAPI                 | `{project_id}-api-pr`       |
| `infra/auth/**`, `.azdo/azure-pipelines-auth*.yml`                   | `{project_id}-auth-pr`      |
| `infra/web/**`, `src/*.Frontend/**`                                  | `{front_end_project_id}-pr` |
| `infra/sub/**`, `src/*.Subscriptions/**`, `src/*.ServiceBus.Seed/**` | `{project_id}-sub-pr`       |
| `docs/**`, `mkdocs.yml`, `.azdo/vars/docs.yml`                       | `{project_id}-docs-pr`      |
| Package sources in a package repository                              | `{project_id}-pr`           |

!!! note "The auth split is about review, not deployment"
    `infra/auth` is excluded from the API PR validation filter so that auth changes route to the
    auth validation and the auth approver group. It is **not** excluded because auth deploys
    separately — the API pipeline still applies auth on merge. A pull request touching both
    application code and `infra/auth` runs **both** validations and requires **both** reviewer
    groups.

---

## 📁 What your repository gets

Service templates do not ship pipelines directly. They run a setup script that composes
**feature templates**, and each feature contributes its own `.azdo` files. The pipeline set in a
repository therefore depends on the template and the options chosen at scaffold time.

### API services (`saif-api-exp`, `saif-api-proc`, `saif-api-sys`)

All three compose an identical feature set:

| Feature template                             | Included when               | Pipelines contributed                                      |
| -------------------------------------------- | --------------------------- | ---------------------------------------------------------- |
| `saif-feature-base`                          | Always                      | None — contributes `.azdo/vars/base.yml` only              |
| `saif-feature-api`                           | `application_type == api`   | `-api`, `-api-pr`, `-auth`, `-auth-pr`, `-pr-slot-cleanup` |
| `saif-feature-front-end-react`               | `front_end_type == react`   | `{front_end_project_id}`, `{front_end_project_id}-pr`      |
| `saif-feature-dependabot`                    | Always                      | `-dependabot`                                              |
| `saif-feature-docs-site`                     | `include_docs_site == true` | `-docs`, `-docs-pr`                                        |
| `saif-feature-database-oracle` / `-cosmosdb` | Database selected           | None                                                       |
| `saif-feature-webhook`                       | `has_webhook == true`       | None                                                       |

So a processing API with no front end and no docs site has **six** pipelines; the same API with
React and a docs site has **ten**.

### Front-end services (`saif-frontend-service`)

| Feature template               | Included when               | Pipelines contributed                                 |
| ------------------------------ | --------------------------- | ----------------------------------------------------- |
| `saif-feature-base`            | Always                      | None                                                  |
| `saif-feature-front-end-react` | Always                      | `{front_end_project_id}`, `{front_end_project_id}-pr` |
| `saif-feature-web-standalone`  | Always                      | Overrides the web pipelines, adds `-pr-slot-cleanup`  |
| `saif-feature-docs-site`       | `include_docs_site == true` | `-docs`, `-docs-pr`                                   |

`saif-feature-web-standalone` runs after `saif-feature-front-end-react` and overwrites
`azure-pipelines-web*.yml` with the standalone variants. There is no API, auth, or Dependabot
pipeline.

### Added after scaffolding

Some features are applied to an existing repository rather than at creation:

```bash
saif new saif-feature-event-subscription   # adds -sub and -sub-pr
saif new saif-feature-docs-site            # adds -docs and -docs-pr
saif new saif-feature-dependabot           # adds -dependabot
```

### Standalone repository templates

`saif-event-service`, `saif-nuget-package`, `saif-typespec-package`, `saif-terraform-module`,
`saif-dynatrace-config`, `saif-azure-terraform-team`, `saif-business-roles`, and
`saif-test-tools` are not composed from features — each is its own repository type with a fixed
pipeline set, listed below.

---

## 🏗️ How pipelines are wired

### Registration and branch policies

Pipelines are not registered by hand. Each template declares them in
`.template.config/saif-cli.host.json`, and the SAIF CLI creates the Azure DevOps pipeline
definitions and branch policies from that declaration:

```json
{
  "pipelines": [
    {
      "path": ".azdo/azure-pipelines-api-pr.yml",
      "name": "{project_id}-api-pr",
      "validations": [
        {
          "branch": "refs/heads/main",
          "displayName": "Api Validation",
          "pathFilter": "/*;!/infra/auth/*;!/infra/web/*"
        }
      ]
    }
  ]
}
```

| Field         | Meaning                                                                       |
| ------------- | ----------------------------------------------------------------------------- |
| `path`        | The YAML file in the repository                                               |
| `name`        | The pipeline name in Azure DevOps (`{project_id}` is substituted at scaffold) |
| `validations` | Creates a build validation branch policy; `pathFilter` decides when it runs   |

A pipeline **without** a `validations` entry is never attached to a branch policy. That is why
deploy pipelines use `trigger:` for CI and PR pipelines use `trigger: none` — the PR run is
started by the branch policy, not by the YAML.

### Path filter syntax

`pathFilter` is a semicolon-separated list. A leading `!` excludes:

```text
/*;!/infra/auth/*;!/infra/web/*;!/src/*.Frontend/*
```

Reads as: everything, except auth, except web infrastructure, except frontend source.

### Shared templates

Pipeline logic lives in `SAIF/pipeline-templates`, not in your repository. Repository YAML only
supplies parameters:

```yaml
resources:
  repositories:
    - repository: templates
      type: git
      name: SAIF/pipeline-templates
      ref: refs/heads/releases/v3

extends:
  template: azure-dotnet-api-v3.yml@templates
  parameters:
    applicationName: ${{ variables.ApplicationName }}
    projectId: ${{ variables.ProjectId }}
```

### Orchestrators

The shared templates are thin wrappers. Two orchestrators do the real work, and the deploy and
PR variants of a pipeline are the **same orchestrator** with a different `enablePreview` value:

| Orchestrator                                  | Used by                                                  | Components                          |
| --------------------------------------------- | -------------------------------------------------------- | ----------------------------------- |
| `/v2/orchestrators/azure-app-orchestrator-v3.yml` | `azure-dotnet-api-v3.yml`, `azure-dotnet-api-pr-v3.yml`, `azure-react-web-v3.yml`, `azure-react-web-pr-v3.yml` | App, web, and auth |
| `/v2/orchestrators/azure-auth-orchestrator.yml`   | `azure-auth.yml`, `azure-auth-pr.yml`                     | Auth only                           |

| Parameter               | Effect                                                        |
| ----------------------- | ------------------------------------------------------------- |
| `enablePreview: true`   | PR validation — Terraform plan and validation only             |
| `enablePreview: false`  | Full deployment — Terraform apply and deploy                   |
| `enableApi`             | Enables the `api_*` components                                 |
| `enableFrontEnd`        | Enables the `web_*` components                                 |
| `deployToSlot`          | Creates an App Service deployment slot per PR                  |

The auth orchestrator runs `auth_ext_okta_client`, `auth_ext_app`, `auth_ext_user`, and
`auth_corp` — exactly the same auth components the app orchestrator runs. That is why the
standalone auth pipeline is a shortcut rather than a separate deployment path.

---

## 📦 Application pipelines

### API (`saif-feature-api`)

| Pipeline                              | Azure DevOps name              | Trigger                                                                | Shared template              |
| ------------------------------------- | ------------------------------ | ---------------------------------------------------------------------- | ---------------------------- |
| `azure-pipelines-api.yml`             | `{project_id}-api`             | CI on `main` — `.azdo`, `infra`, `src`, `*.sln`; excludes `infra/auth` | `azure-dotnet-api-v3.yml`    |
| `azure-pipelines-api-pr.yml`          | `{project_id}-api-pr`          | Branch policy                                                          | `azure-dotnet-api-pr-v3.yml` |
| `azure-pipelines-auth.yml`            | `{project_id}-auth`            | Manual — optional shortcut for auth-only changes                       | `azure-auth.yml`             |
| `azure-pipelines-auth-pr.yml`         | `{project_id}-auth-pr`         | Branch policy                                                          | `azure-auth-pr.yml`          |
| `azure-pipelines-pr-slot-cleanup.yml` | `{project_id}-pr-slot-cleanup` | Webhook on PR closed                                                   | `azure-pr-slot-cleanup.yml`  |

`azure-dotnet-api-v3.yml` deploys the application **and** the auth configuration — both extend
the same orchestrator, `/v2/orchestrators/azure-app-orchestrator-v3.yml`, which runs these
components:

```text
setup_pr_slot_cleanup → bootstrap → auth_ext_okta_client
  → api_infra → api_crypto → api_container → api_openapi → feature_flags → tag_api
  → web_infra → web_container → web_private_link → tag_web
  → auth_ext_app → auth_ext_user → auth_corp
```

`auth_ext_app`, `auth_ext_user`, and `auth_corp` all depend on `auth_ext_okta_client`. The API
and web components are gated by the `enableApi` and `enableFrontEnd` parameters, so the API
pipeline runs the `api_*` components and the web pipeline runs the `web_*` components — both run
the auth components.

`infra/auth` is excluded from the CI trigger so that auth-only commits do not force a full
application redeploy. The auth components still execute whenever the pipeline runs for any other
reason.

When the API is scaffolded with a React front end, the API trigger additionally excludes
`.azdo/azure-pipelines-web*.yml`, `infra/web`, and `src/*.Frontend`, so front-end changes do not
redeploy the API.

The slot cleanup pipeline is driven by a `PRClosed-{project_id}` webhook resource rather than a
branch trigger. See [PR Slot Deployments](../guides/development/pr-slot-deployments.md).

### Web (`saif-feature-front-end-react`, `saif-feature-web-standalone`)

| Pipeline                              | Azure DevOps name                        | Trigger                                                                                       | Shared template             |
| ------------------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------- |
| `azure-pipelines-web.yml`             | `{front_end_project_id}`                 | CI on `main` — `.azdo`, `infra`, `src/*.Frontend`; excludes `infra/api`, `infra/auth`         | `azure-react-web-v3.yml`    |
| `azure-pipelines-web-pr.yml`          | `{front_end_project_id}-pr`              | Branch policy; standalone also declares a `pr:` trigger on `.azdo`, `infra`, `src/*.Frontend` | `azure-react-web-pr-v3.yml` |
| `azure-pipelines-pr-slot-cleanup.yml` | `{front_end_project_id}-pr-slot-cleanup` | Webhook on PR closed (standalone only)                                                        | `azure-pr-slot-cleanup.yml` |

### Event subscription (`saif-feature-event-subscription`)

| Pipeline                     | Azure DevOps name     | Trigger                                                                                          | Shared template              |
| ---------------------------- | --------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------- |
| `azure-pipelines-sub.yml`    | `{project_id}-sub`    | CI on `main` — `.azdo`, `infra`, `src`, `*.sln`; excludes `infra/auth`, `infra/api`, `infra/web` | `azure-dotnet-sub-v2.yml`    |
| `azure-pipelines-sub-pr.yml` | `{project_id}-sub-pr` | Branch policy                                                                                    | `azure-dotnet-sub-pr-v2.yml` |

### Event service (`saif-event-service`)

| Pipeline                 | Azure DevOps name | Trigger       | Shared template              |
| ------------------------ | ----------------- | ------------- | ---------------------------- |
| `azure-pipelines.yml`    | `{project_id}`    | CI on `main`  | `azure-event-service.yml`    |
| `azure-pipelines-pr.yml` | `{project_id}-pr` | Branch policy | `azure-event-service-pr.yml` |

---

## 📚 Documentation and scanning pipelines

### Docs site (`saif-feature-docs-site`)

| Pipeline            | Azure DevOps name      | Trigger                                                                 | Shared template        |
| ------------------- | ---------------------- | ----------------------------------------------------------------------- | ---------------------- |
| `azure-docs.yml`    | `{project_id}-docs`    | Manual (`trigger: none`, `pr: none`)                                    | `azure-docs-v2.yml`    |
| `azure-docs-pr.yml` | `{project_id}-docs-pr` | `pr:` trigger on `main` — `docs/*`, `mkdocs.yml`, `.azdo/vars/docs.yml` | `azure-docs-pr-v2.yml` |

!!! info "Docs PR validation uses a `pr:` trigger"
    Unlike the API and web PR pipelines, `azure-docs-pr.yml` declares its own `pr:` trigger in
    YAML rather than relying solely on a branch policy path filter. Publishing the site is a
    deliberate manual step — run `{project_id}-docs` when you want the site updated. See
    [Documentation Site](../guides/development/documentation-site.md).

### Dependabot (`saif-feature-dependabot`)

| Pipeline              | Azure DevOps name         | Trigger                                            | Behaviour                                                                          |
| --------------------- | ------------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------- |
| `dependabot-scan.yml` | `{project_id}-dependabot` | Daily cron `0 11 * * *` (3 AM PST), `always: true` | Runs the `dependabot@2` task; opens update PRs with squash merge and auto-complete |

Can also be run manually for an immediate scan.

---

## 📦 Package and infrastructure pipelines

These repositories publish artifacts rather than deploying an application. All follow the same
two-pipeline shape: CI on `main` publishes, and a branch-policy PR pipeline validates.

| Template                    | Deploy pipeline   | PR pipeline          | Shared templates                                             |
| --------------------------- | ----------------- | -------------------- | ------------------------------------------------------------ |
| `saif-nuget-package`        | `{project_id}`    | `{project_id}-pr`    | `package-dotnet-nuget.yml` / `package-dotnet-nuget-pr.yml`   |
| `saif-typespec-package`     | `{project_id}`    | `{project_id}-pr`    | `package-typespec-npm.yml` / `package-typespec-npm-pr.yml`   |
| `saif-terraform-module`     | `{project_id}`    | `{project_id}-pr`    | `package-terraform.yml` / `package-terraform-pr.yml`         |
| `saif-dynatrace-config`     | `{project_id}`    | `{project_id}-pr`    | `dynatrace-terraform.yml` / `dynatrace-terraform-pr.yml`     |
| `saif-azure-terraform-team` | `Azure.Terraform` | `Azure.Terraform-pr` | `azure-terraform-infrastructure-owner-v2.yml` / `-pr-v2.yml` |

### Business roles (`saif-business-roles`)

| Pipeline              | Azure DevOps name | Trigger                                                                            | Shared template                                                             |
| --------------------- | ----------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
| `azure-pipelines.yml` | `{project_id}`    | CI on `main` — `infra/entra/business-roles.yml` or `infra/okta/business-roles.yml` | `entra-terraform-business-roles.yml` or `okta-terraform-business-roles.yml` |

No PR pipeline. The provider-specific path and template are chosen at scaffold time based on
whether the application is corp or external.

### Test tools (`saif-test-tools`)

| Pipeline                           | Azure DevOps name                  | Trigger                                | Shared template                          |
| ---------------------------------- | ---------------------------------- | -------------------------------------- | ---------------------------------------- |
| `gen-token-pipeline.yml`           | `{project_id}-gen-np-jwt`          | Manual (branch include list is `none`) | `okta-testing-gen-np-accesstoken.yml`    |
| `np-roles-pipeline.yml`            | `{project_id}-np-roles`            | CI on `main`                           | `okta-terraform-np-roles-user.yml`       |
| `external-test-users-pipeline.yml` | `{project_id}-external-test-users` | Manual                                 | `okta-terraform-external-test-users.yml` |

See [Create Test Tools Repository](../guides/security/testing/create-test-tools-repository.md).

---

## ⚙️ Generating pipelines with Aspire publish

`aspire publish` can generate the `.azdo` files from your AppHost instead of you maintaining
them by hand. It writes to the same paths, overwriting in place, and derives the file set from
the resources registered in `AppHost.cs`.

| Registered resource | Generates                                                                |
| ------------------- | ------------------------------------------------------------------------ |
| API service         | `azure-pipelines-api.yml`, `azure-pipelines-api-pr.yml`, `vars/api.yml`   |
| Security            | `azure-pipelines-auth.yml`, `azure-pipelines-auth-pr.yml`, `vars/auth.yml` |
| Frontend            | `azure-pipelines-web.yml`, `azure-pipelines-web-pr.yml`, `vars/web.yml`   |
| Subscription        | `azure-pipelines-sub.yml`, `azure-pipelines-sub-pr.yml`, `vars/sub.yml`   |
| Event service       | `azure-pipelines-event-service.yml`, `azure-pipelines-event-service-pr.yml`, `vars/event-service.yml` |

`vars/base.yml` is always generated.

!!! warning "Event service filenames differ from the template"
    The `saif-event-service` template ships `azure-pipelines.yml` and `azure-pipelines-pr.yml`.
    Aspire publish generates `azure-pipelines-event-service.yml` and
    `azure-pipelines-event-service-pr.yml`. In a repository scaffolded from the template,
    publishing adds new files rather than overwriting the existing ones — reconcile them and
    update the pipeline definition paths in Azure DevOps before deleting either set.

Aspire publish does not generate docs, Dependabot, or PR slot cleanup pipelines. See
[Aspire Publish](../guides/development/aspire-publish.md).

---

## 🔧 Variable files

Repository YAML pulls variables from local `vars/` templates and from the shared templates
repository.

### Local variable files

| File                    | Defines                                                                   | Used by                    |
| ----------------------- | -------------------------------------------------------------------------- | -------------------------- |
| `.azdo/vars/base.yml`   | `ApplicationName`, `BusinessDomain`, `ProjectId`, `Owner`, `Tenant`        | All pipelines in the repo  |
| `.azdo/vars/api.yml`    | `ApplicationType`, `ApiType`, `OpenAPIFileName`, version components        | API pipelines              |
| `.azdo/vars/web.yml`    | `FrontEndProjectId`, version components                                    | Web pipelines              |
| `.azdo/vars/auth.yml`   | Version components for auth releases                                       | Auth pipelines             |
| `.azdo/vars/sub.yml`    | `FunctionAppProjectId`, version components                                 | Subscription pipelines     |
| `.azdo/vars/docs.yml`   | `projectId`, `documentationSourceDirectory`, `outputDirectory`, `siteType`, `containerFolderName` | Docs pipelines |
| `.azdo/vars/event-service.yml` | `openApiFileName`, `secretsVariableGroupName`, date-based version components | Event service pipelines    |
| `.azdo/vars.yml`        | `ApplicationName`, `Owner`, `ApplicationType`, `ProjectId`, `BusinessDomain`, version components | Package and config repos (single-file variant) |

### Shared variable templates

| Template                             | Provides                                              |
| ------------------------------------ | ----------------------------------------------------- |
| `saif-vars.yml@templates`            | Platform-wide values                                  |
| `Variables/environments.yml@templates` | Environment-to-subscription mapping (deploy pipelines only) |
| `Variables/versioning.yml@templates`  | `MajorVersion`, `MinorVersion`, `PatchVersion`, pre-release separator |

Environment mapping lives in the shared templates repository — there is no local
`environments.yml`. See [Environments](environments.md).

### Variable groups

| Group            | Purpose                                  |
| ---------------- | ---------------------------------------- |
| `TerraformCloud` | Terraform Cloud credentials              |
| `Dynatrace`      | Observability configuration              |
| `{project_id}`   | Per-project secrets (Oracle-backed APIs) |

---

## ❓ Why didn't my pipeline run?

| Symptom                                    | Cause                                                                                            |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| No PR validation appeared                  | Changed paths do not match any `pathFilter`, or no `validations` entry exists                    |
| Auth validation ran instead of API         | The change touched `infra/auth`, which the API filter excludes                                   |
| API deploy did not run after merge         | Changed paths fall inside the trigger's `exclude` list                                           |
| Both API and auth validations ran          | Expected — the PR touches both scopes and needs both reviewer groups                             |
| Auth changes merged but nothing deployed   | `infra/auth` is excluded from the API CI trigger — run `{project_id}-api` or `{project_id}-auth` |
| Auth stages skipped inside the API run     | The pipeline detected no auth configuration changes                                              |
| Docs pipeline did not publish              | `azure-docs.yml` is manual by design                                                             |
| No pipeline exists for a feature you added | Database, webhook, and base features contribute no pipelines                                     |

---

## 📚 Resources

- [Pipeline Troubleshooting](../guides/development/pipeline-troubleshooting.md) - Diagnose failing pipeline runs
- [PR Slot Deployments](../guides/development/pr-slot-deployments.md) - How PR slots are created and cleaned up
- [Documentation Site](../guides/development/documentation-site.md) - Publishing a docs site
- [Aspire Publish](../guides/development/aspire-publish.md) - Generate pipeline YAML from your AppHost
- [Project Templates](../about/contributing/project-templates.md) - How service and feature templates compose
- [Event Subscription](../guides/development/eventing/event-subscription.md) - Adding subscription pipelines to an existing repository
- [Environments](environments.md) - Environment configurations and approvers
- [Deployment Workflow](../guides/security/configuration/deployment-workflow.md) - Auth stages inside the API pipeline and correct deployment order
- [Azure DevOps Services](tools/azure-devops-services.md) - Platform overview
- [SAIF pipeline-templates](https://dev.azure.com/SAIFCorporation/SAIF/_git/pipeline-templates) - Shared template repository
