CI/CD Pipeline for .NET with GitHub Actions

Khimananda Oli 8 min read Programming and Languages
CI/CD Pipeline for .NET with GitHub Actions

By Khimananda Oli | Last reviewed: August 2026

Shipping .NET applications reliably requires automating the path from commit to production without manual intervention or fragile scripts. A properly configured CI/CD pipeline for .NET with GitHub Actions handles restoration, testing, building, and deployment in a single YAML workflow that lives alongside your code. This guide walks through constructing a production-grade workflow that integrates security scanning, containerization, and passwordless Azure deployment.

How do you structure a CI/CD pipeline for .NET with GitHub Actions?

The foundation of any reliable automation is a predictable directory structure and a workflow that separates concerns. In practice, I keep workflows in .github/workflows/ with descriptive names like dotnet-ci.yml and dotnet-deploy.yml. Separating continuous integration from deployment allows you to run fast feedback loops on every PR while restricting production deployments to protected branches.

Git Push / PRTrigger EventRestore & Testdotnet testSecurity ScanBuild & PackDocker BuildPush ArtifactDeploy (OIDC)Azure LoginApp Service / AKSProductionLive Traffic
High-level architecture of a CI/CD pipeline for .NET with GitHub Actions showing four distinct stages

Your workflow should always pin specific SDK versions rather than relying on latest. Reproducibility matters more than convenience when debugging failed builds at 2 AM. For teams managing multiple services, consider reading about GitHub Actions reusable workflows and matrix builds to avoid duplicating logic across repositories.

Defining triggers and environment variables

Start by declaring precise triggers. Running on every push to main and every pull request targeting main covers most scenarios. Use paths-ignore to skip runs for documentation-only changes. Define environment variables at the workflow level for values like the .NET version and project path, making future upgrades a single-line change.

name: .NET CI/CD Pipeline
on:
  push:
    branches: [ main ]
    paths-ignore: [ '.md', 'docs/' ]
  pull_request:
    branches: [ main ]

env:
  DOTNET_VERSION: '9.0.x'
  PROJECT_PATH: './src/MyApp.Web'
  IMAGE_NAME: 'ghcr.io/${{ github.repository }}'

How do you restore, test, and build .NET projects in GitHub Actions?

The core CI job must be fast and deterministic. NuGet package restoration is often the bottleneck; caching reduces restore times from minutes to seconds. The actions/setup-dotnet action handles SDK installation and includes built-in caching when you specify cache: 'nuget'. Always run dotnet restore explicitly before building so cache hits are visible in logs.

Testing deserves special attention. Run dotnet test with the --no-restore flag since you already restored in the previous step. Add --logger trx to generate test result files and use dorny/test-reporter to publish results directly in the PR check. This visibility prevents merging broken code. If your application depends on databases, explore integration testing in CI pipelines using service containers.

Setup .NET SDKCache NuGet Packagesdotnet restoreHydrate from Cachedotnet testGenerate TRX Reportdotnet publishOutput: ./publishUpload ArtifactRetain for Deploy JobTrivy Security ScanFail on High/Critical
Sequential build steps including caching, testing, publishing, and security scanning within the CI/CD pipeline for .NET with GitHub Actions

Publishing and uploading artifacts

After tests pass, publish the application as a self-contained unit. Use dotnet publish -c Release --no-restore -o ./publish to create a deployment-ready folder. Upload this directory using actions/upload-artifact@v4. Artifacts bridge the gap between your CI and CD jobs without rebuilding. Set a retention period of 1–3 days to manage storage costs; old artifacts rarely help debugging.

How do you containerize .NET apps securely in GitHub Actions?

Containers provide consistency between local development and cloud infrastructure. Multi-stage Dockerfiles are non-negotiable for .NET; they reduce final image size by excluding the SDK. Your build stage uses mcr.microsoft.com/dotnet/sdk:9.0, while the runtime stage uses mcr.microsoft.com/dotnet/aspnet:9.0-alpine. Alpine-based images minimize attack surface and improve pull times.

