Intermediate

DevOps CI/CD: Setting up a CI/CD Pipeline

CI/CD concepts and building a deployment pipeline with GitHub Actions for a .NET API.

Demo project: Wired Brain Coffee — .NET 8 API


Table of Contents

  1. Introduction and CI/CD Concepts
  2. Module 1 — Understanding CI/CD and Creating a Repository
  3. Module 2 — Creating a Deployment Pipeline with GitHub Actions
  4. Module 3 — Comparing CI/CD Platforms
  5. Reference Diagrams
  6. Reference Tables
  7. Summary and Best Practices

1. Introduction and CI/CD Concepts

Definitions: CI vs CD

CI — Continuous Integration is an automated process that builds and tests every change made to a software system. Each time a developer integrates a change into the repository, the CI system automatically triggers a build and test suite to validate that change.

CD — Continuous Delivery (or Continuous Deployment) goes further: after CI confirms the code is correct, CD automatically deploys that code to one or more environments (dev, QA, staging, production).

Components of a Good CI/CD Workflow

A good CI/CD workflow systematically includes:

  1. Source Control Repository — Starting point of every pipeline (GitHub, GitLab, Azure Repos, Bitbucket)
  2. Automatic trigger — Triggered on every push to a target branch (e.g., main)
  3. Build Step — Compiling the source code
  4. Test Step — Running unit and/or integration tests, with code coverage report
  5. Artifact Retention — Keeping the compiled deliverable for deployment
  6. Deploy Step — Deploying to the target environment (cloud, server, container)
  7. Feedback Loop — Notifying developers on failure
PlatformTypeKey Feature
GitHub ActionsCloud-hostedNative GitHub, reusable action marketplace
Azure DevOps PipelinesCloud-hosted (Microsoft)Enterprise integration, stages/jobs/steps, rich UI
GitLab CI/CDCloud-hosted / Self-hostedDocker image per job, native coverage reporting
JenkinsSelf-hostedOpen source, highly configurable, requires infrastructure

Module 1 — Understanding CI/CD and Creating a Repository

Context: Wired Brain Coffee

Wired Brain Coffee just hired its first internal developer/DevOps engineer. They inherited a .NET API application with these problems:

  • No automated CI/CD pipeline
  • Build, tests, and deployment done manually from the developer’s machine
  • No access to existing deployment scripts or procedures
  • A local Git repository exists but isn’t synchronized with a hosted service

Solution adopted: Migrate to GitHub, then configure a complete GitHub Actions pipeline.

Creating a GitHub Repository

# 1. Configure GitHub as remote
git remote add origin https://github.com/<username>/WiredBrainApi.git

# 2. Push all existing code
git push -u origin main

Repository structure after configuration:

WiredBrainApi/
├── .github/
│   └── workflows/
│       └── cicd.yaml          # GitHub Actions Pipeline
├── WiredBrainApi.Web/         # Main application
├── WiredBrain.Tests/          # Unit test project
└── WiredBrainApi.sln

Module 2 — Creating a Deployment Pipeline with GitHub Actions

GitHub Actions File Structure

A GitHub Actions file is a YAML file placed in .github/workflows/. Basic structure:

name: <Readable workflow name>           # Appears in GitHub's Actions tab

on:                                       # Trigger conditions
  workflow_dispatch:                      # Manual trigger from UI
  push:
    branches: ['main']                    # Automatic trigger on push to main

jobs:
  <job-name>:                             # Job identifier
    runs-on: ubuntu-latest                # Runner type (VM)
    steps:
      - name: <Step name>
        run: <shell command>
      - name: <Step with pre-built action>
        uses: <owner>/<action>@<version>
        with:
          <parameter>: <value>

Key points:

  • name: Name displayed in the GitHub Actions interface
  • on: Controls when the workflow is triggered
  • jobs: Contains parallelizable work units
  • runs-on: Specifies the virtual machine (runner) type
  • steps: Sequence of steps in a job
  • uses: References a pre-built action from the marketplace
  • run: Executes a shell command directly

Build Step

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4          # Checkout source code

    - name: Setup .NET
      uses: actions/setup-dotnet@v4
      with:
        dotnet-version: 8.0.x

    - name: Restore Dependencies
      run: dotnet restore

    - id: build
      name: Build
      run: dotnet build --no-restore

