Other CI tools: GitLab CI, CircleCI and Buildkite
The same shop-api pipeline in GitLab CI, CircleCI and Buildkite: how each names jobs, order, caches, artifacts and approvals, and where each runs your jobs.
By the end of this lesson you'll be able to
0 of 6 completeOne pipeline, four spellings
Every CI tool runs the same kind of pipeline: jobs in some order, on some machine, with caches, artifacts, secrets and approvals. Once you can map the words, you can read any of them:
This lesson writes shop-api's pipeline (lint, unit, integration) in each tool. All three files below were checked against the tool's published JSON schema.
GitLab CI
GitLab CI is built into GitLab. The pipeline is .gitlab-ci.yml at the top of the repository, and jobs run
on runners: GitLab's shared ones, or your own.
# .gitlab-ci.yml stages: [lint, test, build] default: image: node:22-alpine # every job runs in this container cache: key: files: [package-lock.json] # new cache when the lockfile changes paths: [.npm/] before_script: - npm ci --cache .npm --prefer-offline lint: stage: lint script: - npm run lint unit: stage: test script: - npm run test:coverage integration: stage: test needs: [lint] # start as soon as lint passes script: - npm run test:integration package: stage: build script: - mkdir -p dist && cp -r src package.json dist/ - tar -czf shop-api-$CI_COMMIT_SHORT_SHA.tgz -C dist . artifacts: paths: [shop-api-*.tgz] expire_in: 7 days rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
| GitLab | GitHub Actions equivalent |
|---|---|
stages: | job order through needs (Actions has no stages) |
image: | container: on a job, or a Docker step |
script: | run: steps |
needs: | needs: |
rules: - if: | if: |
artifacts: | upload-artifact |
$CI_COMMIT_SHORT_SHA, $CI_DEFAULT_BRANCH | github.sha, github.event.repository.default_branch |
| Settings → CI/CD → Variables (masked, protected) | secrets and variables |
environment: + when: manual | environments with required reviewers |
Stages run in order, each waiting for the whole previous stage. needs overrides that with a graph:
integration starts the moment lint passes, without waiting for unit. GitLab validates the file as you
type in Build → Pipeline editor.
CircleCI
CircleCI is a hosted CI service that connects to GitHub or Bitbucket. The config is .circleci/config.yml:
# .circleci/config.yml version: 2.1 executors: node: docker: - image: cimg/node:22.23 # CircleCI's convenience image resource_class: small # the machine size you pay for commands: install: # reusable steps, like a composite action steps: - checkout - restore_cache: keys: - npm-v1-{{ checksum "package-lock.json" }} - run: npm ci - save_cache: key: npm-v1-{{ checksum "package-lock.json" }} paths: [~/.npm] jobs: lint: executor: node steps: - install - run: npm run lint unit: executor: node steps: - install - run: npm run test:coverage integration: executor: node steps: - install - run: npm run test:integration workflows: ci: jobs: - lint - unit - integration: requires: [lint, unit]
| CircleCI | Means |
|---|---|
executors | where a job runs: Docker image, machine VM, macOS; plus resource_class |
commands | reusable step sequences |
jobs | the units of work |
workflows | the order: requires: is needs: |
orbs | shared, versioned packages of jobs and commands (like actions) |
type: approval | a job that waits for a person to click Approve |
Caching is explicit: you choose the key and the paths, and {{ checksum "package-lock.json" }} plays the role
of hashFiles(). Secrets live in project settings or contexts, which can be restricted to security
groups. circleci config validate (CircleCI CLI) checks the file locally.
Buildkite
Buildkite splits CI in two: Buildkite runs the control plane (the web UI, scheduling, logs), and you run the agents that execute jobs, on your own machines, cloud or Kubernetes. Your code and secrets never leave your infrastructure, which is why large companies with strict security or special hardware choose it.
# .buildkite/pipeline.yml steps: - label: ":eslint: lint" key: lint command: npm ci && npm run lint plugins: - docker#v5.14.0: image: node:22-alpine - label: ":test_tube: unit" key: unit command: npm ci && npm run test:coverage plugins: - docker#v5.14.0: image: node:22-alpine - label: ":globe_with_meridians: integration" depends_on: [lint, unit] command: npm ci && npm run test:integration plugins: - docker#v5.14.0: image: node:22-alpine - wait # everything above must pass - block: ":rocket: Deploy to production?" branches: main - label: ":rocket: deploy" command: ./scripts/deploy.sh production branches: main
| Buildkite | Means |
|---|---|
a step with command: | a job |
key + depends_on | needs |
wait | a barrier: all previous steps must pass |
block | pauses the build until someone unblocks it in the UI |
plugins | reusable behaviour, such as running the step in a Docker image |
| agents and queues | where steps run: your machines, matched by tags |
Pipelines can also be dynamic: a step runs a script that generates more steps with buildkite-agent pipeline upload, which is how monorepos test only the parts that changed.
Choosing a tool
| GitHub Actions | GitLab CI | CircleCI | Buildkite | |
|---|---|---|---|---|
| Lives with | GitHub | GitLab | GitHub, Bitbucket | any Git host |
| Runs jobs on | GitHub's runners or yours | GitLab's runners or yours | CircleCI's machines or yours | your agents only |
| Strength | marketplace of actions, tight GitHub integration | one product for code, CI and registry | fast, flexible caching and machine sizes | scale, security, your own hardware |
Most teams use what comes with their Git host. The skills transfer: order checks by cost, cache dependencies, build once, gate production behind approval, keep secrets out of the repository and pin what you depend on.
Run shop-api's pipeline on GitLab
Mirror shop-api to GitLab, add the GitLab CI pipeline from this lesson, and get lint, unit, integration and package passing. Then change integration to wait for both lint and unit, and compare the pipeline graph.
A green GitLab pipeline with a package artifactReveal one safe solution
$ git remote add gitlab git@gitlab.com:<your-user>/shop-api.git $ nano .gitlab-ci.yml # the pipeline from this lesson $ git add .gitlab-ci.yml && git commit -m "ci: add GitLab CI pipeline" $ git push gitlab main
git remote add gitlab git@gitlab.com:<your-user>/shop-api.gitgitversion control
git add .gitlab-ci.yml && git commit -m "ci: add GitLab CI pipeline"addstage changes&&run the next command only if this one workedcommitsave staged changes
git push gitlab mainpushupload commits
In GitLab, open Build → Pipelines and the latest pipeline: integration starts as soon as lint
passes, even while unit is running. Download shop-api-<sha>.tgz from the package job's artifacts.
Then change needs: [lint] to needs: [lint, unit], push, and watch the graph draw both lines into
integration. New GitLab.com accounts may need to verify an identity before shared runners pick up jobs.
Try it yourself
0 of 6 steps doneRun each task in your own account or terminal, then tick it off.
Quick check
In GitLab CI, what does needs do that stages alone don't?
Official reading for this lesson
Or get every quiz answer right and it completes itself.