Migrate from GitHub Actions

This guide helps GitHub Actions users migrate to Buildkite Pipelines, covering key differences between the platforms.

Understand the differences

Most concepts will feel familiar, but there are some differences to understand about the approaches.

System architecture

GitHub Actions is fully hosted by GitHub.

Buildkite Pipelines uses a hybrid model:

See Buildkite Pipelines architecture for more details.

The difference in default checkout behaviors

The checkout process in Buildkite Pipelines is fundamentally different from GitHub Actions due to different default checkout strategies.

GitHub Actions' actions/checkout@v4 uses a shallow clone (--depth=1) and skips Git LFS by default.

In Buildkite Pipelines:

  • Git LFS is enabled by default. You can disable it with GIT_LFS_SKIP_SMUDGE=1.
  • Agents check out the full repository. However, you can configure shallow clones using the Git Shallow Clone plugin or an agent checkout hook with --depth=1, --single-branch, and --no-recurse-submodules.

For further checkout optimization in Buildkite Pipelines, you can use additional plugins: Sparse Checkout and Custom Checkout.

Learn more in Git checkout optimization.

Security

Buildkite's hybrid architecture, which combines the centralized Buildkite SaaS platform with your own Buildkite agents, provides a unique approach to security. Buildkite takes care of the security of the SaaS platform, including user authentication, pipeline management, and the web interface. The Buildkite agents, which run on your infrastructure, allow you to maintain control over the environment, security, and other build-related resources.

While Buildkite Pipelines provides its own secrets management capabilities, you are also able to configure Buildkite Pipelines so that it doesn't store your secrets. Buildkite Pipelines does not have or need access to your source code. Only the agents you host within your infrastructure would need access to clone your repositories, and your secrets that provide this access can also be managed through secrets management tools hosted within your infrastructure.

See the Security and Secrets to learn more.

Pipeline configuration concepts

