| name | ci-cd |
| description | CI/CD pipelines for .NET applications. Covers GitHub Actions and Azure DevOps YAML pipelines with build, test, publish, and deploy stages. Load this skill when setting up continuous integration, automated testing, deployment workflows, or when the user mentions "CI/CD", "pipeline", "GitHub Actions", "Azure DevOps", "workflow", "deploy", "build pipeline", "publish", "NuGet push", "release", or "continuous integration".
|
CI/CD
Core Principles
- Pipeline as code — YAML pipelines committed to the repo. No click-ops in the UI.
- Fast feedback — Build and test on every push. Cache NuGet packages. Fail fast.
- Build once, deploy many — Build the artifact once, promote it through environments (dev → staging → production).
- Never skip tests — Tests gate the pipeline. No deployment without passing tests.
Patterns
GitHub Actions — Build + Test
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
DOTNET_VERSION: '10.0.x'
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
jobs:
build-and-test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17
env:
POSTGRES_DB: testdb
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Restore
run: dotnet restore
- name: Build
run: dotnet build --no-restore --configuration Release
- name: Format check
run: dotnet format --verify-no-changes --no-restore
- name: Test
run: dotnet test --no-build --configuration Release --logger trx --results-directory TestResults
env:
ConnectionStrings__Default: "Host=localhost;Database=testdb;Username=postgres;Password=postgres"
- name: Publish test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: TestResults/*.trx
GitHub Actions — Build + Publish Docker Image
name: Publish
on:
push:
tags: ['v*']
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract version from tag
id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:${{ steps.version.outputs.VERSION }}
ghcr.io/${{ github.repository }}:latest
Azure DevOps — Build + Test
Same restore → build → format → test flow as GitHub Actions. Key differences:
trigger:
branches:
include: [main]
paths:
exclude: ['*.md', docs/]
pool:
vmImage: 'ubuntu-latest'
variables:
dotnetVersion: '10.0.x'
NuGet Package Publishing
- name: Pack
run: dotnet pack src/MyLibrary -c Release -o ./nupkg --no-build
- name: Push to NuGet
run: dotnet nuget push ./nupkg/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json
Anti-patterns
Don't Build Different Artifacts per Environment
- script: dotnet publish -c Debug
- script: dotnet publish -c Release
- script: dotnet publish -c Release -o ./publish
Don't Skip Format Checks in CI
steps:
- run: dotnet build
- run: dotnet test
steps:
- run: dotnet build
- run: dotnet format --verify-no-changes
- run: dotnet test
Don't Hardcode Secrets in Pipelines
env:
DB_PASSWORD: "my-secret-password"
env:
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
Decision Guide
| Scenario | Recommendation |
|---|
| Open source project | GitHub Actions |
| Enterprise with Azure | Azure DevOps Pipelines |
| Docker deployment | Multi-stage build in CI, push to container registry |
| NuGet library | Build → Test → Pack → Push on tag |
| Database migrations | Run in CI test stage, script for production |
| Environment promotion | Same artifact, different configuration |