Demo project: Wired Brain Coffee — .NET 8 API
Table of Contents
- Introduction and CI/CD Concepts
- Module 1 — Understanding CI/CD and Creating a Repository
- Module 2 — Creating a Deployment Pipeline with GitHub Actions
- Module 3 — Comparing CI/CD Platforms
- Reference Diagrams
- Reference Tables
- 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:
- Source Control Repository — Starting point of every pipeline (GitHub, GitLab, Azure Repos, Bitbucket)
- Automatic trigger — Triggered on every
pushto a target branch (e.g.,main) - Build Step — Compiling the source code
- Test Step — Running unit and/or integration tests, with code coverage report
- Artifact Retention — Keeping the compiled deliverable for deployment
- Deploy Step — Deploying to the target environment (cloud, server, container)
- Feedback Loop — Notifying developers on failure
Popular CI/CD Platforms
| Platform | Type | Key Feature |
|---|---|---|
| GitHub Actions | Cloud-hosted | Native GitHub, reusable action marketplace |
| Azure DevOps Pipelines | Cloud-hosted (Microsoft) | Enterprise integration, stages/jobs/steps, rich UI |
| GitLab CI/CD | Cloud-hosted / Self-hosted | Docker image per job, native coverage reporting |
| Jenkins | Self-hosted | Open 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 interfaceon: Controls when the workflow is triggeredjobs: Contains parallelizable work unitsruns-on: Specifies the virtual machine (runner) typesteps: Sequence of steps in a jobuses: References a pre-built action from the marketplacerun: 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 anidon 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
| Concept | GitHub Actions | Azure DevOps |
|---|---|---|
| Global unit | Workflow | Pipeline |
| Intermediate level | (absent) | Stage |
| Work unit | Job | Job |
| Atomic command | Step | Step |
| Reusable action | Action | Task |
| Runner | GitHub-hosted / Self-hosted | Microsoft-hosted / Self-hosted |
Notable difference for code coverage:
- GitHub Actions: Requires a third-party marketplace action (
irongut/CodeCoverageSummary) - Azure DevOps: Natively provides
PublishCodeCoverageResults@1with an integrated Coverage tab in the UI
GitHub Actions vs GitLab CI
| Feature | GitHub Actions | GitLab CI |
|---|---|---|
| Docker image per job | No (pre-configured runner) | Yes — image: per job |
| .NET setup required | Yes (actions/setup-dotnet) | No (image with SDK included) |
| Coverage reporting | Third party | Native via coverage_report |
| Pipeline config | File in .github/workflows/ | .gitlab-ci.yml at root |
| Structure | on / jobs / steps | stages / 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
| Step | Command | Purpose |
|---|---|---|
| Checkout | actions/checkout@v4 | Get source code from repo |
| Restore | dotnet restore | Download NuGet packages |
| Build | dotnet build --no-restore | Compile source code |
| Test | dotnet test --collect "XPlat Code Coverage" | Run tests + coverage |
| Publish | dotnet publish -c Release -o ./publish | Create deployable artifact |
| Archive | zip app.zip ./publish | Package for deployment |
| Upload | actions/upload-artifact@v4 | Retain artifact |
| Deploy | azure/webapps-deploy@v2 | Deploy to cloud |
Platform Comparison
| Aspect | GitHub Actions | Azure DevOps | GitLab CI |
|---|---|---|---|
| Configuration file | .github/workflows/*.yaml | azure-pipelines.yaml | .gitlab-ci.yml |
| Trigger | on: push | trigger: | push: |
| Work unit | jobs | stages > jobs | stages > jobs |
| Commands | steps / run | steps / script | script |
| Docker per job | No | No | Yes (image:) |
| Coverage reporting | Third-party action | Native task | Native |
| Secrets | Settings > Secrets | Library > Variable Groups | Settings > Variables |
Summary and Best Practices
| Practice | Why |
|---|---|
Trigger on main only | Avoid unnecessary builds on feature branches |
Use if: steps.build.outcome == 'success' | Skip tests if build fails |
| Store secrets in vault | Never hardcode credentials in YAML |
| Upload artifacts | Enables rollback without rebuilding |
| Separate build and deploy jobs | Separation of concerns, easier to debug |
Use needs: for job dependencies | Control execution order |
| Set coverage thresholds | Enforce code quality standards |
| Keep pipelines fast | Developers need fast feedback |
Search Terms
ci/cd · devops · setting · pipeline · git · github · actions · azure · deploy · gitlab · platforms · reference · repository · test