All Blogs Tutorials 4 min read

GitHub Actions CI/CD Pipeline for ASP.NET Core: Real YAML Walkthrough

Sandeep Pal
June 3, 2026
GitHub Actions CI/CD Pipeline for ASP.NET Core: Real YAML Walkthrough

From "dotnet publish on my laptop" to repeatable pipelines

Most .NET teams know they need CI/CD; fewer have a workflow they trust when a intern merges on Friday afternoon. GitHub Actions is attractive because it lives next to your code, supports reusable workflows, and integrates cleanly with Azure, AWS, and container registries. This walkthrough mirrors a pipeline we use for ASP.NET Core Web APIs: restore, build, test, publish artifacts, deploy to staging, smoke test, swap to production.

You will copy YAML that works with .NET 8, adjust secrets once, and understand each job—not just green checkmarks.

Repository layout assumptions

src/
  MyApi/
    MyApi.csproj
  MyApi.Tests/
    MyApi.Tests.csproj
  MyApi.IntegrationTests/
    MyApi.IntegrationTests.csproj
Dockerfile
.github/workflows/ci-cd.yml

Integration tests use Testcontainers for SQL Server or the workflow service container below. Adjust paths to match your solution.

Complete workflow file (annotated sections)

name: CI/CD ASP.NET Core

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  DOTNET_VERSION: '8.0.x'
  AZURE_WEBAPP_NAME: myapi-prod
  AZURE_WEBAPP_PACKAGE_PATH: ./publish

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    services:
      sqlserver:
        image: mcr.microsoft.com/mssql/server:2022-latest
        env:
          ACCEPT_EULA: Y
          SA_PASSWORD: Your_strong_password123
        ports:
          - 1433:1433
        options: >-
          --health-cmd "/opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P Your_strong_password123 -Q 'SELECT 1'"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 10

    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ env.DOTNET_VERSION }}

      - name: Cache NuGet
        uses: actions/cache@v4
        with:
          path: ~/.nuget/packages
          key: nuget-${{ hashFiles('**/*.csproj') }}
          restore-keys: nuget-

      - name: Restore
        run: dotnet restore src/MyApi/MyApi.csproj

      - name: Build
        run: dotnet build src/MyApi/MyApi.csproj --configuration Release --no-restore

      - name: Unit tests
        run: dotnet test src/MyApi.Tests/MyApi.Tests.csproj --configuration Release --no-build --verbosity normal

      - name: Integration tests
        env:
          ConnectionStrings__Default: Server=localhost,1433;Database=MyApiTest;User Id=sa;Password=Your_strong_password123;TrustServerCertificate=True
        run: dotnet test src/MyApi.IntegrationTests/MyApi.IntegrationTests.csproj --configuration Release

      - name: Publish
        run: dotnet publish src/MyApi/MyApi.csproj -c Release -o ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} --no-restore

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: webapp
          path: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }}

  security-scan:
    runs-on: ubuntu-latest
    needs: build-and-test
    steps:
      - uses: actions/checkout@v4
      - name: Run Trivy filesystem scan
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: fs
          severity: HIGH,CRITICAL
          exit-code: 1

  deploy-staging:
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    needs: [build-and-test, security-scan]
    environment: staging
    steps:
      - name: Download artifact
        uses: actions/download-artifact@v4
        with:
          name: webapp
          path: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }}

      - name: Deploy to Azure Web App (staging slot)
        uses: azure/webapps-deploy@v3
        with:
          app-name: ${{ env.AZURE_WEBAPP_NAME }}
          slot-name: staging
          publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE_STAGING }}
          package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }}

      - name: Smoke test
        run: |
          curl -f https://${{ env.AZURE_WEBAPP_NAME }}-staging.azurewebsites.net/health || exit 1

  deploy-production:
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    needs: deploy-staging
    environment: production
    steps:
      - name: Swap staging to production
        uses: azure/CLI@v2
        with:
          inlineScript: |
            az webapp deployment slot swap \
              --resource-group my-rg \
              --name ${{ env.AZURE_WEBAPP_NAME }} \
              --slot staging \
              --target-slot production

Secrets and environments you must configure

  • AZURE_WEBAPP_PUBLISH_PROFILE_STAGING — download from Azure portal for the staging slot.
  • GitHub Environments staging and production with required reviewers on production.
  • Optional: AZURE_CREDENTIALS service principal JSON if using az CLI heavily or deploying to AKS.

Never commit publish profiles or connection strings. Use Key Vault references in App Service configuration populated separately.

Pull request workflows

On pull_request, run build-and-test and security-scan only—skip deploy jobs. Add a job commenting coverage from dotnet test --collect:"XPlat Code Coverage" if your team tracks thresholds. For fork PRs, restrict secrets with pull_request_target only when you understand the security implications; default safe pattern is tests without deployment secrets.

Docker alternative job

If you deploy containers to Azure Container Apps or ACR:

      - name: Login ACR
        uses: azure/docker-login@v2
        with:
          login-server: myregistry.azurecr.io
          username: ${{ secrets.ACR_USERNAME }}
          password: ${{ secrets.ACR_PASSWORD }}
      - name: Build and push
        run: |
          docker build -t myregistry.azurecr.io/myapi:${{ github.sha }} .
          docker push myregistry.azurecr.io/myapi:${{ github.sha }}

Speed tips that matter at scale

NuGet caching saves two to four minutes per run. --no-restore after restore avoids duplicate work. Split slow integration tests into a nightly workflow if they block hotfix deploys. Use concurrency groups to cancel obsolete runs on the same branch:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

AI in the pipeline (optional, bounded)

Some teams add a job that posts an AI-generated summary of EF migration diffs on PRs—useful for reviewers, not a substitute for human approval on schema changes. Keep API keys in org secrets; cap tokens; never send production connection strings to external models. Static analysis with built-in Roslyn analyzers and Trivy remains the compliance baseline.

Failure modes we see repeatedly

  • Tests pass locally but fail in CI because ASPNETCORE_ENVIRONMENT differs.
  • Publish profile expired; rotate and update secrets proactively.
  • Smoke test hits wrong hostname after custom domains.
  • Missing TrustServerCertificate in CI SQL connection strings.

A real GitHub Actions pipeline for ASP.NET Core is boring when it works: fast feedback on PRs, gated production, artifacts you can redeploy without rebuilding. Start with the YAML above, trim jobs you do not need yet, and add environments before your next release crunch—not after an outage.

1 views 0 likes 0 comments
Comments (0)
Sign in to leave a comment
Toolliyo Assistant
Ask about tutorials, ebooks, training, pricing, mentor services, and support. I use public site content only—not admin or internal tools.

care@toolliyo.com

Need callback? Share your details