CI/CD Pipelines
Build and test every push automatically, and release the same artifact through each environment
Guess first
Pick an answer. You will see if you were right at the end of the lesson.
What does continuous integration do?
Real-world analogy
What is it?
CI/CD automates the path from a git push to a running release. CI (continuous integration) builds the code and runs the tests on every push. CD (continuous delivery or deployment) takes a build that passed and releases it through environments. A pipeline is a file in the repo that lists jobs and steps, such as a GitHub Actions YAML workflow. Good pipelines fail fast and keep secrets out of logs. They build the artifact once, so the thing you tested is the thing you ship.
Real-world relevance
A team used to deploy by copying files from a laptop on Friday evenings. One release shipped a half-finished change that nobody had tested, and the weekend went on the rollback. After moving to a pipeline, every merge to main builds, tests and produces an image. Deploying that image to staging is one click. Developers still own the tests and the Dockerfile, and the DevOps team owns the servers and the deploy tooling.
Key points
- CI builds every push — Continuous integration means every push is built and tested automatically on a clean machine. It finds a broken build in minutes, while the change is still fresh.
- CD releases tested builds — Continuous delivery means a build that passed can be released with one action. Continuous deployment goes further and releases it automatically.
- Stages run in order — A typical order is restore, build, test, publish an artifact or image, then deploy. Each stage runs only if the one before it passed.
- Fail fast — Put the cheap, quick checks first, such as build and unit tests. A broken change should stop the pipeline before the slow steps use up time.
- Secrets live in the CI system — Keep passwords and tokens in the CI secret store, not in the repo. GitHub hides a secret's exact value in logs, but not a changed copy, such as an encoded one. So never print a secret, or anything built from it.
- Build once, promote the artifact — Build one artifact or image, then move that same one through test, staging and production. Rebuilding per environment means you ship something you did not test.
- Approvals guard production — Environments can require a named person to approve before a deploy runs. This adds a human check for the step with the highest risk.
- Migrations need care — Run database migrations as a deliberate pipeline step. Where you can, keep them compatible with both the old and new app version. A bad migration is hard to roll back.
- Plan the rollback — Decide before release how you go back: redeploy the previous artifact, or switch a feature flag. A developer owns that the code can be rolled back, and DevOps often owns the tooling.
Quick check
Answer from memory — scrolling up first makes it stick less.
Why build the artifact once and promote the same one through environments?
In the workflow, why does the docker-image job say needs: build-test?
Code example
name: ci
on:
push:
branches: [main]
pull_request:
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-dotnet@v6
with:
dotnet-version: 10.0.x
- run: dotnet restore
- run: dotnet build --no-restore -c Release
- run: dotnet test --no-build -c Release
docker-image:
needs: build-test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: docker build -t shop-api:${{ github.sha }} .Line-by-line walkthrough
- 1. This is a GitHub Actions workflow file. It assumes one solution file at the repo root and a Dockerfile there. The workflow runs on pushes to main and on pull requests.
- 2. The build-test job runs on a fresh Ubuntu machine. It checks out the code and installs the .NET 10 SDK with setup-dotnet.
- 3. The steps run in order: restore, build in Release mode, then test. If a step fails, the job stops and later steps do not run.
- 4. The docker-image job has needs: build-test, so it starts only after build-test succeeds.
- 5. The if line limits it to pushes on main. Pull requests are built and tested, but they do not produce an image.
- 6. docker build tags the image with the commit SHA, so each image can be traced to one commit. This workflow builds the image but does not push or deploy it.
Spot the bug
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: dotnet test
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: ./deploy.shNeed a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- GitHub Actions documentation (GitHub Docs)
- Workflow syntax for GitHub Actions (GitHub Docs)
- Building and testing .NET with GitHub Actions (Microsoft Learn)
Lock it in