Run GitHub Actions workflows in Buildkite
Running GitHub Actions workflows in Buildkite is currently in public preview. To report issues with the preview, open an issue in the buildkite-gha repository. For help migrating to native Buildkite Pipelines steps, contact the Buildkite Support team at support@buildkite.com.
The plugin and runtime are under active development. Review the GitHub Actions compatibility reference, which describes buildkite-gha v0.102.1, before adding a workflow.
Buildkite Pipelines can match GitHub events against your workflows and create one build per matching workflow for each event. This server-side workflow dispatch is the recommended way to run GitHub Actions workflows. The GitHub Actions Buildkite plugin runs the selected workflow as Pipelines jobs, so you can migrate with minimal changes, then replace imported jobs with native Buildkite Pipelines steps over time.
During the preview, start with a simple workflow in a public github.com repository that targets Linux x86-64. Private repository checkout, private reusable workflows, statically named Buildkite secrets, GitHub deployment environments and variables, temporary GitHub tokens, and OIDC require additional setup. Review the supported functionality and limitations before you begin.
Add a GitHub Actions workflow to a pipeline
Use GitHub Actions setup mode when creating a pipeline in the Buildkite interface. If setup mode isn't available to your organization, configure the equivalent steps, settings, and trigger manually using the CLI, API, or an agent-based automation workflow.
Create a GitHub Actions pipeline
GitHub Actions setup mode isn't available to Buildkite organizations that require pipeline templates, or that still use the legacy new pipeline page. These organizations can use the manual API setup.
Connect your repository using the full-access GitHub repository provider, which gives Buildkite Pipelines access to workflow files. GitHub (Limited Access) and GitHub Enterprise Server aren't supported for server-side dispatch.
To create a pipeline:
- From the Buildkite dashboard, select New Pipeline.
- Under Git scope, select your GitHub account or organization. Then select a repository that contains GitHub Actions workflow files on its default branch.
- When Buildkite Pipelines detects your workflows, review the Run your GitHub Actions workflows setup and the pipeline details.
- Select Create GitHub Actions pipeline.
The new pipeline uses the following configuration without an explicit workflow, workflows, or importer key:
steps:
- label: ":github:"
plugin: github-actions
This setup creates an enabled GitHub Actions pipeline trigger and provisions its own signed repository webhook. This skips the traditional GitHub webhook setup, not webhooks altogether. Don't add a second native repository webhook for this pipeline, because it can create duplicate builds.
The pipeline enables workflow-authorized GitHub access tokens and disables pipeline-level commit status publishing. Review token permissions and trust boundaries, and disable workflow access tokens in the pipeline's GitHub settings if your workflows don't need them.
Configure an equivalent pipeline manually
Creating or uploading pipeline YAML alone doesn't configure server-side dispatch. CLI, API, and agent-based setup must also configure the pipeline settings and create the GitHub Actions pipeline trigger.
- Connect the GitHub.com repository using the full-access GitHub repository provider.
-
Create a YAML pipeline with the following JSON request body. Replace
YOUR_CLUSTER_ID, the repository URL, andmainwith your cluster, repository, and default branch. The settings match GitHub Actions setup mode in the interface.{ "name": "GitHub Actions", "repository": "https://github.com/acme-inc/example.git", "cluster_id": "YOUR_CLUSTER_ID", "default_branch": "main", "configuration": "steps:\n - label: \"<img class="emoji" title="github" alt=":github:" src="https://buildkiteassets.com/emojis/img-buildkite-64/github.png" draggable="false" />\"\n plugin: github-actions\n", "branch_configuration": null, "skip_queued_branch_builds": false, "cancel_running_branch_builds": false, "provider_settings": { "github_workflow_access_tokens_enabled": true, "publish_commit_status": false, "build_branches": true, "build_pull_requests": true, "build_tags": true, "filter_enabled": false, "filter_condition": null, "pull_request_branch_filter_enabled": false, "pull_request_branch_filter_configuration": null, "ignore_default_branch_pull_requests": false, "skip_builds_for_closed_pull_requests": false, "skip_builds_for_existing_commits": false, "skip_pull_request_builds_for_existing_commits": false, "trigger_mode": "none" } }Setting
trigger_modetononedisables traditional GitHub webhook triggering without affecting the GitHub Actions pipeline trigger. This prevents native webhook deliveries from creating duplicate builds.If your organization requires pipeline templates, the API rejects custom
configuration. Create an approved template containing the GitHub Actions plugin configuration, then replaceconfigurationin this request with itspipeline_template_uuid. Don't send both fields. Configure the provider settings shown above and complete the remaining trigger setup separately. Create a GitHub Actions pipeline trigger with
type: "github_actions", alabelsuch as"GitHub Actions",enabled: true, andcreate_webhook: true. This provisions a signed repository webhook. Don't configure a traditional repository webhook for the pipeline.Save the response's
endpoint_urlsecurely and checkwebhook_creation.status. A201 Createdresponse confirms trigger creation, not successful webhook provisioning. If provisioning failed, repair the webhook setup for the existing trigger rather than creating another trigger.Send a supported GitHub event and inspect Recent Deliveries and the resulting builds. An event matching two workflows should create two builds, each running its matched workflow.
The REST API requires the write_pipelines scope to create pipelines and triggers, and Full Access to manage the pipeline's triggers. See the provider settings reference for the settings payload. github_actions_mode is interface presentation metadata, not an API switch that configures these resources.
Trigger builds from workflow events
A GitHub Actions pipeline trigger reads top-level workflow files under .github/workflows/ and matches their on declarations against each incoming GitHub event before creating builds. The trigger creates one build per matching workflow per event, and passes the selected workflow to the plugin. If a delivery produces more than 100 workflow candidates, the entire delivery is rejected before any builds are created. Don't set workflow or workflows in the plugin configuration: either explicit selector overrides the server's choice.
The plugin uploads a server-selected workflow's jobs without a workflow group. Unlike an explicitly configured path, a missing or untracked server-selected workflow fails the importer step.
Server-side dispatch is not GitHub's workflow_dispatch event. The trigger handles supported branch_protection_rule, create, delete, deployment, deployment_status, discussion, discussion_comment, fork, gollum, issue_comment, issues, label, merge_group, milestone, page_build, public, pull_request, pull_request_review, pull_request_review_comment, push, release, and watch declarations. The trigger doesn't start builds for standalone workflow_dispatch, schedule, or workflow_call declarations. Manual and scheduled Buildkite builds remain available through the explicit-workflow setup.
The pull_request_target event is intentionally unsupported, because its privileged execution model would expose base-repository authority to untrusted contributions. The repository_dispatch, workflow_run, check_run, and check_suite events aren't supported yet.
The trigger supports branch and tag pushes, plus same-repository pull requests. It supports branches, branches-ignore, tags, and tags-ignore. Without an explicit types filter, or with types: null or types: [], a pull request workflow triggers on opened, reopened, and synchronize. Pull request builds use the head branch and commit, while GITHUB_WORKFLOW_REF identifies refs/pull/<N>/merge. Fork pull requests, unsupported activity types, and unsupported filter patterns fail closed and appear in Recent Deliveries.
For events other than push, pull_request, and merge_group, the trigger accepts and ignores branches, branches-ignore, tags, tags-ignore, paths, and paths-ignore, matching GitHub. Release workflows can select any of GitHub's seven release activities, and a bare release declaration selects all of them. Repository events such as fork, watch, discussion, and milestone run the workflow from the repository's default branch. A pipeline trigger's existing webhook might not be subscribed to events that Buildkite added after the webhook was provisioned. If a supported event doesn't appear in Recent Deliveries, contact the Buildkite Support team at support@buildkite.com.
For push and pull request workflows, the server accepts paths and paths-ignore but defers path matching to the importer. A candidate build can be created before the importer determines that the workflow should be skipped. Path matching requires a verified linked webhook and matching diff evidence from the checkout. Runtime event support alone doesn't imply server-side dispatch support; review the pipeline trigger selection contract and the workflow names and triggers reference for event-specific limits.
The trigger's workflow matching decides which builds to create. Traditional provider build filters and duplicate-commit qualification aren't applied to these builds. Don't rely on those settings to restrict workflow-triggered builds. Configure supported workflow filters instead, and leave branch-wide intermediate-build skipping and cancellation disabled so one workflow's build doesn't suppress another.
Migrate to server-side dispatch
Buildkite strongly recommends migrating existing explicit-workflow pipelines to server-side dispatch. The explicit-workflow plugin and native GitHub webhook setup remain supported.
- Review the workflow's events and filters against the server-side dispatch limits above. Keep unsupported event paths, such as manual or scheduled builds, in a separately configured explicit-workflow pipeline if needed.
- In the pipeline's GitHub settings, select Disable Incoming GitHub Webhook Processing, or use the Disable GitHub webhook processing API. This disables native webhook processing for this pipeline without affecting GitHub Actions pipeline triggers. If this feature isn't available for your organization, disable only the GitHub repository webhook dedicated to this pipeline. Don't delete or disable webhooks used by other pipelines.
- Remove
workflowandworkflowsfrom the plugin configuration so the server-selected workflow runs. For a dedicated GitHub Actions pipeline, use the minimal configuration above. Preserve any required plugin options, such as runner mappings or OIDC configuration, when using the expanded plugin syntax. - Apply the pipeline and provider settings from the manual setup example using Update a pipeline. Review token access before enabling it. Disable branch-wide intermediate-build skipping and cancellation. Update required GitHub checks to use the workflow checks before disabling pipeline-level commit status publishing, so branch protection doesn't wait for a status that is no longer published.
- Create the GitHub Actions pipeline trigger with webhook provisioning as described above. Check the provisioning result, preserve the one-time endpoint URL, and repair partial failures without recreating the trigger.
- Verify a supported event in Recent Deliveries. Confirm that each matching workflow creates its own build and that no native webhook creates additional builds.
If the existing pipeline mixes imported and native steps, review those steps before migrating: every workflow-triggered build runs the pipeline configuration. Native steps that previously ran once per event might now run once per matching workflow.
Use explicit workflows in an existing build
The previous setup remains supported for pipelines where native GitHub webhooks, schedules, or manual actions create the build and the plugin selects workflows inside it. Prefer server-side dispatch for new GitHub Actions pipelines.
Configure the plugin manually
Add the following step to your pipeline configuration. Set workflow to the path of one workflow file in your repository. Give the step a unique key so Buildkite Pipelines can connect it to the jobs created by the plugin:
steps:
- label: ":github: GitHub Actions"
key: "github-actions"
agents:
queue: "importer-linux"
plugins:
- github-actions:
workflow: ".github/workflows/ci.yml"
To select multiple workflows, use workflows with an array of explicit paths:
steps:
- label: ":github: GitHub Actions"
key: "github-actions"
agents:
queue: "importer-linux"
plugins:
- github-actions:
workflows:
- ".github/workflows/ci.yml"
- ".github/workflows/release.yml"
Use either workflow or workflows, but not both. Each present path must identify a regular, tracked .yml or .yaml file inside the repository. Missing or untracked paths produce a warning and are skipped. If every configured path is missing or untracked, the importer succeeds without uploading a pipeline. Directories, tracked files missing from the checkout, globs, symlinks, and paths outside the repository cause the import to fail.
When this step runs, the plugin turns the workflows into a dynamic pipeline. Each successfully compiled, directly runnable workflow becomes a group that depends on the plugin step, even when you select only one workflow. The generated jobs appear inside the group. When you select several workflows, a parse, compilation, or trigger-translation failure in one of them becomes a failing top-level replacement step, and the other valid workflows continue. A parse failure in a single selected workflow, and event-input, admission, artifact, and upload failures, abort the transaction.
The plugin supports the following configuration:
| Property | Required | Description | |||
|---|---|---|---|---|---|
| Property | workflow |
Required | Only without server selection | Description | Path to one GitHub Actions workflow in the repository. Overrides the server-selected workflow. |
| Property | workflows |
Required | Only without server selection | Description | Array of paths to GitHub Actions workflows in the repository. Overrides the server-selected workflow. Use this or workflow, not both. |
| Property | version |
Required | No | Description | Latest stable or an exact buildkite-gha runtime release from 0.9.0 onward. The default is latest. |
| Property | source-ref |
Required | No | Description | A commit, branch, or tag from the public buildkite-gha repository for testing unreleased changes. The importer resolves a branch or tag to a commit once at startup. Use a full lowercase 40-character commit for repeatable builds. Source builds don't include a Linux arm64 runtime. This property can't be used with version. |
| Property | minimum-release-age |
Required | No | Description | Minimum release age used by mise when resolving latest. The default is 0s. |
| Property | experimental-runner-user |
Required | No | Description | Run generated Linux jobs as a dedicated runner user. The default is true. Set this property to false only as a temporary compatibility measure. |
| Property | private-reusable-workflows |
Required | No | Description | Allow reusable workflows from private repositories that the importer's existing Git credentials can read. The default is false. |
| Property | oidc |
Required | No | Description | Buildkite OIDC token options for jobs that declare permissions: id-token: write. |
| Property | runners |
Required | No | Description | Explicit mappings from GitHub runner labels to Buildkite queues, optional Linux x86-64 images, and optional Buildkite hosted cache volumes. A mapped selector bypasses automatic runner resolution, but the Agent API still validates its queue. |
The Git ref after github-actions# selects the plugin code. This is separate from version, which selects the buildkite-gha runtime. Use a specific plugin release, such as github-actions#v0.13.0, and an exact runtime version when you need immutable version selection.
In this explicit-workflow setup, the plugin doesn't create build triggers from the workflow's on key. Set up native GitHub triggers and schedules in Buildkite Pipelines, or start a build yourself by selecting New Build or using the REST API. Within the existing build, the on key determines whether each selected workflow is eligible to run. In the recommended server-side setup, workflow matching happens before build creation.
For native pull request triggers, turn off Skip when pull request has existing build for commit and branch and Skip when pull request is closed or merged in the pipeline's GitHub settings if those events need to reach the imported workflow.
For manual and scheduled builds, the plugin automatically finds the exact commit after checkout.
The plugin gives each workflow a GitHub event type based on how the Buildkite build started:
- Pull request builds receive
pull_request. - Verified merge queue builds receive
merge_group. - Verified release builds receive
release. - Verified issue activity builds receive
issues. - Builds started from the Buildkite interface or API fall back to
push. Aworkflow_dispatchevent requires an explicit event snapshot or authoritative event metadata; see Select the effective event. - Scheduled builds receive
schedule. - Other builds, including branch, tag, and triggered builds, receive
push.
Pull request builds check out and run against the head commit of the pull request branch (refs/pull/<N>/head). This matches how Buildkite Pipelines handles pull request builds by default, and applies even before GitHub finishes computing the pull request's merge commit.
Release workflows require the GitHub Releases additional webhook and the Code trigger mode. Native release builds deliver only the published, created, and released activity types. A bare release trigger, or types: null or types: [], selects all seven GitHub release activities. If a workflow selects unpublished, edited, deleted, or prereleased, the importer shows a warning that native builds don't deliver those activities. Use the server-side setup to run workflows for every release activity. With the full-access GitHub repository provider, Buildkite Pipelines resolves the release tag to its immutable commit before creating the build. Without this access, the plugin can use the checked-out HEAD as a compatibility fallback, but the build can't receive a workflow access token for the release.
Issue workflows require the private-preview issue activity build setting. A bare issues trigger accepts every supported activity type. You can also list supported GitHub issue activity types explicitly. Branch, tag, and path filters are accepted but ignored, matching GitHub. Workflow filters aren't supported for this event.
Each successfully compiled workflow that declares the effective event becomes a group named for the workflow. A supported, non-empty run-name, which can use github, inputs, and vars expressions, is appended to the group label, but doesn't change the build message or external check name. The external check identifies both the workflow and effective event. A workflow that doesn't declare the effective event becomes a top-level skipped step. After upload, an importer-scoped informational annotation lists skipped workflows. A local reusable workflow that declares only workflow_call can support another selected workflow, but doesn't create its own group.
The runtime supports branch, tag, and bounded path filters for push, and base-branch, activity type, and bounded path filters for pull_request. Path filters require a verified linked GitHub webhook and complete matching diff evidence from the local checkout. When the evidence verifies that no changed path matches, the workflow becomes a skipped step. If that evidence is missing or uncertain, the affected workflow fails instead of running more broadly. A push can include up to 1,000 commits, and a push or pull request can change up to 3,000 files. For merge_group, paths and paths-ignore are accepted but ignored with a warning, matching GitHub behavior. Every workflow that declares schedule is eligible for every Buildkite scheduled build.
Migrate incrementally
You don't have to convert the whole workflow at once. Imported workflow jobs and native Buildkite Pipelines steps can run in the same build. In this example, the native Deploy step waits for all the imported test jobs to finish:
steps:
- label: ":github: Tests"
key: "github-actions-tests"
agents:
queue: "importer-linux"
plugins:
- github-actions:
workflow: ".github/workflows/ci.yml"
- label: "Deploy"
key: "deploy"
depends_on: "github-actions-tests"
command: ".buildkite/deploy.sh"
As you replace jobs with native Buildkite Pipelines steps, the remaining supported workflow jobs can keep running through the plugin. If you want to convert a whole workflow instead, use the Buildkite pipeline converter.
How the plugin and runtime work
The plugin and the buildkite-gha runtime work together to run the workflow. This page calls the step that runs the plugin the importer step, and the jobs it creates the generated jobs. The minimal server-dispatch configuration doesn't require an explicit importer key.
Each part has a different job:
-
GitHub Actions Buildkite plugin: Reads your configuration, prepares
mise, then asks it to select and run the configuredbuildkite-gharelease. -
buildkite-gha: Checks that the workflow is supported, turns its jobs into Buildkite Pipelines command jobs, uploads them, and runs each generated job.
You don't need to install mise or buildkite-gha yourself. The plugin uses a compatible mise from PATH or installs a pinned, verified copy. Mise installs and verifies the runtime release for the importer's Linux x86-64 or macOS arm64 host, then caches the installation before running it. When a workflow needs another supported platform (Linux x86-64, Linux arm64, macOS arm64, or Windows x86-64), the plugin downloads and verifies the matching runtime from the same release.
Jobs that use JavaScript actions need mise 2026.5.12 or later. The runtime checks BUILDKITE_GHA_MISE, then PATH, and downloads and verifies a managed copy if neither provides a compatible version. A runtime image doesn't remove this requirement. Shell-only jobs and jobs that use only native adapters or Docker don't need mise. The validate and compile commands don't need it either.
The importer passes the runtime and compiled execution plans to the generated jobs using Buildkite Pipelines artifacts. Each job verifies these files before using them. This process doesn't create a corresponding workflow run in GitHub. Buildkite handles the schedule, logs, retries, cancellations, and build status.
GitHub Actions concepts map to Buildkite Pipelines as follows:
| GitHub Actions concept | Buildkite Pipelines behavior | ||
|---|---|---|---|
| GitHub Actions concept | Workflow run | Buildkite Pipelines behavior | The current Buildkite Pipelines build. |
| GitHub Actions concept | Job | Buildkite Pipelines behavior | A generated Buildkite Pipelines command job. |
| GitHub Actions concept | Matrix entry | Buildkite Pipelines behavior | A separate generated command job. A matrix taken from another job's output expands in a deferred step after that job runs. |
| GitHub Actions concept | needs |
Buildkite Pipelines behavior | Buildkite Pipelines step dependencies. |
| GitHub Actions concept | Steps within a job | Buildkite Pipelines behavior | Steps run together in one compatibility runtime and share a workspace and lifecycle. |
| GitHub Actions concept | Matrix or runner label from a job output | Buildkite Pipelines behavior | Jobs uploaded by a deferred step after the producing job runs. |
| GitHub Actions concept | runs-on |
Buildkite Pipelines behavior | A supported platform label. A Buildkite Pipelines queue mapping chooses the agent. |
| GitHub Actions concept | Deployment environment with required reviewers | Buildkite Pipelines behavior | A Buildkite Pipelines block step before the job. |
Requirements
The importer step and generated jobs have different host, tooling, network, and caching requirements.
Importer step requirements
Before it can download the runtime and create the workflow jobs, the importer step needs:
- A Linux x86-64 or native macOS arm64 agent. When configuring an importer step explicitly, select a compatible queue with its
agentsconfiguration. The plugin'srunnersconfiguration doesn't schedule the importer. - Buildkite agent v4, or v3.129.0 or later in the v3 release series. Agent v4 rejects pipeline uploads that contain secrets by default. Don't set
BUILDKITE_AGENT_PIPELINE_UPLOAD_ALLOW_SECRETSon the importer's agent. Runtime versions earlier than 0.46.2 use the--reject-secretsoption, which Agent v4 doesn't provide, so they require Agent v3. - Bash,
cp,curl,mktemp,tar, and eithersha256sumon Linux orshasumon macOS. The download tools are used only when a compatiblemiseisn't already onPATH. - Git when
BUILDKITE_COMMITisn't already a full commit SHA. - Outbound HTTPS access to public GitHub release and action sources.
Generated job requirements
Generated jobs need Buildkite agent v3.129.0 or later and an execution environment that matches their runner mapping. Linux x86-64 jobs can run on Buildkite hosted agents, the Agent Stack for Kubernetes, or other self-hosted agents that provide the tools used by the workflow. Linux arm64 jobs require an explicitly mapped Linux arm64 queue and buildkite-gha v0.97.0 or later. macOS jobs require a native macOS arm64 queue. Windows jobs require a Windows Server 2022 x86-64 queue. The runtime tells the agent to skip its usual repository checkout so that it can prepare the workflow's workspace instead.
Every Linux and macOS generated-job host needs Bash, buildkite-agent, mktemp, rm, awk, chmod, and either sha256sum or shasum -a 256. Depending on the workflow, it also needs:
-
gitavailable onPATHforactions/checkout, plus Git LFS when the action setslfs: true. - Docker available on
PATHfor Linux job containers, service containers, and Dockerfile actions. Dockerfile actions also require Docker Buildx. The default Buildx builder must use the localdockerdriver. macOS jobs don't support Dockerfile actions or other Docker capabilities. -
tarand either thezstdtool suite orgzipavailable onPATHforactions/cache.
With the default dedicated runner user, generated Linux job hosts also need getent, useradd, usermod, install, and sudo. If the Docker socket exists and its group doesn't exist, the host also needs groupadd.
During upload, configured runners mappings bypass automatic runner resolution. The runtime asks the job-scoped Agent API to resolve each remaining runs-on selector to a complete target: a queue, a platform, and, for Linux, either an immutable image or a native Linux host environment selected through agent tags. Exact supported Ubuntu selectors (ubuntu-latest, ubuntu-24.04, and ubuntu-22.04) resolve to the hosted Linux queue with the matching Ubuntu image and no warning. Other selectors that look Linux-compatible, such as older ubuntu-* versions or custom self-hosted labels, resolve to the hosted Linux queue using the latest Ubuntu image. The job shows a warning annotation recommending an explicit runner mapping instead. These automatic hosted resolutions require an eligible Buildkite hosted linux-medium Linux AMD64 queue in the job's cluster. Without that queue, selectors that don't have a runtime preset, including older Ubuntu selectors, require an explicit mapping. Linux arm64 selectors have no automatic resolution and require an explicit mapping. Windows selectors require Windows setup. Selectors for other incompatible platforms, such as macOS x86-64 or Windows arm64, still fail closed with an unmapped labels error.
If the Agent API rejects a selector, the importer reports the reason at the job's runs-on before it uploads the pipeline. If the Agent API can't be reached, an import without explicit mappings shows a warning and falls back to the runtime presets for ubuntu-latest, ubuntu-24.04, ubuntu-22.04, and macos-latest.
For macOS, macos-latest resolves to macos-medium. The versioned macos-14, macos-15, macos-26, and macos-27 labels resolve to the matching macos-<version>-medium queue when it exists in the job's cluster, then fall back to macos-medium. New Buildkite organizations include these four versioned queues, but they aren't automatically added to existing organizations. If neither suitable queue exists, add an explicit mapping. Every macOS target is native arm64. These labels don't guarantee the same operating system, installed software, or Xcode versions as GitHub-hosted runners, particularly when a versioned label falls back to macos-medium.
An Agent API result takes precedence over runtime presets. Add a runners entry to map a GitHub runner label explicitly to a Buildkite queue, select a pinned macOS version using a versioned queue, or avoid fallback warnings.
steps:
- label: ":github: GitHub Actions"
key: "github-actions"
agents:
queue: "importer-linux"
plugins:
- github-actions:
workflow: ".github/workflows/ci.yml"
runners:
- runs-on: "ubuntu-latest"
queue: "hosted"
- runs-on: "macos-14"
queue: "gha-macos-arm64"
Each entry requires runs-on and queue. A configured Ubuntu label uses the runtime's pinned default toolchain image unless you set image to another lowercase registry reference pinned to an immutable SHA-256 digest. The image must provide /opt/hostedtoolcache. Images are supported only on Buildkite hosted agents or Agent Stack for Kubernetes controller v0.30.0 or later. For other self-hosted Linux agents, omit the runner mapping and use the pipeline or organization's default agent targeting. Linux arm64, macOS, and Windows entries don't support image.
Before it uploads any jobs, the importer asks the job-scoped Agent API to validate each explicit mapping. The configured queue must be active in the job's cluster. A Buildkite hosted queue must also match the label's operating system and architecture. Self-hosted queues are checked for existence only, so make sure their agents match the mapped platform. If validation is unavailable, the import stops rather than trusting an unchecked mapping. Offline validate and compile commands don't check queues against the cluster.
Generated Linux jobs run as a dedicated runner user by default. The generated job must start as root, or as an existing runner user that has the home directory /home/runner and can use sudo without a password, so the runtime can provision this account. The runner user can use sudo without a password and access the Docker socket when it exists, so the account isn't a security boundary. Set experimental-runner-user: false only as a temporary compatibility measure. This runs generated jobs as the agent's original user without the runner user setup.
Because these queues can run untrusted workflow code, they must provide whole-job isolation, no ambient protected credentials, and a clean machine for each untrusted job. Persistent self-hosted agents can expose host resources and state left by earlier jobs.
The generated jobs also need network access for anything they download at runtime:
- Jobs that use public GitHub Actions need outbound HTTPS access to
codeload.github.com, where the runtime downloads each action's source archive. - Jobs that use JavaScript actions need outbound HTTPS access to the managed Node.js and
misedownload sources. Actions that declarenode16run on managed Node 16.20.2 and produce a deprecation warning. Actions that declarenode20ornode24run on managed Node 24.18.0. On Linux, managed Node binaries require glibc 2.28 or newer. Shell-only workflows don't have this glibc requirement.
When resolving a mutable tag or branch for a public action, the importer uses a dedicated action-source token only for public GitHub metadata requests and reuses it across the selected workflows and nested composite actions. Metadata requests for the repository that triggered the build and action archive downloads from codeload.github.com remain anonymous. If the importer can't obtain the token, it reports a warning and retries anonymously. A lowercase, full 40-character commit SHA doesn't require an API request.
Run Linux arm64 jobs
An explicit mapping declares that its label runs on Linux x86-64, except for known macOS labels, Windows labels, and these Linux arm64 labels: ubuntu-24.04-arm, ubuntu-22.04-arm, and labels ending in -arm64 or -aarch64. Linux arm64 labels have no default mapping, so map each one to your own Linux arm64 queue:
plugins:
- github-actions:
workflow: ".github/workflows/ci.yml"
runners:
- runs-on: "ubuntu-24.04-arm"
queue: "my-linux-arm64-queue"
Linux arm64 jobs require buildkite-gha v0.97.0 or later. The plugin downloads and verifies the Linux arm64 runtime from the same release only when a selected workflow needs it. A mapping doesn't create a queue, enable a Buildkite hosted Linux arm64 queue, or fall back to an x86-64 image or emulation. If you test unreleased runtime changes with source-ref, workflows with Linux arm64 jobs fail, because source builds don't include a Linux arm64 runtime.
Run Windows jobs
Windows Server 2022 x86-64 jobs are supported. As with other platforms, compatibility gaps might exist. To report an issue, open an issue in the buildkite-gha repository. The importer still runs on Linux or macOS. Generated Windows jobs run on a Windows Server 2022 x86-64 queue whose agents provide PowerShell 7 (pwsh) and Git on PATH. Runner labels don't install GitHub's runner image or its tools.
The windows-latest and windows-2022 labels have no default mapping and are rejected unless you choose one of these routes:
-
Explicit mapping: Map the workflow's label to an existing Windows Server 2022 x86-64 queue in the importer job's cluster:
plugins: - github-actions: workflow: ".github/workflows/ci.yml" runners: - runs-on: "windows-2022" queue: "my-windows-queue" Automatic routing: Contact the Buildkite Support team at support@buildkite.com to confirm that your organization has access to hosted Windows agents and Agent API routing. The importer job's cluster needs an active Buildkite hosted Windows AMD64 Medium queue named
windows-medium. When routing is enabled, the Agent API also mapswindows-2025and Depotdepot-windows-2025labels to this queue, with a warning that the queue might run Windows Server 2022 and doesn't preserve Depot hardware, caches, or networking.
Neither route creates queues or grants access to Windows hosted agents, which are in private preview. Don't change a Windows-only job's labels to Linux to get past runner validation.
Windows jobs default to the pwsh shell. They also support Windows PowerShell (powershell), MSYS2 custom shells, and JavaScript and composite actions. Write file commands, such as GITHUB_OUTPUT, in UTF-8. Windows PowerShell needs Out-File -Encoding utf8 -Append instead of its default UTF-16 redirection. The cmd shell, containers, services, Docker actions, custom images, and cache volumes aren't supported on Windows. See the Windows jobs reference for runner diagnostics and runtime boundaries.
Cache generated-job directories
An explicit Linux runners mapping can attach one Buildkite hosted cache volume to every generated job that uses the mapping:
plugins:
- github-actions:
workflow: ".github/workflows/ci.yml"
runners:
- runs-on: "ubuntu-latest"
queue: "hosted"
cache:
paths:
- "/home/runner/.gradle/caches"
- "/home/runner/.gradle/wrapper"
name: "gradle-${BUILDKITE_BRANCH}"
size: "40g"
The paths array is required and must contain unique absolute paths. The name and size attributes are optional. Cache volume sizes must be at least 20g. Generated jobs that use a job container, and Windows jobs, can't use this cache configuration.
Cache volumes are best-effort, pipeline- and cluster-scoped accelerators that are committed only after successful jobs. Treat their contents as untrusted executable state, and don't use them as durable storage. This cache is separate from the importer step's mise cache and the workflow's actions/cache behavior.
Cache mise installations
On Buildkite hosted agents, attach a mise data cache to avoid reinstalling mise and buildkite-gha:
steps:
- label: ":github: GitHub Actions"
key: "github-actions"
agents:
queue: "importer-linux"
cache: "/cache/bkcache/mise"
plugins:
- github-actions:
workflow: ".github/workflows/ci.yml"
Without this volume, mise uses the agent or user data directory. Treat the mise data directory as executable state. Don't share it with untrusted jobs or principals that can modify it. This importer cache is separate from generated-job runtime caching and the workflow's actions/cache behavior.
This example configures an explicit-workflow importer. If you add caching to a server-dispatch pipeline, omit workflow and workflows so the plugin uses the server's selection.
Supported functionality and limitations
The preview supports an evolving subset of GitHub Actions. The following lists summarize common supported features and limitations:
- Linux x86-64 jobs using
ubuntu-latest,ubuntu-24.04, orubuntu-22.04, and native macOS arm64 jobs usingmacos-latest,macos-14,macos-15,macos-26, ormacos-27. These labels identify a compatible platform, but don't give the agent the same tools, image layout, operating system, or Xcode installation as a GitHub-hosted runner. Otherruns-onlabels that look Linux-compatible, such as older Ubuntu versions, can use the latest supported Ubuntu image when the job's cluster has an eligible Buildkite hostedlinux-mediumLinux AMD64 queue. The job shows a warning annotation recommending an explicit runner mapping. Without an eligible queue, these labels require an explicit mapping. -
Linux arm64 jobs mapped explicitly to your own Linux arm64 queue, such as for
ubuntu-24.04-arm. -
Windows Server 2022 x86-64 jobs using
windows-latestorwindows-2022, with an explicit mapping or Agent API routing. - Bash,
sh,pwsh,powershell,python, and custom shell template run steps on Linux and macOS when the selected command is available onPATH. PowerShell isn't installed automatically. Windows jobs default topwsh, and also support Windows PowerShell and MSYS2 custom shells. Thecmdshell isn't supported. - Static job dependencies and matrices, including
include,exclude, and compile-time expressions such asfromJSON(), up to 256 expanded instances per job. Each expanded job can read thestrategy.job-index,strategy.job-total,strategy.fail-fast, andstrategy.max-parallelvalues in supported job and step fields. -
Matrices and runner labels from job outputs, such as
fromJSON(needs.plan.outputs.matrix), which expand in a deferred step after the producing job runs. - Supported job and step conditions, outputs, and timeouts. Job- and step-level
continue-on-errorcan use expressions that resolve to a Boolean value. Step-leveltimeout-minutescan use expressions that resolve to a number greater than zero and no more than 360. Job-leveltimeout-minutesaccepts literal values only. Expressions in supported fields can use fallbacks, such as${{ github.head_ref || github.ref_name }}. A tolerated job failure remains visible as a Buildkite soft failure, but downstream jobs receivesuccessthrough theneedscontext. Timeout cancellations and runtime infrastructure failures remain hard failures. - Public JavaScript, composite, and local actions on Linux, macOS, and Windows jobs, plus compiler-verified Dockerfile and public prebuilt-image actions on Linux. Prebuilt images are pulled anonymously. Pin the image by digest because a mutable tag can resolve to different content for each job.
- Self-repository actions, such as
uses: $/.github/actions/build, which download the action from the repository and exact commit containing the workflow withoutactions/checkout. - Local, literal public, and opt-in private reusable workflows, up to four nesting levels, including self-repository calls such as
$/.github/workflows/ci.yml. String inputs can embedneeds.<job>.outputs.<name>values from a direct dependency, and Boolean or number inputs can derive from them through a single expression. Local and verified self-repository calls can use one-hopsecrets: inheritwhen each nested call repeats it, or explicitly map a declared alias from one direct secret reference. - Linux job and service containers, including compile-time container image expressions and supported health checks, registry credentials, ports, volumes, and the
job.servicescontext. Job containers supportoptionsandvolumes. Serviceenvcan use job environment values and named secrets. A container image expression that resolves to an empty value runs the job on the host. - Literal deployment environments on top-level jobs, with required-reviewer approval gates and environment secret names, plus repository, organization, and environment
vars. -
hashFiles()in workflow step fields, step conditions, and JavaScript action lifecycle conditions. Each call accepts up to 255 patterns, scans up to 100,000 workspace entries, matches up to 10,000 files, and reads up to 1 GiB. It doesn't hash or follow symlinks. Multi-file hashes can differ from GitHub because this runtime uses lexical path order. -
toJSON(github)in supported step runtime fields. The runtime returns only its bounded GitHub context, applies normalGITHUB_TOKENauthorization, and redacts the token from logs and workflow outputs. - Direct, projected, whole, and dynamically indexed
github.eventaccess. Jobs that need a whole, projected, or dynamically selected value load a digest-verified event payload artifact from the importer. - The complete event payload in a job-scoped file at
GITHUB_EVENT_PATH, for builds with linked webhook data or an explicit event snapshot. Builds without linked webhook data, such as manual and scheduled builds, don't create this file.github.event_pathexpressions aren't supported. -
github.workspace,github.run_id,github.run_number, andgithub.run_attempt, with matchingGITHUB_WORKSPACE,GITHUB_RUN_ID,GITHUB_RUN_NUMBER, andGITHUB_RUN_ATTEMPTenvironment variables. Run identity values represent the Buildkite build ID, build number, and retry count plus one. They don't identify a GitHub Actions run or work in GitHub run URLs and APIs. -
actions/checkoutfor a detached checkout of the event repository at the exact commit that triggered the build or a static branch. Supported inputs include nested paths, partial clone filters, sparse checkout, Git LFS, and submodules. Checkout is anonymous for a public repository. For a private repository, it uses Buildkite's repository-provider Git credentials when they are enabled for the job and Buildkite authorizes the repository URL. - Statically resolvable workflow- and job-level
concurrency, including workflow-level concurrency in local and public called workflows. The runtime maps groups to repository-scoped Buildkite Pipelines concurrency groups. - Native-backed
actions/upload-artifactandactions/download-artifactfor known revisions. An unknown lowercase, full 40-character commit uses the current bounded adapter contract and produces a warning instead of running the action's JavaScript. -
actions/cachefor audited revisions, using the Buildkite Results service by default. A resolved commit outside the audited snapshot runs a substituted audited release instead. Workflow- and job-levelcache-modevalues ofread,write,write-only, andnonecontrol whether the cache client restores and saves, but aren't a security boundary. Only cache clients that honorACTIONS_CACHE_MODE, such as the auditedactions/cachecommit that bundles@actions/cache6.2.0, apply the mode. Theactions/cachev6.1.0 release and older bundled clients ignore it, so they can still save caches withcache-mode: read. The pipeline must use GitHub or Origin as its repository provider. Jobs must be able to reach the Results service and the Agent API. Buildkite mints a fresh cache token for each action phase, valid for up to six hours or the organization's maximum OIDC lifetime quota, whichever is lower. - Statically named Buildkite secrets in direct jobs and in local reusable workflow jobs using
secrets: inheritor explicit declared-alias mappings. - Opt-in temporary
GITHUB_TOKENand Buildkite-issued OIDC tokens within the documented authority boundaries.
Event payload artifacts are readable
An event payload artifact can be up to 25 MiB and follows the Buildkite build's artifact access and retention settings. The payload isn't redacted or a secret store. Anyone who can download the artifact can read its user-provided values. Tokens, resolved secrets, OIDC credentials, registry credentials, and internal admission data aren't added to it.
The runtime rejects many unsupported or privileged features before it uploads any jobs. However, some unsupported settings are ignored rather than rejected. Important limitations include:
- GitHub Enterprise Server repositories, non-GitHub repository providers, and private actions. Private reusable workflows require an explicit opt-in.
- Secret values stored in GitHub, ambient
GITHUB_TOKEN, alternate-repository checkout, tags, and arbitrary dynamic checkout commits. To copy GitHub Actions repository secrets into Buildkite secrets, see Migrate GitHub Actions secrets. - Windows arm64, Windows Server 2025, and macOS x86-64 jobs. Linux arm64 and Windows jobs need your own queues or Agent API routing.
- Dockerfile actions, job containers, service containers, and other Docker capabilities on macOS and Windows.
- Direct workflow
uses: docker://...steps, private prebuilt action images, and ambient Docker registry credentials. - Matrices derived from step outputs, or from anything other than exactly one
fromJSON(needs.<job>.outputs.<name>)expression, and dynamically selected reusable workflows. A job that takes its runner label from a job output can't also declare a matrix. - GitHub environment wait timers, deployment branch policies, custom protection rules, and deployment records. GitHub reviewer lists aren't enforced.
- The
pull_request_targetevent, which is intentionally unsupported for security reasons. - The matrix
strategy.fail-fastsetting. The runtime accepts this setting but doesn't enforce it, so a failed matrix job won't cancel the others. This differs from the GitHub Actions default. Iffail-fastcontains an expression, the workflow doesn't compile. - Exact upstream behavior for unaudited revisions of natively supported actions. Unknown immutable checkout and upload artifact commits use the v7.0.1 contract, while unknown immutable download artifact commits use the v8.0.1 contract. These warning-producing fallbacks can differ from the action's actual manifest. Unknown
actions/cachecommits run a substituted audited release. - The complete GitHub context and general emulation of GitHub services such as Packages, Releases, Checks, deployments, and GitHub artifact APIs.
Known preview gaps
You may need to update a workflow before you can run it during the preview:
-
Check the
actions/upload-artifactrevision and inputs: The native adapter supports known revisions from v1 through v7. An unknown lowercase, full 40-character commit uses the v7.0.1 contract with a warning. Known unsupported revisions, including v3.2.2, remain rejected. The v1 adapter accepts one literal file or directory. Later adapters accept up to 32 clean literal paths or bounded file globs using*,?, character classes, and recursive**. Jobs that run directly on the host can use workspace-relative or absolute paths, including Windows drive paths such asC:\build\dist\*.zip. An absolute glob archives files relative to its literal search root. Jobs that use a job container require workspace-relative paths. Exclusions, braces, extglobs, leading glob comments, traversing paths, symlinks, and special files aren't supported. Hidden-file behavior and accepted inputs depend on the action revision. The runtime acceptsretention-dayswhere the action declares it, but treats the value as advisory because Buildkite controls artifact retention. Each upload can contain up to 10,000 files, 1 GiB of source data, and a 1 GiB ZIP archive.
See the GitHub Actions compatibility reference for the complete supported functionality and limitations of buildkite-gha v0.102.1. If a feature isn't listed in the reference, treat it as unsupported.
Treat workflow code as build code
All steps in an imported job share a workspace, environment changes, processes, and action lifecycle. Docker actions and containers provide packaging, not a security boundary. Run imported jobs on a queue that provides whole-job isolation, no ambient protected credentials, a clean machine for each untrusted job, and host-level resource limits. Review the security model for the complete trust boundaries.
Matrices and runners from job outputs
A job's whole matrix, or its whole include list, can be exactly fromJSON(needs.<job>.outputs.<name>). The matrix then expands after the producing job runs:
jobs:
plan:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.plan.outputs.matrix }}
steps:
- id: plan
run: echo 'matrix=[{"target":"amd64","runner":"ubuntu-latest"}]' >> "$GITHUB_OUTPUT"
build:
needs: plan
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include: ${{ fromJSON(needs.plan.outputs.matrix) }}
steps:
- uses: actions/checkout@v7
- run: ./build "${{ matrix.target }}"
publish:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: ./publish
The initial upload creates plan and a deferred step labeled <img class="emoji" title="github" alt=":github:" src="https://buildkiteassets.com/emojis/img-buildkite-64/github.png" draggable="false" /> matrix ยท build. After plan runs, the deferred step reads the verified output, expands the matrix using the static-matrix rules, and uploads build and every job that depends on it, such as publish. The expanded jobs match the jobs a literal matrix with the same rows would produce. Row values that select runners still go through your runner mappings and Agent API resolution, so a producer can't select an unmapped queue or widen permissions. Deferred matrices can join and chain, including a matrix that reads its rows from a job that another deferred matrix created.
A job without a matrix can also select its runs-on label from one declared output of a prerequisite, for example, runs-on: ${{ fromJSON(needs.plan.outputs.runner) }}. The initial upload creates a deferred step labeled <img class="emoji" title="github" alt=":github:" src="https://buildkiteassets.com/emojis/img-buildkite-64/github.png" draggable="false" /> runs-on ยท build, which uploads the job and its dependents after the producer runs.
A matrix job can also take strategy.max-parallel, or a job-level concurrency group, from other declared outputs of the same producer.
These deferred uploads have the following limits:
- The producer must be a single-instance job on Linux or macOS, and must declare the output in
outputs. A matrix output is JSON of up to 1 KiB, with rows that are objects of up to 64 scalar values. A runner output is also limited to 1 KiB. - An expanded matrix can have up to 256 instances, and a workflow can generate up to 1,024 jobs in total, including deferred jobs.
- The workflow must be a tracked file whose content at the build commit matches the content that the importer compiled.
- If the producer fails, is skipped, or is cancelled, the deferred step uploads skipped placeholders for the deferred jobs. Dependents with
if: always()are also skipped. - A missing or invalid output, or a runner that fails admission, fails the deferred step without uploading its jobs.
- You can safely retry a deferred step. If you retry a producer job after its matrix has expanded, start a new build to expand the matrix again.
The compile command can't render pipeline YAML for a workflow with deferred uploads. Use validate or compile --format ir-json to inspect its job graph. See the matrices from job outputs reference for the complete rules.
Concurrency
The runtime turns each static concurrency group into a case-insensitive Buildkite Pipelines concurrency group scoped to the repository. Workflow-level groups use ordered opening and closing gates, while job-level groups use a concurrency limit of one.
Workflow-level groups can use supported github fields, vars, and static inputs in a called workflow. Job-level groups can also use concrete matrix and strategy values. A job whose matrix comes from a job output can also read its producer's outputs for its job-level group. Local and public called workflows retain their workflow-level concurrency gates, including nested gates. Each static call-matrix instance gets its own gate.
The workflow won't compile if a group can't be resolved. Called-workflow concurrency also isn't supported when a job or nested call reuses an enclosing workflow's concurrency group. A call with needs keeps its gate when its prerequisites have no concurrency or statically disjoint groups, and a call with if keeps its gate unless its condition is already false at compile time. In both cases, Buildkite Pipelines reserves the gate's queue position at pipeline upload, so a later call can wait behind an earlier one whose prerequisites or condition haven't been evaluated yet. See the concurrency reference for the complete rules and warnings. A workflow with workflow-level concurrency can't contain a matrix or runner label from a job output.
Workflow-level cancel-in-progress accepts literal values and expressions that resolve statically to a Boolean value. A resolved false is accepted without a warning. A literal or statically resolved true produces a warning but doesn't cancel an older build. Job-level cancellation remains unsupported.
Buildkite Pipelines queues every waiting entry, unlike GitHub's default behavior of replacing an existing pending entry. For an explicit-workflow pipeline, you can turn on Cancel Intermediate Builds and Skip Intermediate Builds in the pipeline's build settings for similar cancellation behavior. These settings work by branch, so they match a workflow concurrency group only when its scope follows the same branch boundaries. Leave both settings disabled for server-side dispatch, where they can cancel or skip sibling workflow builds from the same event.
Deployment environments and variables
Top-level jobs can declare a literal GitHub deployment environment, with or without a url. Expression-valued environment names and environments on reusable-workflow jobs aren't supported. When a workflow declares an environment, Buildkite reads the environment's protection rules, secret names, and variables from GitHub using its own credentials, restricted to the pipeline's repository. No GitHub token or secret value reaches the importer. Environments are supported only for github.com repositories. The Buildkite GitHub App installation needs the Actions: read and Environments: read permissions to resolve environments, and Variables: read to resolve variables. If your installation predates these permissions, a GitHub organization owner must approve them. If Buildkite can't resolve an environment, the workflow fails to compile rather than running without its protection rules.
Environment features map to Buildkite Pipelines as follows:
- Required reviewers: Become one block step per workflow and environment, which gates the affected jobs. Any user who can unblock the pipeline can approve. GitHub reviewer lists, self-review prevention, and administrator bypass aren't enforced. Other jobs keep running while approval is pending. You can't manually retry a gated job, so start a new build to get a fresh approval.
-
Environment secrets: A referenced secret
NAMEdefined in the environment resolves to the Buildkite secret<ENVIRONMENT>_<NAME>. For example, secretDEPLOY_KEYin environmentproductionresolves toPRODUCTION_DEPLOY_KEY. The prefix is the uppercase environment name, with every character other thanA-Z,0-9, and_replaced by_, and a leading_added when the name starts with a digit. For example, secretDEPLOY_KEYin environment1stresolves to_1ST_DEPLOY_KEY. Other secret names resolve unchanged. - Wait timers, deployment branch policies, and custom protection rules: Cause the workflow to fail to compile.
- Deployment records: Aren't created. Builds don't create GitHub deployments or deployment statuses.
${{ vars.NAME }} expressions resolve GitHub repository and organization variables, and environment variables for jobs that declare an environment. Job if conditions and compile-time fields, such as runs-on, strategy, concurrency, job names, and container images, see only repository and organization variables. Other job and step fields also see environment variables, which take precedence. A name that no scope defines evaluates to an empty string. Variable values are configuration, not secrets: they're stored in job plan artifacts and are visible to anyone who can download the build's artifacts.
Credentials, secrets, and OIDC
To check out the private repository that triggered the build, enable Buildkite's repository-provider Git credentials for the job. Buildkite must also authorize the repository URL. Without both, checkout is anonymous. Buildkite issues these repository-provider credentials only to command jobs running on Buildkite hosted agents, for pipelines connected through the full-access GitHub repository provider. This access doesn't provide GITHUB_TOKEN or github.token, and it can't be used for private actions or other repositories.
Direct jobs can use statically named ${{ secrets.NAME }} references. Local reusable-workflow calls, including verified self-repository calls such as $/.github/workflows/deploy.yml, can pass secret authority using one-hop secrets: inherit, which each nested call must repeat. A local call can instead map an alias declared by the called workflow from one direct ${{ secrets.NAME }} or ${{ secrets['NAME'] }} reference. Every required alias must be mapped, and an unmapped optional alias is empty. The runtime retrieves each value using the generated job's Buildkite secret access policy. Dynamic secret names, literal or compound mappings, and secret forwarding to remote reusable workflows aren't supported. These values are Buildkite secrets, not GitHub repository, environment, event, or fork-scoped secrets. Environment secret names map to prefixed Buildkite secret names.
GitHub doesn't let you read a secret's value after you save it. To copy GitHub Actions repository secrets into Buildkite secrets, see Migrate GitHub Actions secrets.
Buildkite Pipelines can provide a short-lived token for the repository that triggered the build. The pipeline's workflow access token setting must be enabled. GitHub Actions setup mode enables Allow workflow-authorized GitHub access tokens by default. Disable it in the pipeline's GitHub settings if your workflows don't need tokens. Adding the plugin to an existing pipeline doesn't enable this setting; configure it separately.
Workflow access tokens are available only to running command jobs on Buildkite hosted agents, in github.com pipelines connected through the full-access GitHub repository provider. Jobs on self-hosted agents can't receive a workflow access token.
The workflow file must be directly under .github/workflows/ and have a simple .yml or .yaml filename. The job must either reference secrets.GITHUB_TOKEN directly or use an action whose default input references github.token.
If the workflow doesn't include top-level permissions, the token receives only contents: read, regardless of the GitHub repository or organization defaults. A non-empty top-level permissions map replaces that default. The read-all alias expands to every supported read permission. Buildkite Pipelines rejects the write-all alias, so a workflow that uses it can't receive a token. List each required write permission in a top-level map instead. An empty map or a map containing only none doesn't produce a token. Job-level repository permission maps don't change the token scope. A job expanded from a local reusable workflow can receive a token, but its repository permissions always come from the top-level requesting workflow. The separate job-level id-token permission retains its documented behavior. Pull request builds and their triggered or rebuilt descendants can't receive more than contents: read. Merge queue builds and their descendants can't receive a token. The runtime doesn't add the token to the job's initial environment, although an action can make it available to later steps through GITHUB_ENV, as it can on a GitHub runner. An ambient GITHUB_TOKEN isn't available.
Each job can request up to 10 workflow access tokens per hour. Requests beyond this limit receive a 429 Too Many Requests response with a Retry-After header, and no token is issued. This limit is tracked separately for each job. When buildkite-gha retries a temporary token failure, each attempt counts toward this limit.
Protect tokens in trusted branch and manual builds
For builds outside pull requests and merge queues, a user who can create a build at an arbitrary commit may select code that requests the workflow's allowed permissions. Enable write tokens only when branch builds and other build-creation paths run trusted code.
For OIDC, the workflow job must declare permissions: id-token: write. Configure the plugin with the Buildkite claims that your identity provider accepts:
plugins:
- github-actions:
workflow: ".github/workflows/deploy.yml"
oidc:
claims:
- "organization_id"
aws-session-tags:
- "organization_slug"
- "pipeline_id"
subject-claim: "pipeline_id"
These OIDC tokens use the Buildkite issuer and claims, not the GitHub issuer. Host JavaScript actions, including JavaScript actions called by composite actions, can request them. Shell steps, Docker actions, and actions in job containers can't request OIDC tokens. The oidc plugin configuration doesn't grant access to a job that omits permissions: id-token: write.
Use OIDC with AWS
Imported workflows receive Buildkite-issued OIDC tokens, not GitHub-issued tokens. An AWS role that trusts only the GitHub issuer or matches the GitHub sub claim rejects them. To use an existing role from both systems:
- Register
https://agent.buildkite.comas another IAM OIDC provider with audiencests.amazonaws.com. - Add a separate trust-policy statement for the provider ARN
arn:aws:iam::AWS_ACCOUNT_ID:oidc-provider/agent.buildkite.com. - Match
agent.buildkite.com:audandagent.buildkite.com:subinstead of the equivalenttoken.actions.githubusercontent.comcondition keys. Scope the Buildkite subject to the intended organization and pipeline. - Keep the existing GitHub provider statement while workflows run in both systems. Remove it only when nothing still uses GitHub-issued tokens.
For example, the Buildkite statement can use these conditions. The sts:TagSession action is required when the plugin's oidc configuration sets aws-session-tags:
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::AWS_ACCOUNT_ID:oidc-provider/agent.buildkite.com"
},
"Action": [
"sts:AssumeRoleWithWebIdentity",
"sts:TagSession"
],
"Condition": {
"StringEquals": {
"agent.buildkite.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"agent.buildkite.com:sub": "organization:ORGANIZATION_SLUG:pipeline:PIPELINE_SLUG:*"
}
}
}
This sub pattern matches the default compound subject. If the plugin's oidc configuration sets subject-claim, such as subject-claim: "pipeline_id" in the previous section, the sub claim contains only that identifier, for example, the pipeline UUID. In that case, match it with a StringEquals condition on agent.buildkite.com:sub instead of this pattern.
Host JavaScript and composite actions, including aws-actions/configure-aws-credentials, can use the token endpoint. Use the plugin's oidc configuration, shown in the previous section, to add claims and AWS session tags, or to replace the default compound subject for every token minted by imported jobs. Each workflow job must still declare permissions: id-token: write to receive the endpoint.
See OIDC with AWS for the complete IAM configuration, and the OIDC claims reference for the full subject format and available claims.
Private reusable workflows
To call reusable workflows from the pipeline repository or other private GitHub repositories, set private-reusable-workflows: true:
plugins:
- github-actions:
workflow: ".github/workflows/ci.yml"
private-reusable-workflows: true
The importer resolves these workflows with its existing Git credentials, such as the Buildkite agent's repository-provider credential helper, which requests approval for each repository. Missing and denied repositories, refs, and paths produce the same error. Private actions remain unsupported, including private actions in a privately fetched reusable workflow.
When you enable this option, workflow source can select any workflow in any GitHub repository that the importer's Git credentials can read. Restrict the importer's Git credentials, or use the repository-provider access policy to approve only the required repositories.
Troubleshooting
Start with the Buildkite annotation. Its concise heading and message identify the user-visible cause and a corrective action or compatibility link. Expand Diagnostic detail for lower-level evidence, including resolved commits, adapter, service, and admission boundaries, and complete supported-value lists. Provider check summaries show concise guidance only.
When an upload contains several workflows, a parse, compilation, or trigger-translation failure in one workflow becomes a failing top-level replacement step. Other valid workflows continue. A parse failure in a single workflow, and event-input, admission, artifact, and upload failures, abort the transaction. A workflow with a matrix or runner label from a job output is never uploaded job by job: if any of its jobs fails to compile, one failing step replaces the whole workflow.
The importer can't verify path filters
The compatibility runtime supports bounded paths and paths-ignore filters for branch pushes and pull requests. The build must have a verified linked GitHub webhook, and the local checkout must provide complete diff evidence that matches the webhook. Generated or explicit event snapshots can't provide this evidence. Missing, shallow, mismatched, or uncertain evidence replaces the affected workflow with a failing top-level step instead of broadening when it runs.
The diagnostic detail identifies the missing evidence or the limit that was reached:
- Linked webhook data is missing: Use a build triggered by a GitHub push or pull request with its original webhook payload. Manual builds and explicit event snapshots can't provide this evidence.
-
The checkout is shallow: Path filtering runs before the workflow's
actions/checkoutstep, so that step can't fix the importer's checkout. Ifgit rev-parse --is-shallow-repositoryreturnstruein the importer's checkout, ask your agent administrator to fetch full history before the import. - The push exceeds 1,000 commits: Push in batches of 1,000 commits or fewer. Buildkite doesn't reproduce GitHub's fallback of running the workflow when a push exceeds this limit.
- The diff exceeds 3,000 changed files: Split the change into smaller diffs.
-
Pushed commit evidence, the push's
beforecommit, or rename conformance data is unavailable: Contact the Buildkite Support team at support@buildkite.com with the build URL and the diagnostic detail.
Removing paths or paths-ignore is a workaround that changes which workflows run, not a fix for missing evidence. If the evidence requirements don't suit the pipeline, remove the filters or use the Buildkite pipeline converter to translate path filtering to native if_changed conditions.
No workflow jobs appear
Check the importer log for missing or untracked workflow path warnings. The importer skips these paths. If every configured path is missing or untracked, the importer succeeds without uploading a pipeline.
Also check that the workflow declares the event represented by the Buildkite build. A workflow that doesn't declare the effective event appears as a top-level skipped step and in the importer-scoped informational annotation. If the importer reports Uploaded 0 jobs from N workflows, every selected workflow was skipped because it didn't match the effective event or its filters. For example, a workflow that declares only pull_request is skipped when the build represents a push event. Check the skipped top-level steps, then compare the effective event with the workflow's on trigger configuration.
A reusable workflow whose only trigger is workflow_call doesn't create its own group. Selecting only reusable workflows produces an error, but a reusable workflow can support another selected workflow. For explicit-workflow pipelines, native webhook and schedule settings create builds, and on determines eligibility within the build. For server-side dispatch, check Recent Deliveries first: only a matching supported workflow event creates a build.
macOS jobs wait for an agent
When a workflow uses a macos-* runner label and Buildkite Pipelines can't resolve a compatible queue, the importer reports No compatible runner is configured. If a converted pipeline already targets a queue without an available matching agent, the job remains in the Waiting for agent state.
As a temporary unblocker for a pipeline generated by the Buildkite pipeline converter:
- Create a Buildkite hosted queue in the same cluster as the pipeline.
- Set Machine type to macOS, select the required capacity, and configure a base image with the macOS and Xcode versions that the job needs.
-
Add the queue key as the
agents.queuevalue on each generated macOS command step. For example:steps: - label: "macOS tests" command: "bundle exec rake test" agents: queue: "macos-14-medium"
Buildkite hosted macOS agents support only Apple silicon. Converted macOS jobs must support the arm64 architecture. macOS queues don't support custom base images. A GitHub Actions macos-* label also doesn't guarantee the same tools, image layout, or Xcode installation on a Buildkite hosted agent. Review the macOS hosted agent images and software before selecting a base image.
For workflows that continue to run through the GitHub Actions Buildkite plugin, map the macos-* label to the new queue using the plugin's runners configuration, rather than editing the dynamic pipeline.
A runner is rejected before upload
The importer validates every runner target before it uploads any jobs, and reports the reason at the job's runs-on in the workflow diagnostics annotation:
-
missing_queue: The labels are compatible, but the job's cluster has none of the Buildkite hosted queues they need. Create the named queue or configure an explicit runner mapping. -
queue_not_found: An explicitly mapped queue isn't active in the job's cluster. Correct the mapping or create the queue. -
queue_platform_mismatch: An explicitly mapped Buildkite hosted queue has a different operating system or architecture from the label. Map the label to a compatible queue. -
incompatible_labels: The selector is unsupported, or your organization doesn't have access to hosted Windows agents. For Windows labels, review the Windows routes before changing the labels. -
no_cluster: The job isn't in a cluster, so no hosted queue can be selected. Move the pipeline to a cluster that contains compatible queues.
Explicit mappings also require the importer job to reach the job-scoped Agent API. If validation is unavailable, the import stops instead of trusting an unchecked mapping.
GitHub Actions setup mode isn't available in the legacy Pipelines UI
Buildkite organizations that use the legacy new pipeline page don't see GitHub Actions setup mode, and can't switch to the new Pipelines UI themselves. You can still configure the GitHub Actions Buildkite plugin and trigger manually. Contact the Buildkite Support team at support@buildkite.com to request access.
The new pipeline page shows a repository access notice
If Buildkite doesn't have code access to the selected repository, the new pipeline page doesn't try to detect workflows. Instead, the page shows a notice explaining that Buildkite can't scan the repository. For a GitHub (Limited Access) connection, the notice includes a Manage GitHub access link when GitHub provides one. This connection can't provide code access, so connect the repository using the full-access GitHub repository provider, then select it in the repository picker. If a repository is missing from an existing full-access GitHub App installation, select GitHub settings in the repository picker to add it. The access notice can also appear without an action if Buildkite can't access a repository during a full-access scan.
Other scan failures show a Buildkite couldn't scan this repository notice with a Try again option. Select Try again to retry the scan without reloading the page. If the scan keeps failing, check the Buildkite GitHub App installation:
- Confirm that the installation has access to the affected repository.
- Confirm that the installation has the required
contents: readpermission.
GitHub variables can't be resolved
If the importer can't read GitHub variables, every workflow that references vars fails to compile with an E_VARIABLE_RESOLUTION diagnostic, while other workflows still upload. When the diagnostic includes a specific policy, permission, installation, or repository-access error, correct that cause. For example, confirm that the Buildkite GitHub App installation has the Variables: read permission. If it only reports GitHub variables could not be resolved, contact the Buildkite Support team at support@buildkite.com with the build URL.
Validate a workflow locally
For most workflows, use the plugin. If you need more control or want to diagnose a problem, install buildkite-gha using mise 2026.5.12 or later. See Use the buildkite-gha CLI for every command, option, and report format:
mise use -g --minimum-release-age 0s github:buildkite/buildkite-gha
You can then check a workflow's syntax, declared triggers, and static job graph without running it:
buildkite-gha validate .github/workflows/ci.yml
To resolve actions and apply the production upload policy for each declared supported event, use the hosted profile:
buildkite-gha validate \
--profile hosted \
--all-events \
.github/workflows/ci.yml
An admitted result means the workflow satisfies the upload policy. A not-applicable result means the workflow doesn't declare the selected event, so upload would skip it without compiling it. This check doesn't run arbitrary action code or prove that every GitHub service an action uses is compatible. A context-required result means compilation and policy checks passed, but the generated event doesn't provide evidence required for admission. For example, path filters require a linked webhook and a verified local Git diff. Use --event-path with a bounded event snapshot when exact refs, activity types, repository identity, or payload fields matter. You can also use --event issues to validate against a representative opened issue event. Generated snapshots test compatibility, but don't prove support for every activity type or admission requirement.
The CLI also provides compile and upload commands. Pass one or more explicit workflow paths to upload, with each path as a separate argument:
buildkite-gha upload \
--runner-queue ubuntu-latest=hosted \
--runner-queue ubuntu-24.04-arm=my-linux-arm64-queue \
--runner-queue macos-14=gha-macos-arm64 \
--runtime-distribution linux/arm64=/opt/buildkite-gha-linux-arm64 \
--runtime-distribution darwin/arm64=/opt/buildkite-gha-darwin \
-- \
.github/workflows/ci.yml \
.github/workflows/release.yml
The -- separator is required when a path begins with -.
The validate and compile commands don't use mise after you install the CLI, and they don't run workflow code. Each command produces a processing report with the status of each validation and generation stage. Use validate --format json for a machine-readable report. In each diagnostic, message provides actionable guidance, optional detail provides lower-level evidence, and the stable diagnostic code, stage, and location remain separate. Use the compatibility reference as the authority for compatibility rules.
The compile command writes its report to standard error, while upload writes it to the importer job log. When these commands run in a Buildkite job, they also publish processing warnings and errors as job-scoped annotations. A failure to publish an annotation produces a warning but doesn't change the command result. Stages blocked by an earlier failure are reported as not-evaluated, not failed. If a required stage fails, the runtime doesn't publish plans or pipeline output.
Run upload from a keyed Buildkite Pipelines command step so that the BUILDKITE and BUILDKITE_STEP_KEY environment variables are available. A custom importer can run on Linux x86-64, Linux arm64, or macOS arm64. The step must use Buildkite agent v4, or v3.129.0 or later in the v3 release series. A workflow with a matrix or runner label from a job output also needs BUILDKITE_JOB_ID.
As with the plugin, generated jobs manage their own mise setup only when their actions need it. For a custom importer, use repeatable --runner-queue options to map runner labels to queues. Linux x86-64 mappings can use --runner-image with an immutable image digest. The importer provides the runtime for its own platform by default. To run jobs on another platform, provide that platform's runtime executable from the same release with --runtime-distribution, for example, linux/arm64, darwin/arm64, or windows/amd64. Verify a downloaded release archive against that release's checksum file before you extract the executable. The plugin handles the runtime downloads and applies your runners configuration for you, which is why it's the best option for most workflows.
Next steps
- Learn how to migrate from GitHub Actions.
- Migrate GitHub Actions secrets to Buildkite secrets.
- Check every supported workflow feature in the GitHub Actions compatibility reference.
- Validate, compile, and upload workflows with the buildkite-gha CLI.
- Review the security model before enabling private access, write tokens, or caching.
- Translate a GitHub Actions workflow to native Buildkite Pipelines configuration.
- Learn more about using plugins.