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.