The shared concepts

Once you understand one CI/CD (continuous integration and continuous delivery) platform, the others are mostly new vocabulary. All of them read a pipeline definition that is versioned with the code (pipeline as code), start it on an event such as a push or a merge request, run the work on machines that start from a clean state, and report the result back to the commit. The table maps the main terms; the GitHub Actions page explains the left-hand column in detail.

Concept GitHub Actions GitLab CI/CD Azure Pipelines Jenkins
Definition file .github/workflows/*.yml .gitlab-ci.yml A YAML file, usually azure-pipelines.yml Jenkinsfile
Whole automated run Workflow (run) Pipeline Pipeline (run) Pipeline (build)
Ordered phase (jobs ordered with needs) Stage Stage Stage
Unit that runs on one machine Job Job Job (a stage, on an agent)
Single command or action Step (run or uses) A line in script Step (script or task) Step (sh, junit, …)
Machine that runs the work Runner (GitHub-hosted or self-hosted) Runner Agent, from an agent pool (Microsoft-hosted or self-hosted) Agent (node), managed by the controller
When to run on: rules:, workflow: trigger: Triggers, webhooks, multibranch scanning
Reuse Actions, reusable workflows include:, templates, components Templates, tasks Shared libraries, plugins
Secrets Secrets (repository, environment, organization) CI/CD variables (masked, protected) Secret variables, variable groups Credentials
Human approval Environment with required reviewers Manual jobs, protected environments Environment approvals and checks input directive

Two differences in structure are worth noticing. GitHub Actions has no separate "stage" level: jobs run in parallel unless needs orders them. GitLab and Azure Pipelines start from stages that run one after another, with the jobs inside a stage running in parallel. The result is the same graph of work, described from opposite ends.

GitLab CI/CD

GitLab CI/CD is the pipeline system built into GitLab, a DevOps platform that combines Git hosting, merge requests (GitLab's name for pull requests), issues, a container registry and CI/CD in one product. It is available as a hosted service (GitLab.com) and as a self-managed installation, which is a common reason organizations choose it. GitLab releases monthly; the current line at the time of writing is GitLab 19.4 (September 2026).

A pipeline is defined in .gitlab-ci.yml at the root of the repository. The key ideas:

  • Stages run in order. The default list is .pre, build, test, deploy, .post; you can define your own. Jobs in the same stage run in parallel, and the next stage starts only when the previous one succeeded.
  • Jobs are top-level keys with a script (required): the shell commands to run. image sets the container image the job runs in.
  • needs lets a job start as soon as the jobs it depends on finish, instead of waiting for its whole stage, which turns the stages into a directed acyclic graph (DAG).
  • rules decide whether a job runs, for example only on the default branch or only when certain files changed.
  • Runners execute the jobs: GitLab-hosted (instance) runners on GitLab.com, or your own, registered for an instance, group or project.
# .gitlab-ci.yml
stages:
  - lint
  - test
  - deploy

default:
  image: python:3.12-slim

variables:
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"

cache:
  key:
    files:
      - requirements-dev.txt
  paths:
    - .cache/pip

lint:
  stage: lint
  script:
    - pip install -r requirements-dev.txt
    - ruff check .

unit-tests:
  stage: test
  script:
    - pip install -r requirements-dev.txt
    - pytest --junitxml=report.xml
  artifacts:
    when: always
    expire_in: 1 week
    reports:
      junit: report.xml

deploy-production:
  stage: deploy
  script:
    - ./scripts/deploy.sh production
  environment:
    name: production
    url: https://inventory.example.com
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      when: manual
  • default: image runs every job in a Python container; cache keeps downloaded packages between pipelines, keyed on the requirements file.
  • artifacts: reports: junit uploads the test report, so failed tests appear in the merge request. when: always keeps the report even when tests fail, which is exactly when you need it.
  • The deploy job only exists in pipelines for the default branch, and when: manual makes it wait for someone to start it: GitLab's simplest form of approval. $CI_COMMIT_BRANCH and $CI_DEFAULT_BRANCH are predefined variables.
  • Secrets are set as CI/CD variables in the project settings, marked masked (hidden in logs) and protected (only available to pipelines on protected branches and tags), never written in this file.

Azure Pipelines

Azure Pipelines is the CI/CD service of Azure DevOps, Microsoft's suite that also includes Azure Repos (Git hosting), Azure Boards (work tracking) and Azure Artifacts (package feeds). It builds code from Azure Repos, GitHub and other Git hosts, and is common in organizations that already use Azure DevOps for their projects. Its structure, from Microsoft's documentation:

  • A trigger starts a pipeline; a pipeline has stages, a stage has jobs, a job has steps. A step is either a script or a task, a packaged, reusable building block such as UsePythonVersion.
  • Jobs run on agents, Microsoft-hosted or self-hosted, grouped in agent pools. Some jobs need no agent at all (agentless jobs, for example to wait for an approval).
  • Environments carry approvals and checks. A deployment job targets an environment and supports the strategies runOnce, rolling and canary.

Pipelines are written in YAML, conventionally in azure-pipelines.yml at the repository root, though you can point a pipeline at any YAML file. An older graphical "Classic" editor still exists; YAML is what you can review and version.

# azure-pipelines.yml
trigger:
  branches:
    include:
      - main

pool:
  vmImage: ubuntu-latest

stages:
  - stage: Test
    jobs:
      - job: UnitTests
        steps:
          - task: UsePythonVersion@0
            inputs:
              versionSpec: "3.12"
          - script: pip install -r requirements-dev.txt
            displayName: Install dependencies
          - script: pytest --junitxml=test-results.xml
            displayName: Run tests
          - task: PublishTestResults@2
            condition: succeededOrFailed()
            inputs:
              testResultsFiles: test-results.xml

  - stage: Deploy
    dependsOn: Test
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    jobs:
      - deployment: DeployProduction
        environment: production        # approvals and checks are set on the environment
        strategy:
          runOnce:
            deploy:
              steps:
                - checkout: self
                - script: ./scripts/deploy.sh production
                  displayName: Deploy
  • trigger runs the pipeline for pushes to main. How pull requests trigger a pipeline depends on where the code lives: for GitHub repositories, pull request builds are on by default and a pr section narrows them (or, with pr: none, disables them); for Azure Repos the pr keyword is ignored and you use a build-validation branch policy.
  • pool: vmImage picks a Microsoft-hosted agent image. Tasks have a major version after the @, comparable to an action's version tag.
  • condition: succeededOrFailed() publishes the test results even when tests fail; the Deploy stage only runs after a successful Test stage on main.
  • A deployment job does not check out the repository automatically, hence the explicit checkout: self. Approvals, business-hours checks and similar gates are configured on the production environment in the web interface, so the YAML cannot bypass them.

Jenkins

Jenkins is an open-source automation server that you host yourself. It is one of the oldest CI tools still in wide use, and its strength and weakness are the same thing: it does very little on its own and almost anything with plugins (Git integration, credentials, test reports, cloud agents, notifications). The architecture has two parts:

  • The controller (formerly called "master") stores the configuration, schedules work, serves the web interface and holds the plugins and credentials.
  • Agents (nodes) run the builds. They can be permanent machines or short-lived containers and cloud VMs (virtual machines) started on demand. Running builds on the controller itself is discouraged, because a build then has access to everything the controller holds.

Jenkins has a weekly release line and a Long-Term Support (LTS) line for production use. As of 30 September 2026 the current LTS is 2.580.1; LTS releases from 2.555.1 (April 2026) need Java 21 or 25. Keep the controller and its plugins up to date: outdated plugins are a recurring source of security advisories.

The Jenkinsfile

A Jenkinsfile is a text file in the repository that defines a Jenkins pipeline as code. It comes in two syntaxes: declarative, a structured format with fixed sections, and scripted, which is a general-purpose program in Groovy (a programming language for the Java platform). Declarative is the recommended starting point. This Jenkinsfile does the same as the examples above:

pipeline {
    agent { label 'linux' }
    options {
        timeout(time: 20, unit: 'MINUTES')
    }
    stages {
        stage('Lint') {
            steps {
                sh 'pip install -r requirements-dev.txt'
                sh 'ruff check .'
            }
        }
        stage('Test') {
            steps {
                sh 'pytest --junitxml=report.xml'
            }
            post {
                always {
                    junit 'report.xml'
                }
            }
        }
        stage('Deploy') {
            when { branch 'main' }
            input {
                message 'Deploy to production?'
            }
            steps {
                withCredentials([string(credentialsId: 'deploy-token', variable: 'DEPLOY_TOKEN')]) {
                    sh './scripts/deploy.sh production'
                }
            }
        }
    }
}
  • agent { label 'linux' } runs the pipeline on any agent with that label. options { timeout(…) } stops a hanging build.
  • post { always { junit … } } publishes the test report whatever the result (the junit step comes from the JUnit plugin).
  • when { branch 'main' } limits deployment to the main branch in a multibranch pipeline, which creates one pipeline per branch that contains a Jenkinsfile. The stage-level input directive pauses until someone clicks Proceed. An input step inside steps would hold an executor while waiting; Jenkins recommends the directive instead. Because this pipeline has one agent for all stages, that agent is still held during the wait; to free it, use agent none at the top and give each stage its own agent.
  • withCredentials (Credentials Binding plugin) reads the secret deploy-token from Jenkins's credential store into an environment variable for just that block, and masks it in the log.

CircleCI and others

CircleCI is a hosted CI/CD service. Its configuration lives in .circleci/config.yml, starting with version: 2.1; jobs contain steps such as checkout and run, each job runs in an executor (a Docker image, a virtual machine and others), and workflows combine jobs, ordering them with requires:

# .circleci/config.yml
version: 2.1

jobs:
  test:
    docker:
      - image: cimg/python:3.12
    steps:
      - checkout
      - run: pip install -r requirements-dev.txt
      - run: pytest

  deploy:
    docker:
      - image: cimg/python:3.12
    steps:
      - checkout
      - run: ./scripts/deploy.sh production

workflows:
  test-and-deploy:
    jobs:
      - test
      - deploy:
          requires:
            - test
          filters:
            branches:
              only: main

Other platforms you may meet: Bitbucket Pipelines (built into Bitbucket), the cloud providers' own services (AWS CodePipeline with CodeBuild, Google Cloud Build), and Kubernetes-native systems such as Tekton. The concepts in the table above apply to all of them.

How to choose

Technically, every platform on this page can build, test and deploy almost anything. The decision usually comes down to these questions:

  • Where does the code live? The CI built into your Git host (GitHub Actions for GitHub, GitLab CI/CD for GitLab, Azure Pipelines next to Azure Repos) gives the tightest integration: status checks on pull requests, one set of permissions, one place to look. This is the default answer unless something below overrides it.
  • Must it run on your own infrastructure? Some organizations cannot send code or credentials to a cloud service, or have to build in a network without internet access. Jenkins and self-managed GitLab run entirely on your own servers; Azure DevOps has an on-premises edition (Azure DevOps Server), and GitHub has GitHub Enterprise Server. All hosted platforms also accept self-hosted runners or agents, which run the jobs inside your network while the service orchestrates.
  • Ecosystem. Ready-made building blocks save work: the GitHub Marketplace for actions, Azure DevOps tasks and extensions, GitLab templates and components, Jenkins's plugin catalog. Check that the integrations you need (your cloud, your test tools, your ticketing system) exist and are maintained.
  • Cost. Hosted platforms charge per build minute beyond an included amount, with large multipliers for Windows and macOS; self-hosted machines move the cost to your own hardware and the people who maintain it. Jenkins has no license fee, but its maintenance is not free.
  • Compliance and audit. Data residency, required approvals, audit logs, single sign-on and retention of logs and artifacts. Large organizations often decide on these grounds.
  • Skills. A platform the team already knows well is worth a lot; the best pipeline is one that people can read and fix.

Migrating between platforms

Because the concepts are shared, a migration is mostly translation: triggers to triggers, jobs to jobs, secrets to secrets. The work is in what does not translate one to one:

  • Plugins and tasks. A Jenkins plugin or an Azure DevOps task may have no direct equivalent; often it becomes a plain script step, or an action that does something similar.
  • Secrets and credentials. They cannot be exported in plain text (by design), so they have to be recreated, ideally replaced by OIDC (OpenID Connect) trust relationships so no long-lived cloud keys move at all.
  • Implicit behavior. Default shells, working folders, which branch triggers what, and how caches and artifacts are shared between jobs differ per platform.
  • Agents and network access. A Jenkins agent in the data center can reach internal systems; a cloud-hosted runner cannot, unless you add self-hosted runners.

Migration tooling exists: GitHub, for example, offers GitHub Actions Importer, which converts pipelines from several other platforms into draft workflows; check its documentation for the platforms it currently supports. Treat any converted file as a first draft to review. A safe approach is to run the old and new pipelines side by side on the same commits until their results agree, migrate one repository first, and switch the required checks over only when the new pipeline has proven itself.