CI/CD pipelines automate the path from code commit to production deployment. CI (Continuous Integration) runs builds and tests on every commit to catch regressions early. CD (Continuous Delivery/Deployment) automates the release process so that every passing build can be deployed with minimal manual intervention.

GitHub Actions, Azure Pipelines, and Jenkins are three representative choices for .NET teams. The right comparison is current hosting, governance, runner control, identity integration, and total operating cost—not a timeless popularity ranking.

NOTE

The two “CD”s are different. Continuous Delivery means every passing build is automatically made ready to release, but a human clicks the button to push to production (a manual approval gate). Continuous Deployment removes that gate — every commit that passes the pipeline goes to production automatically. Deployment requires more trust in your tests, observability, and rollback (it pairs naturally with blue-green deployments and feature flags). Most teams practice continuous delivery; continuous deployment is the further, optional step.

CI, Delivery, and Deployment

Use one immutable artifact to make the boundary operational. A commit produces checkout-api@sha256:8f31c20000000000000000000000000000000000000000000000000000000000; CI compiles it, runs tests, scans dependencies, and publishes that exact digest. Continuous delivery promotes the same digest through staging and leaves production behind an approval gate. Continuous deployment removes that last human gate when automated checks, rollback, and on-call ownership are good enough. Rebuilding after approval breaks the evidence chain because production no longer runs the bytes that passed the earlier checks.

Commit-to-Production Stages and Gates

StageInput and outputGateFailure response
BuildSource commit to signed package or image digestReproducible build and provenanceStop; do not publish a partial artifact
VerifySame digest plus unit, integration, security, and policy resultsRequired evidence passesFix the commit or explicitly accept tracked risk
DeploySame digest plus environment configurationReadiness and smoke checksRemove the new instances from traffic
ReleaseHealthy deployment plus traffic or feature policySLO and business guardrailsShift traffic back or disable the feature

Build-time checks prove properties of the artifact. Deployment-time checks prove the artifact can start and serve in a particular environment. Before promotion, record the previous digest, backward-compatible database state, rollback command, and the metric threshold that stops the rollout. A pipeline that cannot identify the running digest or reverse its last traffic change is not ready for unattended deployment.

Netflix Delivery Pipeline Case Study

Netflix’s published pipeline is useful as a historical trace, not a current tool prescription. A Gradle build produced application packages; Bakery created an immutable Amazon Machine Image; Spinnaker coordinated deployment; Atlas supplied telemetry; Kayenta compared canary and baseline metrics; PagerDuty carried failed automation into incident response. Each boundary exchanged an immutable artifact or explicit evidence instead of rebuilding the application.

The transferable rule is to separate packaging, rollout, analysis, and response. Adopt the named stack only when its operational cost fits; a smaller team can preserve the same boundaries with a container registry, a managed deployment service, an SLO query, and one paging system.

GitHub Actions

Architecture: YAML workflows stored in .github/workflows/. Triggered by events (push, PR, schedule, manual). Runs on GitHub-hosted runners (Ubuntu, Windows, macOS) or self-hosted runners.

name: Build and Test
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-dotnet@v4
      with:
        dotnet-version: '8.0.x'
    - run: dotnet restore
    - run: dotnet build --no-restore
    - run: dotnet test --no-build --verbosity normal

Strengths: GitHub-hosted runners remove runner infrastructure for supported workloads. Pull requests, checks, branch protection, environments, and OIDC integrate with the repository. Marketplace actions can reduce setup work but must be pinned and reviewed as third-party code.

Weaknesses: GitHub-hosted runners have limited customization. Complex pipelines with many jobs can be slow to iterate on. Vendor lock-in to GitHub.

Best for: Teams already on GitHub, open-source projects, and teams that want zero infrastructure overhead.

Azure DevOps Pipelines

Architecture: YAML pipelines stored in the repo or classic UI-based pipelines. Runs on Microsoft-hosted agents or self-hosted agents. Integrates with Azure Boards, Repos, Artifacts, and Test Plans.

trigger:
  branches:
    include:
    - main
pool:
  vmImage: 'ubuntu-latest'
steps:
- task: UseDotNet@2
  inputs:
    version: '8.0.x'
