CI/CD
What is CI/CD?
CI/CD stands for Continuous Integration and Continuous Delivery (or Continuous Deployment). It’s an automated approach to building, testing, and releasing software that helps teams ship code faster, more reliably, and with fewer bugs.
Think of CI/CD as your automated quality control and delivery pipeline - every time you push code, a series of automated checks and processes run to ensure your changes work correctly and can be safely deployed.
Here’s the flow:
Developer pushes code → CI/CD Pipeline triggers
↓
Build the code (compile, package)
↓
Run automated tests (unit, integration, etc.)
↓
Run code quality checks (linting, security scans)
↓
Deploy to staging/production
Breaking Down the Acronym
CI - Continuous Integration
Continuous Integration is the practice of frequently merging code changes into a shared repository, with automated builds and tests running on every commit.
The Problem CI Solves:
Without CI, you might have:
- Developers working in isolation for days or weeks
- Integration nightmares when merging (“It works on my machine!”)
- Bugs discovered late in the development cycle
- Manual build and test processes that are slow and error-prone
With CI:
Developer commits code → Automated build starts
↓
Code compiles successfully ✓
↓
All tests pass ✓
↓
Code quality checks pass ✓
↓
Developer gets feedback (green ✓ or red ✗)
Key principles:
- Commit frequently - integrate your changes often (daily or more)
- Automate the build - no manual steps to compile or package
- Test in a clone of production - build in an environment that matches production
- Fast feedback - know within minutes if something broke
CD - Continuous Delivery vs Continuous Deployment
Here’s where the “D” gets interesting - it can mean two different things:
Continuous Delivery
Continuous Delivery means your code is always in a deployable state, but you manually trigger the actual deployment to production.
Code pushed → Build → Test → Package → [Manual Approval] → Deploy to Production
This is common when:
- You need human approval before production releases
- You want to control release timing (e.g., deploy during business hours)
- Regulatory requirements demand manual sign-off
- You’re not quite ready for full automation
Continuous Deployment
Continuous Deployment goes one step further - every change that passes all tests is automatically deployed to production. No manual approval needed.
Code pushed → Build → Test → Package → Automatically Deploy to Production
This is ideal when:
- You have excellent test coverage and confidence in your automated tests
- You want to ship features as fast as possible
- Your team embraces DevOps culture fully
- You have good monitoring and rollback capabilities
Example Scenario:
Let’s say you fix a typo in an error message:
-
Without CI/CD: You fix it, test locally, create a pull request, wait for review, merge, wait for someone to manually build and deploy, wait for the next release window. Time to production: days or weeks.
-
With Continuous Delivery: You fix it, push code, automated tests run, build succeeds, you click “Deploy to Production”. Time to production: minutes to hours.
-
With Continuous Deployment: You fix it, push code, automated tests run, build succeeds, automatically deployed. Time to production: minutes.
Why Use CI/CD?
1. Faster Feedback
Find bugs within minutes, not days:
❌ Without CI/CD:
- Write code Monday
- Discover broken tests Thursday
- Fix takes longer because you've forgotten context
✅ With CI/CD:
- Write code, push immediately
- Get feedback in 5 minutes
- Fix while context is fresh
2. Reduce Integration Problems
Small, frequent integrations are easier than large, infrequent ones:
❌ Without CI/CD:
- Everyone codes for 2 weeks in isolation
- Friday afternoon: massive merge conflicts
- Weekend ruined fixing integration issues
✅ With CI/CD:
- Everyone commits multiple times daily
- Small conflicts caught immediately
- Easy to resolve
3. Ship Faster
Automate everything between “code written” and “code in production”:
❌ Manual Process (hours or days):
- Developer: "It's ready to deploy"
- Ops: "Ok, I'll build it tomorrow"
- Next day: Manual build
- Manual testing
- Manual deployment
- Something breaks: start over
✅ Automated Pipeline (minutes):
- Developer: pushes code
- Pipeline: builds, tests, deploys
- Done
4. Reduce Risk
Smaller, more frequent deployments are less risky than large releases:
❌ Monthly Releases:
- 100 changes go live at once
- Hard to identify what broke
- Rolling back loses all 100 changes
✅ Daily/Continuous Deployments:
- 3-5 changes go live
- Easy to identify culprit
- Rollback is simple
CI/CD Platforms
There are many CI/CD platforms available. Here are the most popular:
GitHub Actions
Built into GitHub, great for projects hosted there:
# .github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal
- name: Deploy to Azure
if: github.ref == 'refs/heads/main'
run: |
# Deployment commands here
GitLab CI/CD
Built into GitLab:
# .gitlab-ci.yml
stages:
- build
- test
- deploy
build:
stage: build
script:
- dotnet restore
- dotnet build
test:
stage: test
script:
- dotnet test
deploy:
stage: deploy
script:
- dotnet publish -c Release
- # Deploy to server
only:
- main
Jenkins
Open-source, self-hosted, highly customizable:
// Jenkinsfile
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'dotnet build'
}
}
stage('Test') {
steps {
sh 'dotnet test'
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
sh 'dotnet publish -c Release'
// Deployment steps
}
}
}
}
Azure DevOps (Azure Pipelines)
Microsoft’s platform, excellent .NET integration:
# azure-pipelines.yml
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UseDotNet@2
inputs:
version: '8.0.x'
- script: dotnet restore
displayName: 'Restore dependencies'
- script: dotnet build --configuration Release
displayName: 'Build'
- script: dotnet test --no-build
displayName: 'Run tests'
- task: DotNetCoreCLI@2
displayName: 'Publish'
inputs:
command: 'publish'
publishWebProjects: true
CircleCI
Cloud-based, popular for complex workflows:
# .circleci/config.yml
version: 2.1
jobs:
build-and-test:
docker:
- image: mcr.microsoft.com/dotnet/sdk:8.0
steps:
- checkout
- run: dotnet restore
- run: dotnet build
- run: dotnet test
workflows:
version: 2
build-test-deploy:
jobs:
- build-and-test
Travis CI
Simple, cloud-based:
# .travis.yml
language: csharp
solution: YourSolution.sln
dotnet: 8.0
script:
- dotnet restore
- dotnet build
- dotnet test
Other Notable Platforms:
- Bitbucket Pipelines - Built into Bitbucket
- TeamCity - JetBrains’ CI/CD server
- Bamboo - Atlassian’s CI/CD server
- AWS CodePipeline - AWS native
- Google Cloud Build - GCP native
A Typical CI/CD Pipeline
Here’s what a comprehensive pipeline might look like:
1. Trigger (on push or pull request)
↓
2. Checkout Code
↓
3. Restore Dependencies (dotnet restore, npm install, etc.)
↓
4. Compile/Build (dotnet build)
↓
5. Run Unit Tests
↓
6. Run Integration Tests
↓
7. Code Quality Analysis (linting, static analysis)
↓
8. Security Scanning (dependency vulnerabilities)
↓
9. Build Docker Image (if using containers)
↓
10. Push to Container Registry
↓
11. Deploy to Staging Environment
↓
12. Run Smoke Tests on Staging
↓
13. [Manual Approval - for Continuous Delivery]
↓
14. Deploy to Production
↓
15. Run Smoke Tests on Production
↓
16. Notify Team (Slack, email, etc.)
CI/CD for .NET APIs
Here’s a practical example for a .NET API:
GitHub Actions Example
name: .NET API CI/CD
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
DOTNET_VERSION: '8.0.x'
AZURE_WEBAPP_NAME: 'my-api'
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Run unit tests
run: dotnet test --no-build --verbosity normal --filter Category=Unit
- name: Run integration tests
run: dotnet test --no-build --verbosity normal --filter Category=Integration
- name: Publish
run: dotnet publish -c Release -o ./publish
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: api-artifact
path: ./publish
deploy-staging:
needs: build-and-test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
steps:
- name: Download artifact
uses: actions/download-artifact@v3
with:
name: api-artifact
- name: Deploy to staging
run: |
# Deploy to staging environment
echo "Deploying to staging..."
deploy-production:
needs: build-and-test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: production # Requires manual approval
steps:
- name: Download artifact
uses: actions/download-artifact@v3
with:
name: api-artifact
- name: Deploy to Azure
uses: azure/webapps-deploy@v2
with:
app-name: ${{ env.AZURE_WEBAPP_NAME }}
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
Best Practices
1. Keep Pipelines Fast
Slow pipelines discourage frequent commits:
❌ 45-minute pipeline
- Developers batch changes
- Integration problems increase
✅ 5-10 minute pipeline
- Developers commit frequently
- Fast feedback
Speed up your pipeline:
- Run tests in parallel
- Use caching for dependencies
- Split into multiple jobs
- Only run necessary tests for each change
2. Fail Fast
Put fastest checks first:
stages:
- lint # Fast (seconds)
- unit-tests # Fast (seconds to minutes)
- build # Medium (minutes)
- integration # Slower (minutes)
- deploy # Slowest
If linting fails, no point running the full test suite.
3. Make Pipelines Reliable
Flaky tests destroy confidence:
❌ Tests fail randomly
- Developers ignore red builds
- Real issues get missed
- Pipeline becomes useless
✅ Tests are reliable
- Red build = real problem
- Team investigates immediately
- Pipeline is trusted
Fix or remove flaky tests immediately.
4. Use Environment Parity
Test in environments that match production:
# Use same OS and runtime as production
runs-on: ubuntu-latest # If production is Linux
steps:
- uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x' # Same version as production
5. Secure Your Secrets
Never commit secrets:
# ❌ BAD - Never do this
- name: Deploy
env:
API_KEY: "abc123secret" # Don't hardcode!
# ✅ GOOD - Use secrets management
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
6. Monitor Deployments
Know when things go wrong:
- name: Smoke test production
run: |
response=$(curl -s -o /dev/null -w "%{http_code}" https://api.example.com/health)
if [ $response -ne 200 ]; then
echo "Health check failed!"
exit 1
fi
- name: Notify team
if: failure()
run: |
# Send alert to Slack, email, etc.
The Deployment Pipeline Stages
Continuous Integration (CI)
Goal: Ensure code is always in a working state
Code committed
↓
Build
↓
Run tests
↓
Code analysis
↓
Artifact created
This runs on every commit to every branch.
Continuous Delivery (CD)
Goal: Keep code deployable, but deploy manually
CI passes
↓
Deploy to staging
↓
Automated tests on staging
↓
[Wait for manual approval]
↓
Deploy to production
Production deployment requires manual trigger.
Continuous Deployment (CD)
Goal: Automatically deploy every change
CI passes
↓
Deploy to staging
↓
Automated tests on staging
↓
Automatically deploy to production
↓
Monitor and alert
Production deployment is fully automated.
Common Challenges
1. Test Coverage
You need good tests to trust automation:
Low test coverage → Can't trust pipeline → Manual testing still needed
High test coverage → Trust pipeline → Safe to deploy automatically
2. Database Migrations
Databases add complexity:
- name: Run database migrations
run: dotnet ef database update --connection "${{ secrets.DB_CONNECTION }}"
# Make migrations backward compatible
# Old code should work with new schema
# Allows safe rollback
3. Feature Flags
Deploy code that’s not ready yet:
// Code is deployed but not active
if (_featureFlags.IsEnabled("NewFeature"))
{
// New code path
}
else
{
// Old code path
}
Turn features on when ready, no deployment needed.
4. Rollback Strategy
Have a plan when things go wrong:
- name: Deploy new version
id: deploy
run: ./deploy.sh v2.0
- name: Smoke test
id: smoke-test
run: ./smoke-test.sh
- name: Rollback on failure
if: failure() && steps.deploy.outcome == 'success'
run: ./rollback.sh v1.9
Measuring CI/CD Success
Track these metrics:
Deployment Frequency
How often do you deploy to production?
- Elite performers: Multiple times per day
- High performers: Once per day to once per week
- Medium performers: Once per week to once per month
- Low performers: Less than once per month
Lead Time for Changes
How long from commit to production?
- Elite: Less than one hour
- High: One day to one week
- Medium: One week to one month
- Low: More than one month
Change Failure Rate
What percentage of deployments cause problems?
- Elite: 0-15%
- High: 16-30%
- Medium/Low: Over 30%
Time to Restore Service
How long to recover from failure?
- Elite: Less than one hour
- High: Less than one day
- Medium/Low: More than one day
Wrap Up
CI/CD transforms how you deliver software. Instead of large, risky releases every few weeks, you ship small changes continuously with automated quality checks at every step.
Key takeaways:
- CI = Automate build and test on every commit
- Continuous Delivery = Always deployable, manual deploy trigger
- Continuous Deployment = Fully automated, deploys every change
- Choose the right platform for your needs (GitHub Actions, GitLab CI, Jenkins, etc.)
- Start small - automate build and test first, then gradually add more stages
- Fast feedback is crucial - pipelines should be quick
- Trust your tests - good test coverage is essential
The goal isn’t perfection on day one. Start with basic CI (automated build and test), get comfortable with that, then gradually move toward continuous deployment as your confidence and automation mature. The journey from manual deployments to fully automated continuous deployment is a gradual evolution, not a switch you flip overnight.