Note on id: Specifying an id on a step allows referencing its result in conditions (steps.build.outcome == 'success'). Essential for building dependencies between steps.

Test and Code Coverage Step

    - id: test
      if: steps.build.outcome == 'success'
      name: Test
      run: dotnet test "WiredBrain.Tests/WiredBrain.Tests.csproj"
             --collect "XPlat Code Coverage"
             -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura

    - id: coverage
      name: Code Coverage Report
      uses: irongut/CodeCoverageSummary@v1.3.0
      with:
        filename: WiredBrain.Tests/TestResults/**/coverage.cobertura.xml
        badge: false
        fail_below_min: false
        hide_branch_rate: false
        hide_complexity: true
        indicators: true
        output: console

Deploy Step

    - id: publish
      name: Publish
      run: dotnet publish "WiredBrainApi.Web/WiredBrainApi.Web.csproj"
             -c Release
             -o ./publish
             --self-contained
             -r linux-x64
             /p:UseAppHost=true

    - name: Zip Artifact
      run: zip app.zip ./publish

    - id: upload
      name: Upload Artifact
      uses: actions/upload-artifact@v4
      with:
        name: dotnet-webapp
        path: app.zip

Separate deploy job:

  deploy:
    needs: build
    runs-on: ubuntu-latest

    steps:
    - name: Download Artifact
      uses: actions/download-artifact@v4
      with:
        name: dotnet-webapp

    - name: Azure Login
      uses: azure/login@v2
      with:
        creds: ${{ secrets.AZURE_CREDENTIALS }}

    - name: Deploy to Azure
      uses: azure/webapps-deploy@v2
      with:
        app-name: app-wiredbrainapi
        package: app.zip

GitHub Secrets: Sensitive credentials (AZURE_CREDENTIALS, Docker Hub tokens, etc.) are stored in Settings > Secrets and variables > Actions. They are never exposed in pipeline logs.

Complete GitHub Actions Pipeline

name: Wired Brain Workflow

on:
  workflow_dispatch:
  push:
    branches: ['main']

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4

    - name: Setup .NET
      uses: actions/setup-dotnet@v4
      with:
        dotnet-version: 8.0.x

    - name: Restore Dependencies
      run: dotnet restore

    - id: build
      name: Build
      run: dotnet build --no-restore

    - id: test
      if: steps.build.outcome == 'success'
      name: Test
      run: dotnet test "WiredBrain.Tests/WiredBrain.Tests.csproj"
             --collect "XPlat Code Coverage"
             -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura

    - id: coverage
      name: Code Coverage Report
      uses: irongut/CodeCoverageSummary@v1.3.0
      with:
        filename: WiredBrain.Tests/TestResults/**/coverage.cobertura.xml
        badge: false
        fail_below_min: false
        hide_branch_rate: false
        hide_complexity: true
        indicators: true
        output: console

    - id: publish
      name: Publish
      run: dotnet publish "WiredBrainApi.Web/WiredBrainApi.Web.csproj"
             -c Release
             -o ./publish
             --self-contained
             -r linux-x64
             /p:UseAppHost=true

    - name: Zip Artifact
      run: zip app.zip ./publish

    - id: upload
      name: Upload Artifact
      uses: actions/upload-artifact@v4
      with:
        name: dotnet-webapp
        path: app.zip

  deploy:
    needs: build
    runs-on: ubuntu-latest

    steps:
    - name: Download Artifact
      uses: actions/download-artifact@v4
      with:
        name: dotnet-webapp

    - name: Azure Login
      uses: azure/login@v2
      with:
        creds: ${{ secrets.AZURE_CREDENTIALS }}

    - name: Deploy to Azure
      uses: azure/webapps-deploy@v2
      with:
        app-name: app-wiredbrainapi
        package: app.zip

Module 3 — Comparing CI/CD Platforms

GitHub Actions vs Azure DevOps

ConceptGitHub ActionsAzure DevOps
Global unitWorkflowPipeline
Intermediate level(absent)Stage
Work unitJobJob
Atomic commandStepStep
Reusable actionActionTask
RunnerGitHub-hosted / Self-hostedMicrosoft-hosted / Self-hosted