- script: dotnet restore
- script: dotnet build --no-restore
- script: dotnet test --no-build --logger trx --results-directory $(Agent.TempDirectory)/TestResults
- task: PublishTestResults@2
  inputs:
    testResultsFormat: 'VSTest'
    testResultsFiles: '$(Agent.TempDirectory)/TestResults/*.trx'

Strengths: Deep Azure integration (deploy to AKS, App Service, Azure Functions natively). Built-in test result publishing and code coverage. Enterprise features (approvals, environments, deployment gates). Works with any git host.

Weaknesses: More complex to set up than GitHub Actions. UI can be confusing (classic vs YAML pipelines). Slower iteration cycle for pipeline changes.

Best for: Enterprise .NET teams deploying to Azure, organizations that need approval gates and audit trails, teams using Azure Boards for work tracking.

Jenkins

Architecture: Self-hosted Java application. Pipelines are commonly defined in a Jenkinsfile with the Groovy-based Pipeline DSL and run on controller-managed agents. Plugins extend integrations, which makes plugin inventory, compatibility, and patching part of the operating cost.

pipeline {
    agent any
    stages {
        stage('Build') {
            steps {
                sh 'dotnet restore'
                sh 'dotnet build --no-restore'
            }
        }
        stage('Test') {
            steps {
                sh 'dotnet test --no-build'
            }
        }
    }
}

Strengths: Full control over runner infrastructure and network placement. It can run on-premises and in air-gapped environments when dependencies are mirrored. There is no hosted-runner minute charge, but compute, storage, maintenance, and on-call labor remain.

Weaknesses: The team owns upgrades, backups, plugin compatibility, security patches, agent isolation, and availability. Jenkins commonly stores and injects credentials through its credentials subsystem and plugins, but it is not a replacement for an external secret manager and still depends on job permissions and masking behavior.

Best for: On-premises or air-gapped environments, organizations with existing Jenkins investment, teams that need full infrastructure control.

Comparison

ToolHosting.NET SupportDocker NativeCost ModelBest For
GitHub ActionsCloud (GitHub)ExcellentYesHosted-runner usage and plan-specific included quotas; self-hosted runner costGitHub-native teams
Azure DevOpsCloud (Microsoft)ExcellentYesPer-parallel-jobEnterprise Azure teams
JenkinsSelf-hostedGood (plugins)Yes (plugin)Infrastructure costOn-prem, air-gapped

Decision Rule

flowchart TD
    A{Air-gapped or on-prem, or heavy existing Jenkins investment} -->|Yes| B[Use Jenkins]
    A -->|No| C{Deploying to Azure or need enterprise approval gates and audit trails}
    C -->|Yes| D[Use Azure DevOps Pipelines]
    C -->|No| E[Use GitHub Actions as the default]

Pitfalls

Secret Leakage in Logs

What goes wrong: a pipeline step prints environment variables or request bodies to the log, exposing API keys, connection strings, or tokens. CI logs are often accessible to all team members and sometimes public.

Why it happens: debugging steps (env, printenv, verbose HTTP logging) are added during troubleshooting and not removed.

Mitigation: register secrets through the platform so known values can be masked, but do not treat masking as a security boundary—encoded, transformed, short, or dynamically retrieved values can still leak. Never print the environment or request bodies in pipeline steps. Prefer OIDC/workload identity so the job exchanges its identity for a short-lived scoped cloud token instead of storing a long-lived access key.

Flaky Tests Blocking Deploys

What goes wrong: a test that passes 90% of the time fails randomly in CI, blocking the deployment pipeline. The team learns to re-run the pipeline instead of fixing the test, eroding trust in CI.

Why it happens: tests depend on timing, external services, or shared state that is not properly isolated.

Mitigation: quarantine flaky tests immediately (mark as skipped with a tracking issue). Fix the root cause: use test containers for external dependencies, mock time-dependent behavior, and isolate shared state between tests.

Config Drift Between Environments

What goes wrong: the pipeline deploys successfully to staging but fails in production because of a configuration difference (different connection string format, missing environment variable, different secret name).

Mitigation: use infrastructure-as-code (Bicep, Terraform) to define environment configuration. Promote the same artifact through environments — never rebuild for production. Use environment-specific variable groups in Azure DevOps or environment secrets in GitHub Actions.

Questions

References