.cursor/skills/dotnet-ado-patterns/SKILL.md
Designing composable Azure DevOps YAML pipelines. Templates, variable groups, multi-stage, triggers.
npx skillsauth add AGIBuild/Fulora dotnet-ado-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Composable Azure DevOps YAML pipeline patterns for .NET projects: template references with extends, stages, jobs, and steps keywords for hierarchical pipeline composition, variable groups and variable templates for centralized configuration, pipeline decorators for organization-wide policy injection, conditional insertion with ${{ if }} and ${{ each }} expressions, multi-stage pipelines (build, test, deploy), and pipeline triggers for CI, PR, and scheduled runs.
Version assumptions: Azure Pipelines YAML schema. DotNetCoreCLI@2 task for .NET 8/9/10 builds. Template expressions syntax v2.
Scope boundary: This skill owns composable pipeline design patterns for Azure DevOps YAML. Starter CI templates (basic build/test/pack) are owned by [skill:dotnet-add-ci] -- this skill extends those templates with advanced composition. CLI-specific release pipelines (build-package-release for CLI binaries) are owned by [skill:dotnet-cli-release-pipeline] -- this skill covers general pipeline patterns that CLI pipelines consume. ADO-unique features (environments with approvals, service connections, classic releases) are in [skill:dotnet-ado-unique].
Out of scope: Starter CI templates -- see [skill:dotnet-add-ci]. CLI release pipelines (tag-triggered build-package-release for CLI tools) -- see [skill:dotnet-cli-release-pipeline]. ADO-unique features (environments, service connections, classic releases) -- see [skill:dotnet-ado-unique]. Build/test specifics -- see [skill:dotnet-ado-build-test]. Publishing pipelines -- see [skill:dotnet-ado-publish]. GitHub Actions workflow patterns -- see [skill:dotnet-gha-patterns].
Cross-references: [skill:dotnet-add-ci] for starter templates that these patterns extend, [skill:dotnet-cli-release-pipeline] for CLI-specific release automation.
Stage templates define reusable pipeline stages that callers insert into their multi-stage pipeline:
# templates/stages/build-test.yml
parameters:
- name: dotnetVersion
type: string
default: '8.0.x'
- name: buildConfiguration
type: string
default: 'Release'
- name: projects
type: string
default: '**/*.sln'
stages:
- stage: Build
displayName: 'Build and Test'
jobs:
- job: BuildJob
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UseDotNet@2
displayName: 'Install .NET SDK'
inputs:
packageType: 'sdk'
version: ${{ parameters.dotnetVersion }}
- task: DotNetCoreCLI@2
displayName: 'Restore'
inputs:
command: 'restore'
projects: ${{ parameters.projects }}
- task: DotNetCoreCLI@2
displayName: 'Build'
inputs:
command: 'build'
projects: ${{ parameters.projects }}
arguments: '-c ${{ parameters.buildConfiguration }} --no-restore'
# azure-pipelines.yml
trigger:
branches:
include:
- main
stages:
- template: templates/stages/build-test.yml
parameters:
dotnetVersion: '9.0.x'
buildConfiguration: 'Release'
projects: 'MyApp.sln'
- template: templates/stages/deploy.yml
parameters:
environment: 'staging'
Job templates encapsulate a complete job with its pool and steps:
# templates/jobs/dotnet-build.yml
parameters:
- name: dotnetVersion
type: string
default: '8.0.x'
- name: projects
type: string
jobs:
- job: Build
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UseDotNet@2
inputs:
packageType: 'sdk'
version: ${{ parameters.dotnetVersion }}
- task: DotNetCoreCLI@2
displayName: 'Build'
inputs:
command: 'build'
projects: ${{ parameters.projects }}
arguments: '-c Release'
Step templates define reusable step sequences inserted into an existing job:
# templates/steps/dotnet-setup.yml
parameters:
- name: dotnetVersion
type: string
default: '8.0.x'
- name: nugetFeed
type: string
default: ''
steps:
- task: UseDotNet@2
displayName: 'Install .NET SDK ${{ parameters.dotnetVersion }}'
inputs:
packageType: 'sdk'
version: ${{ parameters.dotnetVersion }}
- ${{ if ne(parameters.nugetFeed, '') }}:
- task: NuGetAuthenticate@1
displayName: 'Authenticate NuGet feed'
- task: DotNetCoreCLI@2
displayName: 'Restore packages'
inputs:
command: 'restore'
projects: '**/*.sln'
${{ if ne(parameters.nugetFeed, '') }}:
feedsToUse: 'select'
vstsFeed: ${{ parameters.nugetFeed }}
jobs:
- job: Build
pool:
vmImage: 'ubuntu-latest'
steps:
- checkout: self
- template: templates/steps/dotnet-setup.yml
parameters:
dotnetVersion: '9.0.x'
nugetFeed: 'MyOrg/MyFeed'
- task: DotNetCoreCLI@2
displayName: 'Build'
inputs:
command: 'build'
arguments: '-c Release --no-restore'
The extends keyword enforces a required pipeline structure defined by an organization template. Callers cannot bypass the structure:
# templates/pipeline-policy.yml
parameters:
- name: stages
type: stageList
default: []
stages:
- stage: SecurityScan
displayName: 'Security Scan (Required)'
jobs:
- job: Scan
pool:
vmImage: 'ubuntu-latest'
steps:
- script: echo "Running mandatory security scan"
- ${{ each stage in parameters.stages }}:
- ${{ stage }}
- stage: Compliance
displayName: 'Compliance Check (Required)'
dependsOn:
- ${{ each stage in parameters.stages }}:
- ${{ stage.stage }}
jobs:
- job: Check
pool:
vmImage: 'ubuntu-latest'
steps:
- script: echo "Running compliance checks"
# azure-pipelines.yml (caller)
extends:
template: templates/pipeline-policy.yml
parameters:
stages:
- stage: Build
jobs:
- job: BuildApp
pool:
vmImage: 'ubuntu-latest'
steps:
- script: dotnet build -c Release
The extends template wraps caller-defined stages with mandatory security and compliance stages that cannot be removed.
Variable groups centralize configuration shared across multiple pipelines. Link them from Azure Pipelines Library:
variables:
- group: 'dotnet-build-settings'
- group: 'nuget-feed-credentials'
- name: buildConfiguration
value: 'Release'
Variable templates define reusable variable sets in YAML files:
# templates/variables/dotnet-defaults.yml
variables:
dotnetVersion: '8.0.x'
buildConfiguration: 'Release'
testResultsDirectory: '$(Build.ArtifactStagingDirectory)/test-results'
coverageDirectory: '$(Build.ArtifactStagingDirectory)/coverage'
# azure-pipelines.yml
variables:
- template: templates/variables/dotnet-defaults.yml
- name: projectPath
value: 'MyApp.sln'
Link variable groups to Azure Key Vault for secret management. Secrets are fetched at pipeline runtime:
# Reference in pipeline
variables:
- group: 'kv-production-secrets' # linked to Azure Key Vault
- name: nonSecretVar
value: 'some-value'
steps:
- script: |
echo "Using secret from Key Vault"
# $(sql-connection-string) resolves at runtime from Key Vault
env:
CONNECTION_STRING: $(sql-connection-string)
Key Vault-linked variable groups require a service connection with Key Vault access. Secret names in Key Vault map to variable names (hyphens become valid variable characters).
Pipeline decorators inject steps into every pipeline in an organization or project, enforcing policies without modifying individual pipeline files. Decorators are an ADO-exclusive feature with no GitHub Actions equivalent -- see [skill:dotnet-ado-unique] for implementation details including extension manifests, deployment guidance, and use case examples.
${{ if }} Expressionsparameters:
- name: runIntegrationTests
type: boolean
default: false
- name: targetEnvironment
type: string
default: 'development'
values:
- development
- staging
- production
stages:
- stage: Build
jobs:
- job: BuildJob
steps:
- script: dotnet build -c Release
- ${{ if eq(parameters.runIntegrationTests, true) }}:
- stage: IntegrationTests
dependsOn: Build
jobs:
- job: IntegrationTestJob
steps:
- script: dotnet test --filter Category=Integration
- ${{ if eq(parameters.targetEnvironment, 'production') }}:
- stage: ApprovalGate
dependsOn: Build
jobs:
- job: WaitForApproval
pool: server
steps:
- task: ManualValidation@0
inputs:
notifyUsers: '[email protected]'
instructions: 'Approve production deployment'
${{ each }} Iterationparameters:
- name: environments
type: object
default:
- name: development
pool: 'ubuntu-latest'
approvals: false
- name: staging
pool: 'ubuntu-latest'
approvals: true
- name: production
pool: 'ubuntu-latest'
approvals: true
stages:
- stage: Build
jobs:
- job: BuildJob
steps:
- script: dotnet build -c Release
- ${{ each env in parameters.environments }}:
- stage: Deploy_${{ env.name }}
displayName: 'Deploy to ${{ env.name }}'
dependsOn: Build
jobs:
- ${{ if eq(env.approvals, true) }}:
- job: Approve
pool: server
steps:
- task: ManualValidation@0
inputs:
instructions: 'Approve deployment to ${{ env.name }}'
- deployment: DeployApp
pool:
vmImage: ${{ env.pool }}
environment: ${{ env.name }}
strategy:
runOnce:
deploy:
steps:
- script: echo "Deploying to ${{ env.name }}"
# templates/steps/dotnet-test.yml
parameters:
- name: collectCoverage
type: boolean
default: false
steps:
- task: DotNetCoreCLI@2
displayName: 'Run tests'
inputs:
command: 'test'
projects: '**/*Tests.csproj'
${{ if eq(parameters.collectCoverage, true) }}:
arguments: '-c Release --collect:"XPlat Code Coverage"'
${{ else }}:
arguments: '-c Release'
- ${{ if eq(parameters.collectCoverage, true) }}:
- task: PublishCodeCoverageResults@2
displayName: 'Publish coverage'
inputs:
summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml'
trigger:
branches:
include:
- main
- release/*
stages:
- stage: Build
displayName: 'Build'
jobs:
- job: BuildJob
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UseDotNet@2
inputs:
packageType: 'sdk'
version: '8.0.x'
- task: DotNetCoreCLI@2
displayName: 'Build'
inputs:
command: 'build'
projects: 'MyApp.sln'
arguments: '-c Release'
- task: DotNetCoreCLI@2
displayName: 'Publish'
inputs:
command: 'publish'
projects: 'src/MyApp/MyApp.csproj'
arguments: '-c Release -o $(Build.ArtifactStagingDirectory)/app'
- task: PublishPipelineArtifact@1
displayName: 'Upload artifact'
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)/app'
artifactName: 'app'
- stage: Test
displayName: 'Test'
dependsOn: Build
jobs:
- job: UnitTests
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UseDotNet@2
inputs:
packageType: 'sdk'
version: '8.0.x'
- task: DotNetCoreCLI@2
displayName: 'Run tests'
inputs:
command: 'test'
projects: '**/*Tests.csproj'
arguments: '-c Release --logger "trx;LogFileName=results.trx"'
- task: PublishTestResults@2
displayName: 'Publish test results'
condition: always()
inputs:
testResultsFormat: 'VSTest'
testResultsFiles: '**/results.trx'
- stage: DeployStaging
displayName: 'Deploy to Staging'
dependsOn: Test
jobs:
- deployment: DeployStaging
pool:
vmImage: 'ubuntu-latest'
environment: 'staging'
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: app
- script: echo "Deploying to staging"
- stage: DeployProduction
displayName: 'Deploy to Production'
dependsOn: DeployStaging
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployProduction
pool:
vmImage: 'ubuntu-latest'
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: app
- script: echo "Deploying to production"
stages:
- stage: Build
jobs:
- job: BuildJob
steps:
- script: dotnet build -c Release
- stage: UnitTests
dependsOn: Build
jobs:
- job: UnitTestJob
steps:
- script: dotnet test --filter Category!=Integration
- stage: IntegrationTests
dependsOn: Build
jobs:
- job: IntegrationTestJob
steps:
- script: dotnet test --filter Category=Integration
# Deploy only if BOTH test stages succeed
- stage: Deploy
dependsOn:
- UnitTests
- IntegrationTests
condition: and(succeeded('UnitTests'), succeeded('IntegrationTests'))
jobs:
- deployment: DeployApp
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- script: echo "Deploying"
trigger:
branches:
include:
- main
- release/*
exclude:
- feature/experimental/*
paths:
include:
- src/**
- tests/**
- '*.sln'
- Directory.Build.props
- Directory.Packages.props
exclude:
- docs/**
- '*.md'
tags:
include:
- 'v*'
pr:
branches:
include:
- main
- release/*
paths:
include:
- src/**
- tests/**
exclude:
- docs/**
drafts: false # do not trigger on draft PRs
schedules:
- cron: '0 6 * * 1-5'
displayName: 'Weekday nightly build'
branches:
include:
- main
always: false # only run if there are changes since last run
- cron: '0 0 * * 0'
displayName: 'Weekly full validation'
branches:
include:
- main
always: true # run even without changes
Trigger a pipeline when another pipeline completes:
resources:
pipelines:
- pipeline: buildPipeline
source: 'MyApp-Build'
trigger:
branches:
include:
- main
stages:
- stage: DeployAfterBuild
jobs:
- deployment: Deploy
environment: 'staging'
strategy:
runOnce:
deploy:
steps:
- download: buildPipeline
artifact: app
- script: echo "Deploying build from upstream pipeline"
type: boolean is expected causes a validation error before the pipeline runs; always match types exactly.extends templates cannot be overridden -- callers cannot inject steps before or after the mandatory stages; this is by design for policy enforcement.${{ variables.mySecret }} resolves at compile time when secrets are not yet available; use $(mySecret) runtime syntax instead.${{ each }} iterates at compile time -- the loop generates YAML before the pipeline runs; runtime variables cannot be used as the iteration source.trigger: none and pr: none -- omitting both trigger and pr sections enables default CI triggering on all branches; explicitly set trigger: none to disable./ or ./; use src/** not ./src/**.branches.include filter applies after the schedule fires; the schedule itself is only evaluated from the default branch YAML.azure-pipelines.yml file path.tools
Captures learnings, errors, and corrections to enable continuous improvement. Use when: (1) A command or operation fails unexpectedly, (2) User corrects Claude ('No, that's wrong...', 'Actually...'), (3) User requests a capability that doesn't exist, (4) An external API or tool fails, (5) Claude realizes its knowledge is outdated or incorrect, (6) A better approach is discovered for a recurring task. Also review learnings before major tasks.
testing
Security headers configuration and best practices for ASP.NET Core Razor Pages applications. Covers CSP, HSTS, X-Frame-Options, and comprehensive security middleware setup. Use when configuring security headers in ASP.NET Core applications, implementing Content Security Policy (CSP), or setting up HSTS and other security-related HTTP headers.
development
Reviews designs and business goals for security vulnerabilities, data protection (in transit/at rest), authorization, and compliance alignment. Use when the user asks for a security review, threat modeling, attack surface analysis, data leakage prevention, or compliance/security assessment.
development
Best practices for building production-grade ASP.NET Core Razor Pages applications. Focuses on structure, lifecycle, binding, validation, security, and maintainability in web apps using Razor Pages as the primary UI framework. Use when building Razor Pages applications, designing PageModels and handlers, implementing model binding and validation, or securing Razor Pages with authentication and authorization.