Notable difference for code coverage:

  • GitHub Actions: Requires a third-party marketplace action (irongut/CodeCoverageSummary)
  • Azure DevOps: Natively provides PublishCodeCoverageResults@1 with an integrated Coverage tab in the UI

GitHub Actions vs GitLab CI

FeatureGitHub ActionsGitLab CI
Docker image per jobNo (pre-configured runner)Yesimage: per job
.NET setup requiredYes (actions/setup-dotnet)No (image with SDK included)
Coverage reportingThird partyNative via coverage_report
Pipeline configFile in .github/workflows/.gitlab-ci.yml at root
Structureon / jobs / stepsstages / jobs / script

Complete Azure DevOps Pipeline

name: Wired Brain Pipeline

trigger:
- main

stages:
- stage: BuildAndTest
  displayName: Build and Test
  jobs:
  - job: Build
    displayName: Build the Application
    pool:
      vmImage: 'ubuntu-latest'
    steps:
    - task: UseDotNet@2
      inputs:
        packageType: 'sdk'
        version: '8.0.x'

    - script: dotnet restore
      displayName: Restore Dependencies

    - script: dotnet build --no-restore --configuration Release
      displayName: Build

    - script: >
        dotnet test "WiredBrain.Tests/WiredBrain.Tests.csproj"
        --collect "XPlat Code Coverage"
        -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura
      condition: succeeded()
      displayName: Run Tests and Collect Code Coverage

    - task: PublishCodeCoverageResults@1
      inputs:
        codeCoverageTool: 'Cobertura'
        summaryFileLocation: '**/coverage.cobertura.xml'
        failIfCoverageEmpty: false

    - script: >
        dotnet publish "WiredBrainApi.Web/WiredBrainApi.Web.csproj"
        -c Release
        -o $(Build.ArtifactStagingDirectory)/publish
        --self-contained -r linux-x64 /p:UseAppHost=true
      displayName: Publish Application

    - task: ArchiveFiles@2
      inputs:
        rootFolderOrFile: '$(Build.ArtifactStagingDirectory)/publish'
        includeRootFolder: false
        archiveType: 'zip'
        archiveFile: '$(Build.ArtifactStagingDirectory)/app.zip'
        replaceExistingArchive: true
      displayName: Create Zip Artifact

    - task: PublishBuildArtifacts@1
      inputs:
        pathToPublish: '$(Build.ArtifactStagingDirectory)/app.zip'
        artifactName: 'dotnet-webapp'

- stage: Deploy
  displayName: Deploy to Azure
  dependsOn: BuildAndTest
  jobs:
  - job: DeployToAzure
    displayName: Deploy to Azure App Service
    pool:
      vmImage: 'ubuntu-latest'
    steps:
    - task: DownloadBuildArtifacts@0
      inputs:
        artifactName: 'dotnet-webapp'
        downloadPath: '$(Pipeline.Workspace)'

    - task: AzureWebApp@1
      inputs:
        azureSubscription: wiredBrainConnection
        appType: 'webAppLinux'
        appName: 'app-wiredbrainapi'
        package: '$(Pipeline.Workspace)/dotnet-webapp/app.zip'

Complete GitLab CI/CD Pipeline

stages:
  - build
  - test
  - package
  - deploy

build:
  stage: build
  image: mcr.microsoft.com/dotnet/sdk:8.0-noble
  script:
    - dotnet restore
    - dotnet build --no-restore --configuration Release
  artifacts:
    paths:
      - WiredBrainApi.Web/bin/

