Dotnet Ado Patterns logo

Dotnet Ado Patterns

Community
wshaddix
dotnet-ado-patterns

Designing composable Azure DevOps YAML pipelines. Templates, variable groups, multi-stage, triggers.

Overview

Publisherwshaddix
Repositorydotnet-skills
Skill namedotnet-ado-patterns
Stars
79
Forks
13
Bundled files
Instructions only
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by wshaddix on GitHub. Read the source before you install it.

Installation

Install the Dotnet Ado Patterns AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/wshaddix/dotnet-skills.git /tmp/dotnet-skills
mkdir -p .claude/skills
cp -r /tmp/dotnet-skills/skills/dotnet-ado-patterns .claude/skills/dotnet-ado-patterns
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Dotnet Ado Patterns in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Dotnet Ado Patterns on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Dotnet Ado Patterns is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

dotnet-ado-patterns

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.


Template References

Stage Templates

Stage templates define reusable pipeline stages that callers insert into their multi-stage pipeline:

yaml
# 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'

Calling a Stage Template

yaml
# 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

Job templates encapsulate a complete job with its pool and steps:

yaml
# 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

Step templates define reusable step sequences inserted into an existing job:

yaml
# 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 }}

Using Step Templates in a Pipeline

yaml
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'

Extends Templates (Enforced Pipeline Structure)

The extends keyword enforces a required pipeline structure defined by an organization template. Callers cannot bypass the structure:

yaml
# 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"
yaml
# 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 and Variable Templates

Variable Groups

Variable groups centralize configuration shared across multiple pipelines. Link them from Azure Pipelines Library:

yaml
variables:
  - group: 'dotnet-build-settings'
  - group: 'nuget-feed-credentials'
  - name: buildConfiguration
    value: 'Release'

Variable Templates

Variable templates define reusable variable sets in YAML files:

yaml
# templates/variables/dotnet-defaults.yml
variables:
  dotnetVersion: '8.0.x'
  buildConfiguration: 'Release'
  testResultsDirectory: '$(Build.ArtifactStagingDirectory)/test-results'
  coverageDirectory: '$(Build.ArtifactStagingDirectory)/coverage'
yaml
# azure-pipelines.yml
variables:
  - template: templates/variables/dotnet-defaults.yml
  - name: projectPath
    value: 'MyApp.sln'

Variable Group with Key Vault Integration

Link variable groups to Azure Key Vault for secret management. Secrets are fetched at pipeline runtime:

yaml
# 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

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.


Conditional Insertion

${{ if }} Expressions

yaml
parameters:
  - 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: 'release-managers@example.com'
                instructions: 'Approve production deployment'

${{ each }} Iteration

yaml
parameters:
  - 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 }}"

Conditional Step Insertion Within Templates

yaml
# 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'

Multi-Stage Pipelines

Build, Test, Deploy Pattern

yaml
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"

Stage Dependencies and Conditions

yaml
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"

Pipeline Triggers

CI Triggers

yaml
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 Triggers

yaml
pr:
  branches:
    include:
      - main
      - release/*
  paths:
    include:
      - src/**
      - tests/**
    exclude:
      - docs/**
  drafts: false  # do not trigger on draft PRs

Scheduled Triggers

yaml
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

Pipeline Resource Triggers

Trigger a pipeline when another pipeline completes:

yaml
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"

Agent Gotchas

  1. Template parameter types are enforced at compile time -- passing a string where type: boolean is expected causes a validation error before the pipeline runs; always match types exactly.
  2. extends templates cannot be overridden -- callers cannot inject steps before or after the mandatory stages; this is by design for policy enforcement.
  3. Variable group secrets are not available in template expressions -- ${{ variables.mySecret }} resolves at compile time when secrets are not yet available; use $(mySecret) runtime syntax instead.
  4. ${{ each }} iterates at compile time -- the loop generates YAML before the pipeline runs; runtime variables cannot be used as the iteration source.
  5. CI and PR triggers are mutually exclusive with trigger: none and pr: none -- omitting both trigger and pr sections enables default CI triggering on all branches; explicitly set trigger: none to disable.
  6. Path filters in triggers use repository root-relative paths -- do not prefix paths with / or ./; use src/** not ./src/**.
  7. Scheduled triggers always run on the default branch first -- the branches.include filter applies after the schedule fires; the schedule itself is only evaluated from the default branch YAML.
  8. Pipeline resource triggers require the source pipeline name, not the YAML file path -- use the pipeline name as shown in ADO, not the azure-pipelines.yml file path.

Frequently asked questions

What does the Dotnet Ado Patterns AI skill do?

Designing composable Azure DevOps YAML pipelines. Templates, variable groups, multi-stage, triggers.

Why use Dotnet Ado Patterns on TypingMind?

Because you install it once and use it with any model. Dotnet Ado Patterns is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Dotnet Ado Patterns in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/wshaddix/dotnet-skills/tree/master/skills/dotnet-ado-patterns. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Dotnet Ado Patterns?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Dotnet Ado Patterns?

As many as you like. As long as a model supports skills, you can use Dotnet Ado Patterns with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Dotnet Ado Patterns AI skill free?

It is published on GitHub by wshaddix. Check the repository for licensing terms. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