.NET Academy

CI/CD with GitHub Actions

Automatically build, test and deploy every change with a GitHub Actions pipeline.

12 min read

You will learn

  • Describe a CI pipeline for a .NET solution
  • Write a GitHub Actions workflow that builds and tests
  • Publish an artifact or container image and deploy it
  • Protect the main branch with required checks

Automate the path to production

Manual releases are slow and error-prone ("did you run the tests?", "which config did you use?"). A CI/CD pipeline runs the same steps automatically on every change:

push / pull request
   └─▶ restore → build → test → (analyze) ─── CI
                                      └─▶ publish → deploy staging → smoke test → deploy production ─── CD

GitHub Actions runs pipelines defined as YAML files in .github/workflows/. Azure DevOps Pipelines and GitLab CI work the same way with different syntax.

A CI workflow for .NET

# .github/workflows/ci.yml
name: CI
 
on:
  push:
    branches: [main]
  pull_request:
 
jobs:
  build-and-test:
    runs-on: ubuntu-latest
 
    steps:
      - uses: actions/checkout@v4
 
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: 8.0.x
 
      - name: Restore
        run: dotnet restore
 
      - name: Build
        run: dotnet build --no-restore -c Release
 
      - name: Test
        run: dotnet test --no-build -c Release --logger trx --collect:"XPlat Code Coverage"
 
      - name: Check for vulnerable packages
        run: dotnet list package --vulnerable --include-transitive

Every push and pull request now gets a ✓ or ✗ in GitHub. Integration tests using Testcontainers work too — Docker is available on the Ubuntu runners.

Protect the main branch

In the repository settings, add branch protection for main: require pull requests, require the CI check to pass, and require a review. Now main is always buildable and tested — the precondition for deploying it at any time.

Continuous delivery

Add a deployment job that runs only on main, after CI succeeds:

  deploy:
    needs: build-and-test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production            # can require manual approval in GitHub
    permissions:
      id-token: write                  # OpenID Connect login to the cloud, no stored password
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with: { dotnet-version: 8.0.x }
      - run: dotnet publish Shop.Api -c Release -o ./publish
      - uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      - uses: azure/webapps-deploy@v3
        with:
          app-name: shop-api
          package: ./publish

For containers, the job instead builds the image, pushes it to a registry (tagged with the commit SHA), and tells the platform to roll out the new tag.

Secrets in pipelines

  • Store credentials as encrypted repository/environment secrets, referenced as ${{ secrets.NAME }} — never in the YAML itself.
  • Prefer OpenID Connect federation (as above): the cloud trusts GitHub's identity token, so there's no long-lived password to leak.
  • Give workflows the minimum permissions they need.

Database migrations in the pipeline

Schema changes must ship with the code that needs them. Common approaches: generate an idempotent SQL script (dotnet ef migrations script --idempotent) and apply it as a pipeline step, or use migration bundles (dotnet ef migrations bundle). Design migrations to be backwards compatible (add columns before using them, remove only after no code needs them) so old and new versions can run side by side during rollout.

Small and often

Teams that deploy small changes many times a day have fewer and smaller incidents than teams that ship big releases monthly. Automation is what makes frequent deployment safe.

Key takeaways

  • CI builds and tests every push and pull request; CD deploys passing changes automatically.
  • GitHub Actions workflows are YAML: triggers (on), jobs, and steps using actions and shell commands.
  • Protect main with required checks and reviews; keep credentials in secrets or use OIDC federation.
  • Ship database migrations with the code, designed to be backwards compatible.

Quiz

4 questions · 3 correct to pass
  1. 1.When does a typical CI workflow run?

  2. 2.Where should deployment credentials (cloud keys, registry passwords) be stored for GitHub Actions?

  3. 3.What do branch protection rules with required status checks do?

  4. 4.Why deploy to a staging environment before production?

To complete this lesson

  • ○ Pass the quiz (3/4 correct)