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.
π§ 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 |
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.
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 |
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:
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:
{
"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:
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:
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:
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.
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 |
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.
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.
βοΈ 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.
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.
π§ 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.
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 - Diagnose failing pipeline runs
- PR Slot Deployments - How PR slots are created and cleaned up
- Documentation Site - Publishing a docs site
- Aspire Publish - Generate pipeline YAML from your AppHost
- Project Templates - How service and feature templates compose
- Event Subscription - Adding subscription pipelines to an existing repository
- Environments - Environment configurations and approvers
- Deployment Workflow - Auth stages inside the API pipeline and correct deployment order
- Azure DevOps Services - Platform overview
- SAIF pipeline-templates - Shared template repository