
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Shipping .NET applications reliably requires automating the path from commit to production without exposing credentials or skipping validation. CI/CD for ASP.NET Core with GitHub Actions provides a native, YAML-driven workflow that integrates directly with your repository and cloud provider identity systems. This guide walks through building a secure, containerized pipeline for .NET 9 that passes audits and scales with your team, avoiding common pitfalls like hardcoded secrets and unoptimized layer caching.
How do you structure a secure CI/CD pipeline for ASP.NET Core?
A production-grade pipeline separates concerns into distinct jobs: linting/building, testing, containerizing, and deploying. Never combine deployment logic with build steps; if tests fail, the artifact should never reach the registry. For teams managing sensitive data, understanding how to handle secrets in CI/CD pipelines safely is the first step before writing any workflow code.
In practice, I structure workflows with explicit dependency chains using the needs keyword. This ensures the deploy job only executes if both build and security scan jobs succeed. Always pin action versions to full SHA hashes rather than tags to prevent supply chain attacks—a critical requirement for SOC 2 compliance.
How do you configure OIDC authentication for Azure or AWS?
Stop storing long-lived cloud credentials as repository secrets. OpenID Connect (OIDC) allows GitHub Actions to request short-lived tokens directly from your cloud provider. This eliminates secret rotation overhead and reduces blast radius if a runner is compromised.
Configuring Azure Federated Credentials
- Create an Azure AD Application and add a federated credential issuer pointing to
https://token.actions.githubusercontent.com. - Set the subject identifier to
repo:YOUR_ORG/YOUR_REPO:ref:refs/heads/mainto restrict access to specific branches. - Assign the app a least-privilege role (e.g., "Website Contributor") on the target resource group.
- In your workflow, use
azure/login@v2withclient-id,tenant-id, andsubscription-id—nocredsparameter needed.
- name: Azure Login (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: Deploy to Azure Web App
uses: azure/webapps-deploy@v3
with:
app-name: 'aspnet-core-prod'
images: 'myregistry.azurecr.io/app:${{ github.sha }}' For teams comparing automation platforms, our analysis of GitHub Actions vs Azure Pipelines highlights that OIDC support is now mature on both, but GitHub’s per-repository scoping offers finer granularity for multi-team organizations.
How do you optimize Docker builds for .NET 9 applications?
Naive Dockerfiles rebuild dependencies on every code change, wasting minutes per commit. Multi-stage builds with layer caching are non-negotiable for .NET workloads. The key is separating dependency restoration from source compilation.
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build
WORKDIR /src
COPY ["MyApp.csproj", "."]
RUN dotnet restore --runtime linux-x64
COPY . .
RUN dotnet publish -c Release -o /app/publish --no-restore --self-contained false
# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS runtime
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApp.dll"] Always specify --runtime during restore to enable NuGet package caching for the target platform. Use Alpine-based images to reduce attack surface and image size. For deeper container fundamentals, see our Docker for beginners guide which covers these patterns across language ecosystems.
How do you implement testing and quality gates in GitHub Actions?
Automated tests without enforcement are just suggestions. Configure your workflow to fail fast on test failures and enforce coverage thresholds before allowing merges.
- Unit Tests: Run
dotnet test --collect:"XPlat Code Coverage"and upload results viadorny/test-reporter@v2for inline PR annotations. - Integration Tests: Use service containers for PostgreSQL or Redis. Never depend on external test databases.
- Security Scanning: Add
dotnet list package --vulnerableand Trivy image scanning as mandatory jobs. - Matrix Builds: Test against multiple .NET versions or OS targets simultaneously to catch platform-specific regressions early.
jobs:
test:
runs-on: ubuntu-24.04
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: testdb
POSTGRES_PASSWORD: testpass
ports: ['5432:5432']
options: --health-cmd pg_isready --health-interval 10s
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- run: dotnet test --logger trx --collect:"XPlat Code Coverage"
- uses: dorny/test-reporter@v2
if: always()
with:
name: .NET Tests
path: '**/*.trx'
reporter: dotnet-trx How does GitHub Actions compare to other CI tools for .NET?
Choosing the right tool depends on your existing ecosystem, compliance needs, and budget. While GitHub Actions dominates for open-source and Azure-native shops, regulated environments sometimes require self-hosted alternatives.
| Feature | GitHub Actions | Azure Pipelines | GitLab CI |
|---|---|---|---|
| .NET Native Support | Excellent (first-party actions) | Excellent (built-in tasks) | Good (community templates) |
| OIDC / Passwordless Auth | Azure, AWS, GCP native | Azure native, others via service connections | Azure/AWS/GCP via ID tokens |
| Self-Hosted Runners | Yes (VM, container, ARC) | Yes (scale sets, agents) | Yes (runner manager) |
| Free Tier (Public) | Unlimited minutes | 1 parallel job, 1800 min/mo | 400 compute min/mo |
| SOC 2 Audit Trail | Workflow logs + API | Pipeline audit logs + extensions | Compliance dashboard built-in |
| Best For | GitHub-native repos, OSS, Azure | Enterprise Azure shops, hybrid | Integrated DevSecOps platform |
What are common mistakes to avoid in .NET GitHub Actions workflows?
After auditing dozens of .NET pipelines, these issues appear repeatedly:
- Hardcoded connection strings in workflow files: Even if the repo is private, this violates every compliance framework. Use environment-scoped secrets or OIDC.
- Skipping vulnerability scans on base images: Your application code may be clean, but
mcr.microsoft.com/dotnet/aspnet:9.0could have CVEs. Scan the final image, not just source. - Not tagging images immutably: Using
:latestmakes rollbacks impossible. Always tag with${{ github.sha }}and optionally semantic version. - Running tests without isolation: Integration tests hitting shared staging databases cause flaky failures. Spin up ephemeral service containers per job.
- Ignoring runner architecture: Building on
ubuntu-24.04-armbut deploying to x64 hosts causes silent crashes. Match build and target architectures explicitly.
Next Steps for Production-Ready ASP.NET Core CI/CD
Implementing CI/CD for ASP.NET Core with GitHub Actions correctly means treating your pipeline as production infrastructure: version-controlled, tested, and secured. Start by migrating one service to OIDC authentication, enforce multi-stage Docker builds, and add Trivy scanning as a required status check. Monitor pipeline duration and failure rates as SLOs—if builds exceed 10 minutes or fail more than 5% of the time, investigate caching and test parallelization. When you're ready to harden your deployment strategy or need an audit-ready pipeline review, reach out to discuss your specific requirements.