Like GitHub Actions, Buildkite Pipelines lets you define pipelines in the web interface or in files checked into a repository. The equivalent of .github/workflows/*.yml is a pipeline.yml (typically in .buildkite/). See Files and syntax for details.

In GitHub Actions, the core description of work is a workflow containing jobs, each with multiple steps. In Buildkite Pipelines, a pipeline is the core description of work.

A Buildkite pipeline contains different types of steps for different tasks:

  • Command step: Runs one or more shell commands on one or more agents.
  • Wait step: Pauses a build until all previous jobs have completed.
  • Block step: Pauses a build until unblocked.
  • Input step: Collects information from a user.
  • Trigger step: Creates a build on another pipeline.
  • Group step: Displays a group of sub-steps as one parent step.

Triggering a Buildkite pipeline creates a build, and any command steps are dispatched as jobs to run on agents. A common practice is to define a pipeline with a single step that uploads the pipeline.yml file in the code repository. The pipeline.yml contains the full pipeline definition and can be generated dynamically.

Provision agent infrastructure

Buildkite agents run your builds, tests, and deployments. They can run as Buildkite hosted agents where the infrastructure is provided for you, or on your own infrastructure (self-hosted)[/docs/pipelines/architecture#self-hosted-hybrid-architecture], similar to self-hosted runners in GitHub Actions.

For self-hosted agents, consider:

  • Infrastructure type: On-premises, cloud (AWS, GCP), or container platforms (Docker, Kubernetes).
  • Resource usage: Evaluate CPU, memory, and disk requirements based on your current runner usage.
  • Platform dependencies: Ensure agents have required tools and libraries (note dependencies from actions/setup-* actions).
  • Network: Agents poll Buildkite's agent API over HTTPS so no incoming firewall access is needed.
  • Scaling: Scale agents independently based on concurrent job requirements.
  • Build isolation: Use agent tags and clusters to target specific agents.

See the Getting started guide for Buildkite hosted agents or Installation guides for your infrastructure type for self-hosted agents.

Pipeline translation fundamentals

Before translating workflows, understand these key differences:

Files and syntax

Pipeline aspect GitHub Actions Buildkite Pipelines
Configuration file .github/workflows/*.yml pipeline.yml (typically in .buildkite/)
Syntax YAML with GitHub-specific expressions YAML
Expressions ${{ expression }} syntax Shell variables and Buildkite interpolation
Triggers Defined in workflow file (on: block) Configured in Buildkite UI or API

The syntax used in Buildkite Pipelines is simpler. You can also generate pipeline definitions at build-time with dynamic pipelines.

Step execution

By default, GitHub Actions runs jobs in parallel (unless you specify needs), while steps within a job run sequentially. Buildkite Pipelines runs all steps in parallel by default, on any available agents that can run them.

To make a Buildkite pipeline run its steps in a specific order, use the depends_on attribute or a wait step.

For instance, in the following Buildkite pipeline example, the Lint and Test steps are run in parallel (by default) first, whereas the Build step is run after the Lint and Test steps have completed.

# Buildkite Pipelines: Explicit sequencing is required to make steps run in sequence
steps:
  - label: "Lint"
    key: lint
    command: npm run lint

  - label: "Test"
    key: test
    command: npm test

  - label: "Build"
    depends_on: [lint, test] # Explicit dependency
    command: npm run build

Workspace state

In GitHub Actions, all steps within a job share the same workspace.

In Buildkite Pipelines, each step runs in a fresh workspace on potentially different agents. Artifacts from previous steps aren't automatically available.

Options for sharing state between steps:

  • Reinstall per step: Simple for fast-installing dependencies like npm ci.
  • Buildkite artifacts: Upload build artifacts from one step for use in subsequent steps. Best for small files and build outputs.
  • Cache plugin: Similar to actions/cache, use the Buildkite cache plugin for larger dependencies using cloud storage (S3, GCS).
  • External storage: Custom solutions for complex state management.

Agent targeting

GitHub Actions uses runs-on to select runners by labels:

jobs:
  build:
    runs-on: ubuntu-latest
  deploy:
    runs-on: [self-hosted, linux, production]

Buildkite Pipelines uses a pull-based model where agents poll queues for work using the agents attribute. This provides better security (no incoming connections), easier scaling with ephemeral agents, and more resilient networking:

steps:
  - label: "Build"
    command: "make build"
    agents:
      queue: "default"
  - label: "Deploy"
    command: "make deploy"
    agents:
      queue: "production"

Translate an example GitHub Actions workflow

This section translates a GitHub Actions workflow (building a Node.js app) into a Buildkite pipeline.

Step 1: Understand the source workflow

Consider the following GitHub Actions workflow:

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm run lint

  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm test
      # ... artifact upload

  build:
    needs: [lint, test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm run build
      # ... artifact upload

Step 2: Create a basic Buildkite pipeline structure

Create a .buildkite/pipeline.yml file in your repository. Start with a basic structure that maps each GitHub Actions job to a Buildkite Pipelines step:

steps:
  - label: ":eslint: Lint"
    key: lint
    command:
      - echo "Lint step placeholder"

  - label: ":test_tube: Test"
    key: test
    command:
      - echo "Test step placeholder"

  - label: ":package: Build"
    key: build
    command:
      - echo "Build step placeholder"

Notice the immediate differences in this pipeline syntax from GitHub Actions:

  • No on: block—triggers are configured in the Buildkite UI or API.
  • No actions/checkout — Buildkite Pipelines checks out code automatically.
  • Emoji support in labels using emoji syntax.
  • Key assignment for dependency references.

Step 3: Configure the step dependencies

The build step should run only after lint and test complete successfully. Configure explicit dependencies on the build step:

  - label: ":package: Build"
    key: build
    depends_on:
      - lint
      - test
    command:
      - echo "Build step placeholder"

Without this depends_on attribute, all three steps would run simultaneously, due to Buildkite Pipelines parallel-by-default behavior.

Step 4: Add the actual commands

Replace the placeholder commands with real commands. Since Buildkite Pipelines assumes tools are pre-installed on agents (or you use Docker), there's no equivalent to actions/setup-node:

  - label: ":eslint: Lint"
    key: lint
    command:
      - npm ci
      - npm run lint

Buildkite agents should be pre-configured with required tools. Alternatively, use the Docker plugin with an appropriate image like node:20.

Step 5: Implement a build matrix

Now, implement the build matrix for Node.js 18, 20, and 22:

  - label: ":test_tube: Test (Node {{matrix.node_version}})"
    key: test
    matrix:
      setup:
        node_version:
          - "18"
          - "20"
          - "22"
    command:
      - npm ci
      - npm test

The {{matrix.node_version}} template variable gets replaced at runtime, creating separate jobs for each Node.js version.

Step 6: Implement artifact collection

Add artifact collection using the artifact_paths attribute:

    artifact_paths:
      - coverage/**/* # Collect test coverage

No separate upload action is required—just specify glob patterns.

Step 7: Add caching

Replace actions/cache (or the cache option in actions/setup-node) with the cache plugin:

  - label: ":eslint: Lint"
    key: lint
    plugins:
      - cache#v1.10.0:
          manifest: package-lock.json
          path: node_modules
    command:
      - npm ci
      - npm run lint

Step 8: Review the complete pipeline

Here's the complete translated pipeline:

steps:
  - label: ":eslint: Lint"
    key: lint
    plugins:
      - cache#v1.10.0:
          manifest: package-lock.json
          path: node_modules
    command:
      - npm ci
      - npm run lint

  - label: ":test_tube: Test (Node {{matrix.node_version}})"
    key: test
    matrix:
      setup:
        node_version:
          - "18"
          - "20"
          - "22"
    plugins:
      - cache#v1.10.0:
          manifest: package-lock.json
          path: node_modules
    command:
      - npm ci
      - npm test
    artifact_paths:
      - coverage/**/*

  - label: ":package: Build"
    depends_on:
      - lint
      - test
    plugins:
      - cache#v1.10.0:
          manifest: package-lock.json
          path: node_modules
    command:
      - npm ci
      - npm run build
    artifact_paths:
      - dist/**/*

Step 9: Refactor with YAML aliases

To eliminate duplication, you can use YAML aliases:

common:
  cache: &cache
    - cache#v1.10.0:
        manifest: package-lock.json
        path: node_modules

steps:
  - label: ":eslint: Lint"
    key: lint
    plugins: *cache
    command:
      - npm ci
      - npm run lint

  - label: ":test_tube: Test (Node {{matrix.node_version}})"
    key: test
    matrix:
      setup:
        node_version:
          - "18"
          - "20"
          - "22"
    plugins: *cache
    command:
      - npm ci
      - npm test
    artifact_paths:
      - coverage/**/*

  - label: ":package: Build"
    depends_on:
      - lint
      - test
    plugins: *cache
    command:
      - npm ci
      - npm run build
    artifact_paths:
      - dist/**/*

Key mappings reference

This table provides quick mappings between common GitHub Actions concepts and their Buildkite Pipelines equivalents:

GitHub Actions Buildkite Pipelines
jobs.<id> steps array item with key: "<id>"
jobs.<id>.name label
jobs.<id>.runs-on agents: { queue: "..." }
jobs.<id>.env env
jobs.<id>.timeout-minutes timeout_in_minutes
needs depends_on
continue-on-error: true soft_fail: true
${{ secrets.NAME }} ${NAME} (configured on agent)
working-directory: ./dir Prepend cd dir && to commands
actions/upload-artifact artifact_paths on the step
actions/download-artifact buildkite-agent artifact download command
actions/cache cache plugin
strategy.matrix matrix attribute
${{ github.sha }} ${BUILDKITE_COMMIT}
${{ github.ref }} ${BUILDKITE_BRANCH}
${{ github.event.pull_request.number }} ${BUILDKITE_PULL_REQUEST}

Translating triggers

GitHub Actions supports many webhook event triggers through the on: block.

Buildkite Pipelines natively supports:

  • push (branches)
  • pull_request
  • tag (via "Build tags" setting)
  • schedule (cron)

These are configured in the Buildkite UI under Pipeline Settings, not in the YAML file.

GitHub Actions trigger Buildkite Pipelines configuration
push UI → Pipeline Settings → GitHub
pull_request UI → Pipeline Settings → GitHub
schedule UI → Pipeline Settings → Schedules
workflow_dispatch input step + "New Build" button/API
release / create (tags) UI → Build tags setting

For triggers not natively supported by Buildkite Pipelines (issues, issue_comment, workflow_run, etc.), you can:

  1. Keep in GitHub Actions: Best for GitHub-specific automation.
  2. Configure webhook: Set up an endpoint to call the Buildkite API.
  3. Use trigger step: Chain from another pipeline.

Translating context variables

GitHub Actions provides context objects (github.*, runner.*, env.*).

Buildkite Pipelines provides environment variables:

GitHub Actions context Buildkite Pipelines environment variable
github.repository BUILDKITE_REPO or BUILDKITE_PIPELINE_SLUG
github.sha BUILDKITE_COMMIT
github.ref BUILDKITE_BRANCH
github.ref_name BUILDKITE_BRANCH
github.actor BUILDKITE_BUILD_CREATOR
github.run_id BUILDKITE_BUILD_ID
github.run_number BUILDKITE_BUILD_NUMBER
github.job BUILDKITE_STEP_KEY
github.workflow BUILDKITE_PIPELINE_SLUG
github.event.pull_request.number BUILDKITE_PULL_REQUEST

Translating conditionals

GitHub Actions conditionals use the if: attribute with expressions.

Buildkite Pipelines also supports if: but with different syntax:

GitHub Actions Buildkite Pipelines
if: github.ref == 'refs/heads/main' if: build.branch == "main"
if: github.event_name == 'push' if: build.source == "webhook"
if: github.event_name == 'pull_request' if: build.pull_request.id != null
if: contains(github.ref, 'release') if: build.branch =~ /release/

For complex conditionals that can't be expressed using if: syntax in Buildkite Pipelines, use shell conditionals in your commands or dynamic pipeline uploads.

Translating matrix builds

Buildkite has native matrix support that maps directly to GitHub Actions' strategy.matrix:

