CI Pipeline (GitHub Actions)
A minimal "build and test every PR" pipeline for a .NET solution — restore, build, test, nothing else. Small enough to actually read in full, and a natural base to extend once you want it to also publish a Docker image.
Build + test on every pull request
Scoped to pull_request against main only — it never runs on pushes directly to main, so the only way code lands there is through a PR that's already passed this check.
name: CI + build only
on:
pull_request:
branches:
- main
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore --configuration Release
- name: Run unit tests
run: dotnet test --no-build --configuration Release --verbosity normal
Each step deliberately reuses the previous one's output — --no-restore on build, --no-build on test — so nothing gets redone. If any step fails, the whole job fails and the PR shows a red ✗ instead of a merge button.
Extending it: build & push the Docker image on merge
A second workflow, triggered on push to main instead of on PR, builds the image from the Dockerfile and pushes it to GitHub Container Registry. Splitting this into its own file keeps "can this PR merge" and "deploy the result" as two independent concerns.
name: Build and push image
on:
push:
branches:
- main
jobs:
push-image:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: ./YourApp.API/Dockerfile
push: true
tags: ghcr.io/${{ github.repository }}:latest
Keep secrets (connection strings, JWT keys) out of the repo and out of workflow files — use Settings → Secrets and variables → Actions and reference them as${{ secrets.NAME }}.secrets.GITHUB_TOKENabove is provided automatically by Actions; you never set it yourself.