Never store Docker credentials as repository secrets if deploying to Azure or AWS. Instead, leverage OpenID Connect (OIDC). Configure a federated credential in your cloud provider and use azure/login@v2 with client-id, tenant-id, and subscription-id. This eliminates long-lived secrets entirely. For teams handling sensitive data, review handling secrets in CI/CD pipelines safely before proceeding.

- name: Log in to Azure via OIDC
  uses: azure/login@v2
  with:
    client-id: ${{ secrets.AZURE_CLIENT_ID }}
    tenant-id: ${{ secrets.AZURE_TENANT_ID }}
    subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- name: Login to ACR
  run: az acr login --name myregistry

- name: Build and push Docker image
  run: |
    docker build -t myregistry.azurecr.io/myapp:${{ github.sha }} .
    docker push myregistry.azurecr.io/myapp:${{ github.sha }}

Scanning images before deployment

Integrate aquasecurity/trivy-action immediately after building. Configure it to fail on HIGH and CRITICAL vulnerabilities. Shift-left security catches issues before they reach production. Generate an SBOM (Software Bill of Materials) alongside your scan for compliance audits. This evidence collection aligns with SOC 2 requirements and demonstrates due diligence during reviews.

How do you deploy .NET to Azure App Service or AKS from GitHub Actions?

Deployment strategy depends on your hosting target. Azure App Service suits most web APIs and MVC applications with minimal operational overhead. Azure Kubernetes Service (AKS) fits microservices architectures requiring fine-grained scaling. Both support OIDC authentication through the same azure/login action used earlier.

CriteriaAzure App ServiceAzure Kubernetes Service
Setup ComplexityLow — managed PaaSHigh — cluster management required
Scaling ModelInstance count / auto-scale rulesHPA + Cluster Autoscaler
Deployment Actionazure/webapps-deployazure/k8s-deploy or Helm
Best ForMonoliths, APIs, small teamsMicroservices, high-scale platforms
Cost BaselinePredictable per-instance pricingHigher base cost + node pools
Built Docker ImageACR / GHCR RegistryAzure App Service Pathazure/webapps-deploy@v3Slot Swap → Production✓ Zero-downtime defaultAKS Deployment Pathazure/k8s-deploy@v1Rolling Update Strategy⚠ Requires HPA configWeb App LivePods Running in Cluster
Decision flow comparing App Service and AKS deployment targets within a CI/CD pipeline for .NET with GitHub Actions

Deploying to Azure App Service with slot swaps

Use deployment slots to achieve zero-downtime releases. Deploy to a staging slot first, run smoke tests against it, then swap into production. The azure/webapps-deploy@v3 action supports this natively via the slot-name parameter. Always configure health checks in App Service so the platform only routes traffic to healthy instances after swap.

Deploying to AKS with manifests or Helm

For Kubernetes, choose between raw manifests and Helm charts based on complexity. Simple deployments work fine with azure/k8s-deploy and baked manifests. Complex environments benefit from Helm templating. Reference deploying to AKS with Azure Pipelines for patterns that translate directly to GitHub Actions. Pin image tags to commit SHAs, never latest, to ensure traceability.

What are common mistakes when building .NET CI/CD pipelines?

Several anti-patterns consistently cause failures in production. Avoiding them saves hours of debugging and prevents security incidents.

  • Hardcoding secrets: Never embed connection strings or API keys in YAML. Use GitHub Environments with required reviewers for production deployments.
  • Skipping cache configuration: Uncached NuGet restores add 2–5 minutes per job. Enable caching in setup-dotnet or use actions/cache manually.
  • Running as root in containers: Add USER app in your Dockerfile. Root containers violate least-privilege principles and fail security scans.
  • Ignoring test failures: Configure workflows to fail fast. A green checkmark on a broken build erodes trust in the entire system.
  • Using mutable tags: Tag images with ${{ github.sha }} or semantic versions. Mutable tags like dev make rollbacks impossible.

