Other CI/CD Platforms
The same ideas in other tools: GitLab CI/CD, Azure Pipelines and Jenkins compared with GitHub Actions, and how to choose between them.
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.imagesets the container image the job runs in. -
needslets 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). -
rulesdecide 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: imageruns every job in a Python container;cachekeeps downloaded packages between pipelines, keyed on the requirements file. -
artifacts: reports: junituploads the test report, so failed tests appear in the merge request.when: alwayskeeps 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: manualmakes it wait for someone to start it: GitLab's simplest form of approval.$CI_COMMIT_BRANCHand$CI_DEFAULT_BRANCHare 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,rollingandcanary.
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
-
triggerruns the pipeline for pushes tomain. How pull requests trigger a pipeline depends on where the code lives: for GitHub repositories, pull request builds are on by default and aprsection narrows them (or, withpr: none, disables them); for Azure Repos theprkeyword is ignored and you use a build-validation branch policy. -
pool: vmImagepicks 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; theDeploystage only runs after a successfulTeststage onmain. -
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 theproductionenvironment 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 (thejunitstep 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-levelinputdirective pauses until someone clicks Proceed. Aninputstep insidestepswould 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, useagent noneat the top and give each stage its own agent. -
withCredentials(Credentials Binding plugin) reads the secretdeploy-tokenfrom 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.