test:
  stage: test
  image: mcr.microsoft.com/dotnet/sdk:8.0-noble
  dependencies:
    - build
  script:
    - dotnet tool install -g dotnet-reportgenerator-globaltool
    - export PATH="$PATH:/root/.dotnet/tools"
    - dotnet test "WiredBrain.Tests/WiredBrain.Tests.csproj"
        --collect "XPlat Code Coverage"
        -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: WiredBrain.Tests/TestResults/*/coverage.cobertura.xml

package:
  stage: package
  image: mcr.microsoft.com/dotnet/sdk:8.0-noble
  dependencies:
    - build
  script:
    - dotnet publish "WiredBrainApi.Web/WiredBrainApi.Web.csproj"
        -c Release -o ./publish
        --self-contained -r linux-x64 /p:UseAppHost=true
    - apt-get update && apt-get install -y zip
    - zip -r app.zip ./publish
  artifacts:
    paths:
      - app.zip

deploy:
  stage: deploy
  image: mcr.microsoft.com/azure-cli
  needs:
    - package
  dependencies:
    - package
  script:
    - az login --service-principal
        --username $APP_ID
        --password $CLIENT_SECRET
        --tenant $TENANT_ID
    - az webapp deploy
        --name "app-wiredbrainapi"
        --resource-group "WiredBrain"
        --src-path app.zip

Reference Diagrams

Complete CI/CD Pipeline

flowchart LR
    DEV([Developer]) -->|git push| REPO[(Repository\nGitHub/GitLab/ADO)]
    REPO -->|Automatic trigger| PIPE

    subgraph PIPE[CI/CD Pipeline]
        direction TB
        B[Build\ndotnet build] --> T[Test\ndotnet test]
        T --> COV[Code Coverage\nReport]
        COV --> PUB[Publish\ndotnet publish]
        PUB --> ART[Upload Artifact\napp.zip]
    end

    ART -->|needs: build| DEPLOY

    subgraph DEPLOY[Deploy Job]
        direction TB
        DL[Download Artifact] --> LOGIN[Cloud Login\nAzure / AWS]
        LOGIN --> PUSH[Deploy to App Service\napp.zip]
    end

    PUSH --> ENV([Environment\nProduction])

    PIPE -->|Failure| NOTIF([Developer\nNotification])

Build / Test / Deploy Flow

sequenceDiagram
    participant Dev as Developer
    participant Git as Repository (main)
    participant CI as CI/CD Runner
    participant Cloud as Azure App Service

    Dev->>Git: git push origin main
    Git-->>CI: Trigger webhook (push event)
    CI->>CI: Checkout code
    CI->>CI: dotnet restore
    CI->>CI: dotnet build --no-restore
    alt Build fails
        CI-->>Dev: Failure notification
    else Build succeeds
        CI->>CI: dotnet test + code coverage
        alt Tests fail
            CI-->>Dev: Failure notification
        else Tests pass
            CI->>CI: dotnet publish
            CI->>CI: zip app.zip
            CI->>CI: Upload artifact
            CI->>Cloud: az webapp deploy app.zip
            Cloud-->>Dev: Deployment successful
        end
    end

Reference Tables

CI/CD Steps

StepCommandPurpose
Checkoutactions/checkout@v4Get source code from repo
Restoredotnet restoreDownload NuGet packages
Builddotnet build --no-restoreCompile source code
Testdotnet test --collect "XPlat Code Coverage"Run tests + coverage
Publishdotnet publish -c Release -o ./publishCreate deployable artifact
Archivezip app.zip ./publishPackage for deployment
Uploadactions/upload-artifact@v4Retain artifact
Deployazure/webapps-deploy@v2Deploy to cloud

Platform Comparison

AspectGitHub ActionsAzure DevOpsGitLab CI
Configuration file.github/workflows/*.yamlazure-pipelines.yaml.gitlab-ci.yml
Triggeron: pushtrigger:push:
Work unitjobsstages > jobsstages > jobs
Commandssteps / runsteps / scriptscript
Docker per jobNoNoYes (image:)
Coverage reportingThird-party actionNative taskNative
SecretsSettings > SecretsLibrary > Variable GroupsSettings > Variables

Summary and Best Practices

PracticeWhy
Trigger on main onlyAvoid unnecessary builds on feature branches
Use if: steps.build.outcome == 'success'Skip tests if build fails
Store secrets in vaultNever hardcode credentials in YAML
Upload artifactsEnables rollback without rebuilding
Separate build and deploy jobsSeparation of concerns, easier to debug
Use needs: for job dependenciesControl execution order
Set coverage thresholdsEnforce code quality standards
Keep pipelines fastDevelopers need fast feedback

Search Terms

ci/cd · devops · setting · pipeline · git · github · actions · azure · deploy · gitlab · platforms · reference · repository · test

Interested in this course?

Contact us to book it or get a custom training plan for your team.