DevOps.Academy
CoursesCurriculumPricing

    Loading…

    ↑ ↓ move · ↵ open · Ctrl K anywhere

    ◔My learningSign in
    DevOps.Academy

    Free, hands-on DevOps lessons. Made with care in India.

    Learn

    All coursesLinux roadmapCurriculumMy learning

    Academy

    PricingCertificates

    Legal

    PrivacyTermsContact

    devops.rajeev.pro

    CI/CD PipelinesLesson 9 of 12
    Your progress · 0%
    On this page
    1. One pipeline, four spellings
    2. GitLab CI
    3. CircleCI
    4. Buildkite
    5. Choosing a tool
    ↖ Course roadmap
    Lesson 9 · Other CI tools

    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.

    35 min read · intermediate · hands-on

    By the end of this lesson you'll be able to

    0 of 6 complete

    One 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:

    Same ideas, different words. Learn the mapping and any pipeline file becomes readable.© Diagram: DevOps Academy

    Same ideas, different words. Learn the mapping and any pipeline file becomes readable.© Diagram: DevOps Academy

    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
    
    GitLabGitHub 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_BRANCHgithub.sha, github.event.repository.default_branch
    Settings → CI/CD → Variables (masked, protected)secrets and variables
    environment: + when: manualenvironments 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]
    
    CircleCIMeans
    executorswhere a job runs: Docker image, machine VM, macOS; plus resource_class
    commandsreusable step sequences
    jobsthe units of work
    workflowsthe order: requires: is needs:
    orbsshared, versioned packages of jobs and commands (like actions)
    type: approvala 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
    
    BuildkiteMeans
    a step with command:a job
    key + depends_onneeds
    waita barrier: all previous steps must pass
    blockpauses the build until someone unblocks it in the UI
    pluginsreusable behaviour, such as running the step in a Docker image
    agents and queueswhere 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 ActionsGitLab CICircleCIBuildkite
    Lives withGitHubGitLabGitHub, Bitbucketany Git host
    Runs jobs onGitHub's runners or yoursGitLab's runners or yoursCircleCI's machines or yoursyour agents only
    Strengthmarketplace of actions, tight GitHub integrationone product for code, CI and registryfast, flexible caching and machine sizesscale, 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.

    Validate before you push

    Each tool publishes a JSON schema for its file. Editors with the YAML extension use them to underline mistakes as you type, and you can check from the terminal. For example, a GitLab job with when: sometimes fails with 'sometimes' is not one of ['on_success', 'on_failure', 'always', 'never', 'manual', 'delayed'].

    ⌘
    Mini mission

    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.

    Input: A free GitLab.com account and the shop-api repositoryOutput: A green GitLab pipeline with a package artifact
    Reveal 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.git
    • gitversion control
    git add .gitlab-ci.yml && git commit -m "ci: add GitLab CI pipeline"
    • addstage changes
    • &&run the next command only if this one worked
    • commitsave staged changes
    git push gitlab main
    • pushupload 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 done

    Run each task in your own account or terminal, then tick it off.

    Knowledge check

    Quick check

    Question 1 of 5

    In GitLab CI, what does needs do that stages alone don't?

    Go deeper

    Official reading for this lesson

    • DocsGitLab CI/CD YAML syntax referenceGitLab Docs ↗
    • GuideGitLab: migrating from GitHub ActionsGitLab Docs ↗
    • DocsCircleCI configuration referenceCircleCI Docs ↗
    • DocsBuildkite: defining your pipeline stepsBuildkite Docs ↗
    • ToolBuildkite Docker pluginBuildkite ↗

    Or get every quiz answer right and it completes itself.

    ← PreviousJenkins: the controller, agents, Jenkinsfile and shared librariesNext lesson →GitOps: Git says what runs, the cluster makes it so