Loading...

The CI/CD Trinity: how to choose

GitHub Actions, GitLab CI and Jenkins solve the same problem from three different starting points. Actions is a marketplace attached to a code host. GitLab CI is one feature of an integrated DevOps platform. Jenkins is a self-hosted automation server that predates both and assumes you will run it yourself.

The decision usually follows where your code already lives. If it is on GitHub, Actions is the path of least resistance — the trigger surface, permissions and secrets are already there. If it is on GitLab, the same argument applies with more force, because GitLab ships the registry, the environments and the security scanning in the same product. Choosing against the host you already use means maintaining a second integration for the rest of the pipeline's life.

Jenkins is the answer when the constraint is not developer experience but control: air-gapped networks, bespoke hardware, build agents with licensed toolchains, or a decade of accumulated pipeline logic that no one is going to rewrite. Its cost is that you own the controller — its upgrades, its plugin compatibility, and its availability.

The same pipeline, expressed three ways

Build a container image on push to main and push it to a registry. The logic is identical; what differs is how much of it you write versus inherit.

Build and push, in each system
# GitHub Actions — .github/workflows/build.yml
on:
  push:
    branches: [main]
permissions:
  contents: read
  packages: write          # GITHUB_TOKEN is minted per job and expires with it,
                           # so there is no stored registry password. This is NOT
                           # OIDC - see the FAQ for federating to a cloud role.
jobs:
  build:
    runs-on: ubuntu-latest
    # Pin actions by commit digest in production (uses: owner/repo@<sha>).
    # Tags are mutable and are shown here only for readability.
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}

# ---------------------------------------------------------------

# GitLab CI — .gitlab-ci.yml
build:
  stage: build
  image: docker:27
  services: [docker:27-dind]
  variables:
    # Without these the client looks for a unix socket while dind is
    # listening on TLS 2376, and docker login cannot connect at all.
    DOCKER_HOST: tcp://docker:2376
    DOCKER_TLS_CERTDIR: "/certs"
    DOCKER_TLS_VERIFY: 1
    DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  script:
    # CI_REGISTRY_* are injected; no secret to configure
    - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
    - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"

# ---------------------------------------------------------------

# Jenkins — Jenkinsfile
pipeline {
  agent { label 'docker' }        // an agent you provision and maintain
  stages {
    stage('Build') {
      when { branch 'main' }
      steps {
        script {
          docker.withRegistry('https://registry.example.com', 'registry-creds') {
            docker.build("app:${env.GIT_COMMIT}").push()
          }
        }
      }
    }
  }
}

Decision matrix: which one fits your situation

Your situationUseWhy
Code is on GitHub, standard cloud targetsGitHub ActionsTriggers, secrets and OIDC to AWS/GCP/Azure are already wired. The marketplace covers most of what you would otherwise script.
Code is on GitLab, want one vendorGitLab CIRegistry, environments, review apps and scanning are the same product. Fewer integrations to own than any alternative.
Air-gapped, regulated, or licensed toolchainsJenkinsRuns entirely inside your network on hardware you control, with no dependency on a vendor's availability.
Existing Jenkins with years of pipeline logicJenkinsMigration cost is the whole project. Modernise the agents before you consider replacing the controller.
Monorepo with heavy fan-out buildsGitLab CI or JenkinsParent-child pipelines and matrix agents handle large DAGs more predictably than Actions' per-job runner model.
Small team, no platform engineerGitHub Actions or GitLab CINeither has a control plane to operate. Jenkins assumes someone owns the controller.

What actually costs you

For the hosted options the recurring cost is compute minutes, and the trap is the multiplier on larger runners rather than the base rate. Self-hosted runners remove the per-minute charge and reintroduce the operational work — patching, scaling and isolating them — which is most of what you were paying to avoid.

Jenkins has no per-minute cost and a permanent people cost. Budget for controller upgrades, plugin drift and the recovery plan for when the controller is down during an incident. The plugin ecosystem is the strength and the liability: it is why Jenkins can do anything, and why a major upgrade is a project.

Frequently asked questions

Is Jenkins still worth choosing for a new project?

Rarely, unless a specific constraint forces it — an air-gapped network, hardware-attached builds, or a licensed toolchain that will not run on a hosted runner. For a greenfield project on GitHub or GitLab, the hosted option removes an entire operational surface. Jenkins remains a reasonable answer to those specific constraints, not a default.

Can I use GitHub Actions with code hosted elsewhere?

Technically yes, by mirroring, but it is a poor trade. Most of Actions' value is the tight coupling to GitHub's events, permissions and OIDC identity. Mirroring gives you the syntax without the integration and adds a synchronisation failure mode you now have to monitor.

How do the security models differ?

Actions and GitLab CI both issue short-lived OIDC tokens that let a job assume a cloud role with no stored credential, which is the single biggest security improvement available in CI. Jenkins can achieve the same with plugins, but it is opt-in and the default path is a long-lived credential in the Jenkins store. On any of the three, treat third-party actions and plugins as supply chain: pin by digest, not by tag.

What about self-hosted runners on the hosted platforms?

They are the middle ground and often the right one: keep the control plane hosted, run the compute in your VPC so builds reach private resources and you avoid per-minute charges. You take on runner patching and isolation, but not controller availability. Never run untrusted pull request code on a self-hosted runner without ephemeral, isolated agents.

Which is fastest?

In practice the platform is rarely the bottleneck — cache strategy and image size dominate. A pipeline that restores a warm dependency cache and builds a small image will be faster on any of the three than one that does not on the fastest runner available. Measure your own pipeline before treating raw runner benchmarks as a deciding factor.

Need this managed for you, not just automated?

We're also a hands-on DevOps consultancy — Kubernetes, CI/CD, and cloud infrastructure.

Explore Our Services