GitHub Actions Buildkite Pipelines
strategy.matrix matrix.setup
strategy.matrix.include matrix.adjustments (add combinations)
strategy.matrix.exclude matrix.adjustments with skip: true
${{ matrix.<name> }} {{matrix.<name>}}
continue-on-error per matrix combo soft_fail in adjustments
fail-fast: false Default behavior (sibling jobs aren't cancelled)

An example of multi-dimensional matrix in GitHub Actions:

# GitHub Actions
strategy:
  matrix:
    os: [ubuntu-latest, macos-latest]
    node: [18, 20]

This is how it translates to Buildkite Pipelines:

#Buildkite Pipelines
steps:
  - label: "test {{matrix.os}} node-{{matrix.node}}"
    command: npm test
    agents:
      queue: "{{matrix.os}}"
    matrix:
      setup:
        os:
          - "linux"
          - "macos"
        node:
          - "18"
          - "20"

Translating services

GitHub Actions provides a services key that allows you to run containerized services (such as databases, caches, or message queues) alongside your job. These service containers are automatically started before your job runs and are accessible via their service name as a hostname.

Buildkite Pipelines handles service containers differently. Instead of a built-in services key, Buildkite Pipelines uses the Docker Compose plugin to manage multi-container environments. This approach gives you full control over container orchestration using standard Docker Compose configuration files.

To migrate your GitHub Actions services:

  1. Create a docker-compose.ci.yml file that defines your application and service containers.
  2. Configure dependencies and health checks to ensure services are ready before your tests run.
  3. Reference this configuration file in your Buildkite pipeline using the Docker Compose plugin.

The following example shows a Docker Compose configuration with a PostgreSQL service:

# docker-compose.ci.yml
services:
  app:
    build: .
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      DATABASE_URL: postgres://postgres:postgres@postgres:5432/test

  postgres:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: postgres
    healthcheck:
      test: ["CMD", "pg_isready"]
      interval: 10s
      timeout: 5s
      retries: 5

The following Buildkite pipeline configuration uses the Docker Compose plugin to run your tests. The run attribute specifies which service container to execute your commands in, while config points to your Docker Compose file. The plugin automatically starts all dependent services (in this case, PostgreSQL) and waits for health checks to pass before running your commands:

# Buildkite Pipelines
steps:
  - label: "test"
    plugins:
      - docker-compose#v5.12.1:
          run: app
          config: docker-compose.ci.yml
    command:
      - npm test

Translating job outputs

GitHub Actions uses $GITHUB_OUTPUT and jobs.<id>.outputs to pass data between jobs:

# GitHub Actions
jobs:
  setup:
    outputs:
      version: ${{ steps.get-version.outputs.version }}
    steps:
      - id: get-version
        run: echo "version=1.2.3" >> $GITHUB_OUTPUT

Buildkite Pipelines uses meta-data:

# Buildkite Pipelines
steps:
  - label: "setup"
    key: "setup"
    command:
      - buildkite-agent meta-data set "version" "1.2.3"

  - label: "build"
    depends_on: "setup"
    command:
      - VERSION=$(buildkite-agent meta-data get "version")
      - echo "Building version $VERSION"

Translating step summaries

GitHub Actions uses $GITHUB_STEP_SUMMARY to add content to the workflow summary:

# GitHub Actions
- run: echo "## Build Complete" >> $GITHUB_STEP_SUMMARY

Buildkite Pipelines uses annotations:

# Buildkite Pipelines
- command:
    - echo "## Build Complete" | buildkite-agent annotate --style "success"

Key differences and benefits of migrating to Buildkite Pipelines

This example pipeline translation demonstrates several important advantages of Buildkite's approach:

  • Simpler pipeline configuration: Buildkite YAML is straightforward with fewer special syntax rules.
  • Execution model: Buildkite Pipelines steps are parallel by default with explicit sequencing, similar to GitHub Actions jobs but applied at the step level.
  • Native features: Buildkite Pipelines provides native artifact handling and build visualization without additional actions.
  • Agent flexibility: Full control over your build environment with self-hosted agents.

For larger deployments, these differences become more significant:

  • The fresh workspace model avoids state leakage between builds.
  • The pull-based agent model simplifies scaling and security.
  • Pipeline-specific plugin versioning eliminates dependency conflicts.

Be aware of common pipeline-translation mistakes, which might include:

  • Forgetting about fresh workspaces (leading to missing dependencies).
  • Assuming tools are installed (when you need Docker or pre-configured agents).
  • Over-parallelizing interdependent steps.

Next steps

Explore these resources to enhance your migrated pipelines:

For hands-on practice, try the Buildkite pipeline converter.

For migration assistance, contact support@buildkite.com.