Observability completes the pipeline. Without monitoring, you cannot verify deployments succeeded beyond HTTP 200 responses. Integrate OpenTelemetry early and correlate deployment events with metrics. Teams new to observability should start with OpenTelemetry as the observability standard to instrument .NET applications consistently.

Implementing Your CI/CD Pipeline for .NET with GitHub Actions

A well-architected CI/CD pipeline for .NET with GitHub Actions transforms deployment from a risky manual process into a repeatable, auditable workflow. Start with the CI foundation—restore, test, build, scan—before adding deployment stages. Adopt OIDC authentication from day one to eliminate secret sprawl. Containerize with multi-stage builds and enforce security gates before production. Whether targeting App Service or AKS, prioritize zero-downtime strategies and immutable artifacts.

If your team needs help designing or auditing .NET automation workflows, reach out to discuss your CI/CD requirements. I help organizations build pipelines that are secure, observable, and audit-ready from the first commit.

Frequently Asked Questions

Create a workflow YAML file in .github/workflows targeting dotnet-build action. Configure triggers for push or pull requests, specify the .NET SDK version, and define build, test, and publish steps using official Microsoft actions for consistent results.

GitHub-hosted runners support .NET 6 through .NET 9 as of 2026. Use setup-dotnet action to install specific SDK versions not preinstalled. Self-hosted runners allow any version you manually configure on your infrastructure.

Private repos get 2,000 free minutes monthly on standard plans. Minutes multiply by OS factor: Linux is 1x, Windows is 2x. Exceeding limits requires billing upgrades or switching to self-hosted runners for unlimited execution.

Yes, use actions/cache with path ~/.nuget/packages and key based on csproj hash.

Use azure/webapps-deploy action with publish profile credentials stored as repository secrets. Configure slot deployment for zero-downtime releases and add health check validation steps before swapping production slots automatically.

Add service containers for PostgreSQL or SQL Server directly in workflow YAML. Configure environment variables for connection strings, wait for container readiness using health checks, and run migrations before executing dotnet test commands against ephemeral test databases.

Store credentials as encrypted repository or organization secrets. Reference them via ${{ secrets.NAME }} syntax. Never hardcode values in YAML. Rotate secrets regularly and use environment-specific secrets for staging versus production isolation.

Configure dotnet pack and nuget push steps triggered on release tags. Store API key as repository secret. Add conditional logic to prevent accidental publishes from feature branches and validate package metadata before pushing to nuget.org feed.

Check SDK version mismatches between local machine and runner. Verify case-sensitive file paths on Linux runners. Ensure all dependencies restore correctly and environment variables match. Enable verbose logging with --verbosity diagnostic flag to identify discrepancies.

Split test projects across matrix strategy entries or use dotnet test --filter with partitioning. Run independent test suites concurrently on multiple runner instances. Aggregate results using dorny/test-reporter action for unified coverage reporting and faster feedback loops.

Specify container image in workflow job definition using official mcr.microsoft.com/dotnet/sdk images. This ensures identical build environments across runs and eliminates host dependency issues while maintaining full access to GitHub Actions features and caching.

Cache NuGet packages and build outputs using actions/cache. Enable incremental builds with --no-restore when appropriate. Use matrix strategies for parallel testing. Consider self-hosted runners with persistent storage to avoid repeated environment setup overhead.

Minimal by default. Grant explicit permissions in workflow YAML for packages write, contents read, or deployments write as needed. Avoid overly broad tokens. Use fine-grained personal access tokens for external service authentication instead of default token.

Enable debug logging by setting ACTIONS_RUNNER_DEBUG and ACTIONS_STEP_DEBUG secrets to true. Review step logs for exact error lines. Reproduce failures locally using act tool. Check runner OS differences and verify all environment variables are correctly passed.

TODO: write this answer during review — the model returned fewer than 15 FAQs.