Use the buildkite-gha CLI
The GitHub Actions Buildkite plugin is the easiest way to run buildkite-gha. Use the CLI directly when you need to validate a workflow, inspect generated output, or build a custom importer.
| Command | Use it to | ||
|---|---|---|---|
| Command | validate |
Use it to | Check one workflow and, optionally, production policy. |
| Command | validate-batch |
Use it to | Check a large workflow corpus. |
| Command | compile |
Use it to | Render pipeline YAML or compiler IR without uploading it. |
| Command | upload |
Use it to | Upload workflows from a custom importer. |
| Command | help <command> |
Use it to | Print the usage and description of one command. Run buildkite-gha help without a command, or buildkite-gha --help, to list all commands. |
| Command | --version |
Use it to | Print the installed buildkite-gha version. |
The run-job command and the upload --stage-digest form are internal commands that generated steps run. Don't invoke them directly.
Before you begin
Install buildkite-gha with mise 2026.5.12 or newer:
mise use -g --minimum-release-age 0s github:buildkite/buildkite-gha
The override avoids the default 24-hour release delay in mise. Without it, mise may select an older release that has no artifact for your platform.
Append @<version> to install an exact release. If a custom importer creates jobs for the other supported platform, it must download and verify that platform's distribution from the same release.
Jobs with JavaScript actions need mise. The runtime checks BUILDKITE_GHA_MISE, then PATH, then downloads a verified managed copy. Shell-only, native-adapter, and Docker-only jobs don't need it.
When a managed cache is configured, the runtime installs Node there rather than reusing system-wide mise installations. The install directory remains pinned even when an agent's mise wrapper overrides MISE_DATA_DIR.
Managed Node binaries require glibc 2.28 or newer. The Go CLI has no glibc requirement.
Validate a workflow
Validate syntax, the static graph, and every declared trigger without an event:
buildkite-gha validate .github/workflows/ci.yml
This event-independent check validates syntax, triggers, and the static graph. It accepts valid push and pull request path filters because upload can evaluate them later with linked-webhook data and a verified local Git diff.
It doesn't:
- resolve actions
- evaluate event payload expressions
- claim that production policy would admit the workflow
Malformed filters, unsupported filter combinations, and unsupported pull request activity types still fail. See Workflow names and triggers for the exact trigger contract.
Resolve actions and apply production policy:
buildkite-gha validate \
--profile hosted \
--event-path .buildkite/events/current.json \
.github/workflows/ci.yml
For a quick compatibility check, generate a minimal supported event snapshot:
buildkite-gha validate \
--profile hosted \
--event pull_request \
.github/workflows/ci.yml
The --event option supports push, pull_request, merge_group, release, deployment, deployment_status, create, delete, label, fork, public, gollum, page_build, watch, milestone, branch_protection_rule, discussion, discussion_comment, issues, issue_comment, pull_request_review, pull_request_review_comment, workflow_dispatch, and schedule. It requires --profile hosted and can't be combined with --event-path.
The generated snapshot contains an example repository and the minimum event fields. It's useful for a quick check, but it's not a real payload. The release snapshot represents one stable, non-prerelease published event. The issues snapshot represents opened, review represents submitted, and both comment events represent created. Deployment snapshots use the staging branch and preview environment; deployment status represents success. Use --event-path when exact refs, activity, repository identity, or payload fields matter.
Use --all-events with --profile hosted to evaluate each declared supported event with its own generated snapshot:
buildkite-gha validate \
--profile hosted \
--all-events \
.github/workflows/ci.yml
The --all-events option can't be combined with --event or --event-path. It doesn't evaluate workflow_call as a standalone event.
Each generated event is checked separately. The result doesn't cover every possible real payload. context-required means the available checks passed, but admission needs evidence the generated snapshot can't provide. Push and pull request path filters, for example, need linked-webhook data and a verified local Git diff.
Reuse downloaded immutable action source across profile validation runs:
mkdir -p .buildkite-gha-action-cache
buildkite-gha validate \
--profile hosted \
--all-events \
--action-cache-dir .buildkite-gha-action-cache \
.github/workflows/ci.yml
The --action-cache-dir option is available only with --profile hosted. It stores verified action source by immutable commit.
Mutable ref resolutions are cached for one hour under $XDG_CACHE_HOME/buildkite-gha/action-ref-resolutions/v1, or the platform's user cache directory. Concurrent validators can share this cache, so a moved tag or branch may use its previous commit for up to one hour. Uploads with private-reusable-workflows enabled skip this cache for called repositories and resolve their refs once per operation. Don't share either writable cache between untrusted validation jobs.
Validate options
| Option | Description | ||
|---|---|---|---|
| Option | --profile hosted |
Description | Resolves actions and applies production upload policy without executing jobs or proving arbitrary action runtime compatibility. Without --profile, validate checks event-independent syntax, the static graph, and every declared trigger, and doesn't evaluate hosted admission. |
| Option | --event <name> |
Description | Generates one minimal compatibility snapshot for the named event. Requires --profile hosted. |
| Option | --event-path <path> |
Description | Uses an exact event snapshot. |
| Option | --all-events |
Description | Evaluates every declared supported event separately. Requires --profile hosted. |
| Option | --action-cache-dir <path> |
Description | Reuses verified immutable action source. Requires --profile hosted. |
| Option | --format <format> |
Description | Selects text (the default) or json output. |
The --event, --event-path, and --all-events options are mutually exclusive. Generated snapshots are test inputs, not substitutes for real payloads.
Validate a workflow corpus
For a large workflow corpus, reuse one validator process and action resolver:
buildkite-gha validate-batch \
--manifest workflows.jsonl \
--output-dir reports \
--corpus-id zenodo:20340547 \
--action-cache-dir .buildkite-gha-action-cache \
--action-cache-max-bytes 21474836480 \
--action-resolution-snapshot .buildkite-gha-action-resolutions
Each JSON Lines manifest record requires id, repository, path, hash, and source. Batch validation:
- applies the hosted profile to every declared supported event
- writes one
processing-report/v3JSON file per workflow - uses one worker per CPU unless
--jobsoverrides it - publishes each report atomically
- resumes reports only when the corpus, record, workflow dependencies, validator executable, and action-resolution generation still match
Records with unresolved local dependencies are processed again.
The --action-cache-max-bytes option requires --action-cache-dir. When the cache exceeds the limit, validation evicts the least recently used immutable action trees. Concurrent validators lock active entries. Maintenance removes abandoned partial entries but leaves active ones alone. The public corpus script defaults to 20 GiB.
The required --action-resolution-snapshot option pins each mutable owner/repository@ref to the first commit it resolves. Reusing the same generation keeps those refs stable across validator versions. Exact commit references bypass the snapshot.
The snapshot records definitively missing public refs. It doesn't record network, cancellation, TLS, rate-limit, or server failures; those are retried. Rate-limited requests follow the retry deadline of the action resolver. Use --refresh-action-resolution-snapshot to start a new generation. The public corpus script then removes old report sets for that corpus record.
The snapshot pins action revisions only. It doesn't make the whole corpus run reproducible.
For authenticated GitHub API resolution, keep the token in an environment variable and name that variable without exposing its value:
GITHUB_TOKEN="$(your-secure-token-command)" \
buildkite-gha validate-batch \
--manifest workflows.jsonl \
--output-dir reports \
--corpus-id example \
--action-resolution-snapshot .buildkite-gha-action-resolutions \
--github-token-env GITHUB_TOKEN
The token authenticates GitHub API metadata requests only. It's not written to arguments, logs, reports, snapshots, or action caches. Validation doesn't run action subprocesses, and it still verifies that every action repository is public.
Batch validation options
| Option | Description | ||
|---|---|---|---|
| Option | --manifest <path> |
Description | Required. The newline-delimited JSON manifest. |
| Option | --output-dir <path> |
Description | Required. The directory for processing-report/v3 results. |
| Option | --corpus-id <id> |
Description | Required. The corpus ID that keys results. |
| Option | --action-resolution-snapshot <path> |
Description | Required. The snapshot that pins mutable public action refs on first use. |
| Option | --refresh-action-resolution-snapshot |
Description | Starts a new snapshot generation. |
| Option | --action-cache-dir <path> |
Description | Reuses verified immutable action source. |
| Option | --action-cache-max-bytes <bytes> |
Description | Limits the action cache size. Requires --action-cache-dir. |
| Option | --github-token-env <name> |
Description | Reads a GitHub token from the named environment variable without placing it in arguments or reports. |
| Option | --jobs <count> |
Description | Sets the number of workers. Defaults to one worker per CPU. |
Inspect validation results
Inspect the aggregate result and each event outcome:
buildkite-gha validate \
--profile hosted \
--all-events \
--format json \
.github/workflows/ci.yml |
jq '{result, events: [.evaluations[] | {event, result: .report.result}]}'
Inspect diagnostics with their generated event names:
buildkite-gha validate \
--profile hosted \
--all-events \
--format json \
.github/workflows/ci.yml |
jq -r '(.validation.diagnostics[] | "validation: \(.code): \(.message)"),
(.evaluations[] | .event as $event | .report.diagnostics[] | "\($event): \(.code): \(.message)")'
The deprecated hosted-tokenless profile name remains an alias for hosted. Hosted validation uses the same runner preset as production upload.
Use --format json for a buildkite-gha/processing-report/v2 report. The --all-events option emits v3, containing the event-independent report and one v2 report per generated event.
The top-level result is:
-
admittedonly when every event is admitted -
context-requiredwhen generated input can't measure an otherwise supported path, unless another finding takes precedence
Reports cover every stage from parsing through pipeline generation. If an earlier stage blocks a later one, the later stage is not-evaluated, not failed.
Warnings and errors become job-scoped Buildkite annotations. A failure that aborts validate, compile, or upload attaches to the current job. Generated failure steps attach their own diagnostics. Their logs identify the root workflow and the source location, job, matrix instance, action, and step of each diagnostic when available. Failure logs use bold red errors, amber warnings, and cyan workflow and source context, with blank lines between diagnostics. Importer logs and generated failure logs share annotations' human-readable explanations, source excerpts, and diagnostic details. Internal diagnostic codes remain in structured reports and telemetry rather than these logs. Warning-only importer output uses an amber Workflow diagnostics heading. Untrusted terminal control characters are removed without changing the underlying report data. If the CLI can't publish an annotation, it warns without changing the command result.
Repository source setup failures in uploads and all-events validation use E_ENVIRONMENT. Their diagnostic detail names the failed local operation and recognized causes, such as missing Git or unavailable temporary storage, without copying paths or arbitrary error text. Remediation applies if you manage the environment running the command; guidance for Buildkite hosted agents directs you to the Buildkite Support team at support@buildkite.com. Unrecognized causes identify the operation and direct you to support rather than guessing a network or credential problem.
For fetched public and private reusable workflows, source locations in annotations link to the resolved commit and line in the source repository, including nested local calls inside that repository. The link opens only for viewers with GitHub access to that repository. Generated failure logs make the source path and coordinates an OSC 8 terminal hyperlink to the same URL, without a separate URL line. Terminals without hyperlink support display the label. If the source couldn't be fetched, the CLI keeps the location without guessing a revision.
Local workflow links use the event's commit only when its file in the checkout's Git object database matches the bytes parsed. This includes local reusable workflows and early syntax errors. Logs and annotations display local source paths relative to the checkout when possible. They keep the location without a link for edited inputs, unavailable revisions, files outside the checkout, or files larger than the 1 MiB verification limit. Changes on disk after parsing don't change which source revision the diagnostic links to.
Annotations and generated failure logs include a real configuration excerpt where safe: literal action and workflow references in uses, standard Ubuntu, Windows, or macOS runs-on labels, and built-in step shell names. Excerpts retain the parsed line numbers, mark the offending line with >, and underline the reference, runner label, or shell with ^. Standalone trigger filter keys, such as branches: and types:, also appear with an underline. Filter values and inline filter declarations are omitted. Rejected filters, invalid filter patterns, and unsupported activity types link to their filter key. Errors without a corresponding field retain the event declaration location. Only eligible adjacent lines are included. Scripts, env, with, comments, expressions, aliases, malformed YAML, and other unclassified content are omitted. Capture is limited to 240 bytes per line and 16 KiB per workflow; excerpts are excluded from JSON reports and telemetry. At annotation size limits, the excerpt is dropped before shortening the explanation.
Profile validation applies the upload trigger policy before compilation. not-applicable means the workflow doesn't declare the selected event and would become a skipped top-level step. Malformed event data is incompatible. An unsupported trigger beside a supported one produces a warning.
Validation may use the public network to resolve actions. It doesn't install Node or execute workflow code. It calls Buildkite only to publish annotations when it runs inside a Buildkite job.
Migrate GitHub Actions secrets
buildkite-gha v0.102.0 removed the migrate-secrets command, and buildkite-gha migrate-secrets now fails with unknown command "migrate-secrets". To copy GitHub Actions repository secrets into Buildkite secrets, use the bk secret migrate github-actions command of the Buildkite CLI instead. See Migrate GitHub Actions secrets.
If you already prepared a migration workflow with buildkite-gha migrate-secrets v0.90.0 or later, you don't need to regenerate or recommit it. Run it with bk secret migrate github-actions run --workflow <path>.
Provide an event snapshot
The compile command, and profile validation with --event-path, need a bounded event snapshot:
{
"provider": "github",
"event": "push",
"repository": {
"owner": "acme",
"name": "widgets",
"clone_url": "https://github.com/acme/widgets.git",
"default_branch": "main"
},
"ref": "refs/heads/main",
"sha": "0123456789abcdef0123456789abcdef01234567",
"actor": "octocat",
"payload": {
"ref": "refs/heads/main"
}
}
The snapshot supplies compile-time context. Plans retain the event name, repository, refs, SHA, actor, and a payload digest. Upload stores the snapshot's payload once as a content-addressed artifact and marks each job to load it for GITHUB_EVENT_PATH, even without event expressions.
The snapshot is compatibility data, not authorization.
Compile a pipeline
Render Buildkite pipeline YAML:
buildkite-gha compile \
--event-path .buildkite/events/current.json \
.github/workflows/ci.yml
Inspect compiler IR:
buildkite-gha compile \
--event-path .buildkite/events/current.json \
--format ir-json \
.github/workflows/ci.yml
The --event-path option is required. The --format option accepts pipeline (the default) or ir-json. Pipeline output references content-addressed plans.
Inside a Buildkite job, the IR includes the resolved repository and organization variables when the workflow references vars.
Workflows whose jobs declare a GitHub environment need GitHub environment access at compile time. Inside a Buildkite job, upload and compile resolve environments automatically through the job-scoped Agent API; no GitHub token reaches the importer. Outside a job, compile fails for such workflows with an error naming the job and its environment; there is no token option.
The compile command doesn't upload the executable, plans, or pipeline, so piping its YAML directly to buildkite-agent pipeline upload is incomplete.
A workflow with a matrix or runner selection from a job output has jobs that only exist after a deferred step runs inside the build, so compile renders its IR but not its pipeline YAML:
buildkite-gha: compile: job "build" takes its matrix from a job output, so upload expands it with a deferred step inside the build; the pipeline format cannot render it. Use --format ir-json to inspect the compiled graph.
The IR lists the deferred step and its jobs under continuations.
Upload from a custom importer
The upload command is the public in-build command for custom importers:
buildkite-gha upload .github/workflows/ci.yml
The importer must run on Linux/amd64, Linux/arm64, or Darwin/arm64 with Buildkite agent v3.129 or newer, BUILDKITE=true, and BUILDKITE_STEP_KEY. The plugin wrapper starts the importer only on Linux/amd64 or Darwin/arm64.
Upload options
| Option | Description | ||
|---|---|---|---|
| Option | --event-path <path> |
Description | Uses an explicit event snapshot. See Select the effective event. |
| Option | --runner-queue <runs-on>=<queue> |
Description | Repeatable. Maps one supported runs-on label to a Buildkite queue and overrides automatic resolution. Duplicate or unsupported mappings fail. |
| Option | --runner-image <runs-on>=<immutable-image> |
Description | Repeatable. Overrides the preset image for a configured profile with an immutable image. |
| Option | --runtime-distribution <platform>=<absolute-path> |
Description | Repeatable. Binds linux/amd64, linux/arm64, darwin/arm64, or windows/amd64 to a verified executable. |
| Option | --private-reusable-workflows |
Description | Enables importer-only Git fallback for private reusable workflows with the job's existing Git credentials. It doesn't enable private actions. |
| Option | --experimental-runner-user=<boolean> |
Description | Set to false to temporarily disable the non-root runner identity for generated Linux jobs. See Run Linux jobs as a non-root user. |
| Option | --runtime-queue hosted |
Description | Deprecated. Accepted for plugin compatibility, but doesn't select a queue. |
| Option | --private-checkout |
Description | Deprecated. Accepted as a no-op. Verified checkout jobs automatically use Buildkite repository-provider Git credentials when the job enables them. |
| Option | -- |
Description | Ends option parsing. See Select workflows. |
Plugin entry point
The hidden, zero-argument buildkite-gha plugin entry point reads plugin configuration from BUILDKITE_PLUGIN_CONFIGURATION. It accepts:
- either one
workflowpath or a non-emptyworkflowsarray -
runnersandoidc - plugin-owned
version,source-ref, andminimum-release-agefields - the Boolean
experimental-runner-userandprivate-reusable-workflowsfields
| Field | Description | ||
|---|---|---|---|
| Field | workflow |
Description | One non-empty workflow path. Mutually exclusive with workflows. |
| Field | workflows |
Description | A non-empty array of non-empty workflow paths. Mutually exclusive with workflow. |
| Field | runners |
Description | A non-empty array of runner mappings. Each mapping requires runs-on and queue strings, and accepts an optional image (an immutable registry sha256 reference) and an optional cache object. Each runner label can be configured only once. Windows mappings reject cache. |
| Field | oidc |
Description | An object that accepts non-empty claims and aws-session-tags arrays of non-empty strings, and a non-empty subject-claim string. |
| Field | experimental-runner-user |
Description | A Boolean. Defaults to true. |
| Field | private-reusable-workflows |
Description | A Boolean. Defaults to false. |
| Field |
version, source-ref, minimum-release-age
|
Description | Plugin-owned fields. |
Unknown fields fail.
Pipeline trigger selection
Server-selected workflow imports run in builds created by a GitHub Actions pipeline trigger. To set up a GitHub Actions pipeline trigger, see Trigger builds from workflow events.
Without an explicit selector, BUILDKITE_GITHUB_WORKFLOW_PATH marks a GitHub Actions pipeline trigger selection. The server also supplies:
-
GITHUB_EVENT_NAME: One ofpush,pull_request,issues,issue_comment,pull_request_review,pull_request_review_comment,release,merge_group,deployment,deployment_status,create,delete,label,fork,public,gollum,page_build,watch,milestone,branch_protection_rule,discussion, ordiscussion_comment. -
GITHUB_WORKFLOW: The workflowname, or its repository-relative path whennameis absent. -
GITHUB_WORKFLOW_REF: The value<owner>/<repo>/<repository-relative-path>@<event-ref>. -
GITHUB_WORKFLOW_SHA: The full commit used to match the workflow. -
BUILDKITE_GITHUB_EVENT: A compatibility duplicate ofGITHUB_EVENT_NAME. -
BUILDKITE_GITHUB_ACTION: The event activity. Push and deployment events, pluscreate,delete,fork,public,gollum, andpage_build, omit it.
The GITHUB_* values take precedence when present. The plugin derives the selected path from GITHUB_WORKFLOW_REF, checks GITHUB_WORKFLOW against the checked-out file, and requires GITHUB_WORKFLOW_SHA to match the checkout commit. A malformed preferred value fails instead of falling back.
For pull requests, GITHUB_WORKFLOW_REF and imported jobs' GITHUB_REF retain refs/pull/<number>/merge, while GITHUB_WORKFLOW_SHA, GITHUB_SHA, and the Buildkite checkout use the pull request head commit. Review and inline review-comment events use the same PR-head contract. They require both workflow identity fields and the original buildkite:webhook payload. The PR number, head SHA, head and base branches, activity, and all three repository identities must agree with the build. Missing payloads (including rebuilds without retained webhook data) fail closed, not as synthetic PR events.
For issues and issue_comment, the ref is the current repository default branch and the SHA is its server-verified tip. Both identity fields are required. The linked payload action and repository must match the Buildkite environment, and issue_comment accepts both issue and pull request conversation comments.
For release, both workflow identity fields and the original linked payload are required. The ref identifies the release tag; the SHA identifies its server-resolved peeled commit. Repository, tag, branch, and activity must agree. Draft releases reject created, edited, deleted, and unpublished. See release compatibility.
Deployment events require both workflow identity fields and the original linked payload. Workflows use the deployment commit and branch or tag ref, or an empty Actions ref for SHA-only deployments. SHA-only workflow identity uses @<sha>. See deployment compatibility for provenance checks and inactive-status suppression.
For merge_group, both workflow identity fields and the original linked payload are required. The selected ref and SHA identify the speculative head; the distinct base branch and SHA must match the Buildkite merge-queue metadata. BUILDKITE_GITHUB_ACTION must match the payload's actual checks_requested or destroyed action. Only tokenless workflows are supported. See merge-group compatibility and the destruction rollout boundary.
BUILDKITE_GITHUB_WORKFLOW_PATH remains the path fallback because GitHub has no GITHUB_WORKFLOW_PATH. BUILDKITE_GITHUB_ACTION remains the action source because the GitHub GITHUB_ACTION variable has a different meaning. An explicit workflow or workflows value takes precedence over server workflow selection.
The plugin also validates these values before upload:
-
BUILDKITE_GITHUB_WORKFLOW_PATHmust be a non-empty path without surrounding whitespace. If any ofGITHUB_EVENT_NAME,GITHUB_WORKFLOW,GITHUB_WORKFLOW_REF, orGITHUB_WORKFLOW_SHAis set without it, the import fails. -
GITHUB_EVENT_NAMEorBUILDKITE_GITHUB_EVENTis required. -
GITHUB_WORKFLOW_REFmust name the same repository as agithub.comBUILDKITE_REPO, and its event ref must match the event:refs/heads/orrefs/tags/forpushandcreate;refs/pull/<number>/mergefor pull request and review events;refs/tags/forrelease;refs/heads/for the other events. Deployment events also accept a full commit SHA. -
GITHUB_WORKFLOW_SHAmust be a full lowercase 40-character hexadecimal commit. Every event other thanpushandpull_requestrequires bothGITHUB_WORKFLOW_REFandGITHUB_WORKFLOW_SHA.
Use the plugin shorthand to request server selection:
steps:
- label: ":github:"
plugin: github-actions
This server-selected form doesn't require the importer step to have a key. The plugin uploads the event, runtime, and plan artifacts before uploading the dynamic pipeline, and scopes artifact reads to the importer job. It doesn't use the job ID as a dependency key. Explicit-selector importers still require a step key, and generated workflow groups depend on it.
Missing or untracked explicitly configured workflow paths warn and are skipped. If every configured path is missing or untracked, the plugin succeeds without uploading a pipeline. A missing or untracked server-selected path fails. Every present path must be a regular, tracked .yml or .yaml file inside the repository. Directories, tracked files missing from the checkout, symlinks, and globs are rejected. The optional oidc object accepts non-empty claims, aws-session-tags, and subject-claim values. Unknown fields and invalid values fail before upload.
The plugin resolves relative workflow paths from BUILDKITE_BUILD_CHECKOUT_PATH, not the command hook's working directory.
The importer reuses its verified executable for jobs on the same platform. It downloads the other platform's distribution from the same release only when a workflow needs it. Runner mappings apply to generated jobs, not the importer.
Configure generated-job cache volumes
An explicit runner mapping can attach one Buildkite hosted agents cache volume to each generated job using that mapping:
plugins:
- github-actions#latest:
workflow: .github/workflows/ci.yml
runners:
- runs-on: ubuntu-latest
queue: hosted
cache:
paths:
- /home/runner/.gradle/caches
- /home/runner/.gradle/wrapper
name: gradle-dependencies
size: 40g
The cache.paths field is a required, non-empty list of unique absolute paths. The name and size fields are optional. Names follow the Buildkite 100-character letters-numbers-hyphens format and may contain ${BUILDKITE_*} variables. Sizes use Ng and must be at least 20g. Without a name or size, Buildkite uses its pipeline-scoped name and 20 GB defaults.
Each Buildkite Pipelines step supports one cache volume. When a job also needs the internally managed mise cache, buildkite-gha adds the mise path to the same volume. A configured name and size apply to that combined volume; otherwise, the managed mise name and the Buildkite default size remain unchanged. Jobs with neither configuration emit no cache attribute.
Runner cache volumes aren't supported for workflow jobs that set container.
Generated Linux jobs run as runner. Configured cache paths are made writable by that user after the bootstrap verifies that they target the Buildkite cache volume. Prefer narrowly scoped paths. For example, caching an entire Gradle User Home also persists init.d scripts and other executable configuration, increasing the impact of cache poisoning. Caching only caches and wrapper reduces that exposure, but a cache-volume miss doesn't provide the setup-gradle archive-cache fallback once the mounted caches directory exists.
Cache volumes are best-effort accelerators, scoped to the Buildkite pipeline and cluster. They commit after successful jobs and are abandoned after failed jobs. Don't use them as durable or trusted storage. See Cache volumes.
Select workflows
Pass every workflow path explicitly:
buildkite-gha upload -- \
.github/workflows/ci.yml \
.github/workflows/release.yml
Every operand must name one regular .yml or .yaml file. For multiple workflows, every path must be tracked inside the repository. Upload canonicalizes, deduplicates, and sorts aliases, so argument order doesn't change the pipeline.
Directories, globs, missing files, other extensions, and symlinks fail before parsing or Buildkite commands run. Multiple-workflow uploads also reject untracked and outside paths.
The -- separator ends option parsing. Use it before any externally supplied paths, and always when a path begins with -. Pass each path as its own argument; the CLI doesn't split one shell string or decode a JSON or YAML list.
Upload is atomic. Skipped workflows become top-level skipped steps. Reusable-only files remain available to local callers but don't create groups. Selecting only reusable workflows is an error.
An explicit non-empty workflow run-name appends — <run-name> to its group label after resolving supported expressions. Workflow names, provider-check names, and the Buildkite build message remain unchanged.
See Aggregate workflow upload for workflow grouping, labels, provider checks, and failure behavior.
Private reusable workflows are off by default. Set the plugin's private-reusable-workflows: true field, or pass upload --private-reusable-workflows from a custom importer. See Reusable workflows for the access boundary and Security for the credential boundary.
A parse, compilation, or trigger-translation error in an aggregate upload replaces only that workflow with a failing top-level step. Compilation continues for later workflows. Single-workflow parse errors and event-input, admission, artifact, and upload failures abort the complete transaction; no partial pipeline is uploaded.
Select the effective event
Event source precedence is:
--event-path-
buildkite:webhookmetadata reserved by Buildkite - A reduced snapshot derived from
BUILDKITE_*variables when no linked webhook is available
Every event source remains unsigned. An explicit path never reads Buildkite metadata. Webhook metadata must be one valid JSON object no larger than 25 MiB. Malformed, unreadable, or oversized data stops upload instead of falling back. The Buildkite repository mapping, commit, and ref remain authoritative.
Raw webhook data isn't embedded in generated plans or pipeline YAML. Upload retains one content-addressed event artifact for linked webhooks and explicit snapshots, so every job and its retries can read the event file. For reduced fallback snapshots, it retains the artifact only when runtime event expressions require it. Event data can't grant queues, secrets, or tokens.
The selected snapshot establishes one event for applicability, compilation, workflow conditions, provider-check names, and explicit run-name evaluation. An explicit event is never replaced with live Buildkite fields.
Linked webhook data can provide native merge_group, release, and issues events. GitHub Actions pipeline trigger identity additionally supports merge_group, release, deployment, deployment_status, create, delete, label, fork, public, gollum, page_build, watch, milestone, branch_protection_rule, discussion, discussion_comment, issues, issue_comment, and PR review events without native event settings. Merge groups and releases need matching Buildkite refs, commits, and activity. Release also needs a valid payload and a tag matching BUILDKITE_TAG and BUILDKITE_BRANCH. Issue and comment payloads need a valid action, object identity, and repository matching the Buildkite checkout. The GitHub Code Access App provides immutable server provenance and is required for hosted release GITHUB_TOKEN issuance.
See Workflow names and triggers for exact matching rules and the environment fallback.
A top-level workflow that doesn't declare the event becomes a skipped step with no plan artifacts. If none apply, upload succeeds with a skipped-only pipeline.
For an applicable workflow, only the selected event contributes a workflow condition. Supported branch, tag, base-branch, and activity filters add their constraints. A workflow whose path filters verifiably don't match becomes a skipped step without workflow jobs or plan artifacts. Conditions from different events are never combined.
Unsupported or uncertain filters replace only the affected workflow with a failing step. Push and pull request path filters need a linked webhook and a matching local checkout. Generated or explicit snapshots can report that need, but can't grant admission. Malformed event data stops the import.
Buildkite Pipelines owns schedule identity, so every on.schedule workflow is eligible for every Buildkite scheduled build. Scheduled groups select the preserved GitHub event or, when it's absent, the Buildkite schedule source.
After all applicable workflows have been attempted, the command uploads the exact executable, content-addressed plans, and synthetic failure steps in one artifact batch with a concurrency limit of 8. It then runs one:
buildkite-agent pipeline upload --no-interpolation
Buildkite agent v4 rejects pipeline uploads containing secrets by default.
Choose runners and runtimes
Use repeatable mappings before the workflow path:
buildkite-gha upload \
--runner-queue ubuntu-latest=hosted \
--runner-queue ubuntu-24.04-arm=my-linux-arm64-queue \
--runner-queue macos-14=macos-sonoma-arm64 \
--runtime-distribution linux/arm64=/opt/buildkite-gha-linux-arm64 \
--runtime-distribution darwin/arm64=/opt/buildkite-gha-darwin \
.github/workflows/ci.yml
Linux arm64 mappings always require an explicit queue and never fall back to an amd64 image or emulation. The release plugin acquires and verifies buildkite-gha_Linux_arm64.tar.gz when a selected workflow needs that runtime. This artifact doesn't enable a Buildkite hosted agents Linux ARM64 queue.
The hosted preset accepts runner labels case-insensitively, so aliases such as macOS-latest and Ubuntu-Latest are equivalent to their lowercase forms. Local presets use Noble for ubuntu-latest and ubuntu-24.04, and Jammy for ubuntu-22.04. Backend resolution can instead select a native Linux environment through agent tags. Use --runner-image with an immutable digest to override the preset for a configured profile; backend tags never replace that explicit image. An explicit mapping declares its selector's platform from the known Linux, macOS, and Windows labels. The importer validates its queue and hosted platform through the job-scoped Agent API before upload, preserving the configured image and cache. An import using explicit mappings stops if that validation is unavailable. The Agent API owns compatibility and returns the complete target for every other selector. The importer publishes returned warnings as annotations. See Compatibility for runner behavior. Runtime distribution paths must be absolute executables. The importer's platform defaults to its running executable; other platforms have no direct-upload default. BUILDKITE_GHA_TARGET_QUEUE and BUILDKITE_GHA_RUNTIME_IMAGE are no longer supported.
Without Agent API resolution, unmapped supported Linux labels retain default agent targeting with the preset image, macos-latest falls back to the hosted macos-medium queue, and macos-14 and macos-15 accept explicit fallback queues. The windows-latest and windows-2022 jobs require explicit queues or enabled Agent API resolution; they have no local preset.
For Windows jobs, map the workflow's label to an existing compatible Windows queue. For example, this plugin configuration imports a workflow using runs-on: windows-2022:
steps:
- label: Import Windows workflow
key: import-windows-workflow
agents:
queue: my-linux-importer-queue
plugins:
- github-actions#latest:
workflow: .github/workflows/ci.yml
runners:
- runs-on: windows-2022
queue: my-windows-queue
Replace both queue names with queues in your pipeline's cluster. The importer runs on Linux or macOS, not Windows. If the workflow uses windows-latest, use that label in the mapping instead. For automatic routing, omit the Windows runners entry only after checking the access and queue requirements.
For direct CLI upload from a Buildkite job, also supply the Windows x86-64 runtime executable from the same release, at an absolute path on the importer:
buildkite-gha upload \
--event-path event.json \
--runner-queue windows-2022=my-windows-queue \
--runtime-distribution windows/amd64=/opt/buildkite-gha.exe \
.github/workflows/ci.yml
The plugin acquires the Windows runtime from the same release only when a selected workflow requires it and verifies the release checksums. Direct CLI users must verify the Windows release archive against that release's checksum file before extracting buildkite-gha.exe. Development plugin runs use BUILDKITE_GHA_PLUGIN_DEV_WINDOWS_RUNTIME as an absolute path to a locally built Windows executable. Windows targets reject --runner-image and cache volumes. These mappings don't create queues or grant hosted Windows access.
The deprecated --runtime-queue hosted argument is accepted as a no-op for compatibility with plugin releases that pass it. Other values are rejected.
Expand a matrix inside the build
When a workflow takes matrices or runner selections from job outputs, upload creates one deferred step per group of overlapping downstream jobs, in addition to the static jobs (see Matrices from job outputs and Runners from job outputs). The importer needs BUILDKITE_JOB_ID for this. Every runtime platform the expanded jobs may need must already be configured with --runtime-distribution; a row that selects an unconfigured platform fails the deferred step. The importer resolves repository and organization variables when any job of the workflow, deferred or not, reads vars in the workflow or in an action it uses, and records the scopes for the deferred steps. Such a workflow is never uploaded job by job: when any of its jobs fails compilation, the whole workflow is replaced with one failing step, because a partial upload would drop the deferred steps.
The importer and every deferred step are stages of one compilation. Each stage compiles the whole workflow through the same compile path, uploads the jobs whose scheduling values it knows, and writes a stage record for each boundary the compiler still defers. The deferred step downloads the importer's executable, then runs the internal form of the same command:
buildkite-gha upload \
--stage-digest sha256:<digest> \
--stage-producer <job-id>
This form accepts no other options or operands. The --stage-producer option is the job whose artifacts hold the stage record: the importer for the first deferred step of a component, or the earlier deferred step when a matrix chains from a job that step compiled (see Matrices from job outputs). The event source and the runtimes always come from the importer the record names. Releases before this form emitted a separate continue command; a deferred step always runs the executable digest its own importer uploaded, so the two never mix inside one build.
A stage step runs inside a Buildkite job with BUILDKITE=true, BUILDKITE_BUILD_ID, BUILDKITE_JOB_ID, and the default checkout. It:
- Downloads the stage record, verifies its digest and compiler version, and checks that the workflow in the checkout is byte-for-byte the one the importer compiled. The record also holds the action revisions and the variable scopes the importer resolved for the deferred jobs, so a stage never requests variables itself, and the rows every earlier stage of a chained component resolved.
- Reads each producer's verified result through the same manifest path that
needsoutputs use, bound to its exact instance key and plan digest. A verified non-success result skips that root's downstream jobs, while a missing or invalid manifest fails the step before any upload. Earlier producers must still match their recorded results (see matrix retries). - Expands successful outputs with the static-matrix rules and limits, or evaluates the runner selection using an output of at most 1 KiB. It checks that the rows and dependents fit the share of the 1,024-job limit the record holds for this step (see Matrices from job outputs). It recompiles the workflow with the recorded event, variables, runner mappings, OIDC, and
private-reusable-workflowssettings and the recorded rows of earlier stages, reads remote reusable workflows and actions through the same repository source asupload, and resolves runners through the Agent API asuploaddoes. It requires the jobs the earlier uploads created to compile identically, requires each deferred job to come from the workflow source the importer recorded, including the commit of a reusable workflow from another repository, and pins each deferred job's actions to the recorded revisions. - Uploads the plans and a pipeline holding only the deferred jobs whose scheduling values exist, including skipped placeholders where needed. Joins appear once, and outside prerequisites remain references to steps already in the build. When some deferred jobs read their matrix from a job this upload compiled, the pipeline also holds the next stage's deferred step, and the upload writes that step's stage record with the rows accepted so far and the unused part of this step's job share.
Buildkite Pipelines rejects an upload whose step keys already exist. When that happens, the stage confirms through buildkite-agent step get that each expected step carries the plan it just compiled and exits 0, so retrying the deferred step never duplicates jobs. For skipped jobs in a merged component, it also verifies the command marker bound to the stage record digest. A missing step or a different binding fails the replay. For output-derived scheduling, it also compares the concurrency group and limit. Any other failure exits 1 with:
Retry the whole build to expand this matrix again. If the matrix producer job was retried, only a new build can expand it.
Runner selection failures instead say:
Retry the whole build to select this runner again. If the producer job was retried, only a new build can select it.
Run Linux jobs as a non-root user
Generated Linux jobs use a dedicated runner user by default. This behavior requires buildkite-gha v0.13.7 or newer. Jobs can start as root or as an existing runner user with home /home/runner and passwordless sudo. For a non-root start, the bootstrap uses sudo -n for privileged setup. It creates the runner user when needed, grants passwordless sudo and Docker socket access when the socket exists, prepares the runner home, temp, mise, and tool-cache paths, then runs buildkite-gha run-job as runner. The verified executable and compiled plan remain root-owned and read-only to runner. Generated jobs skip the Buildkite checkout. When a workflow uses actions/checkout, the native adapter clones as runner, so the runtime doesn't recursively change workspace ownership. This behavior doesn't depend on a queue name and doesn't affect macOS or Windows jobs.
During the transition, set the plugin field to false to run as the agent's original user without this bootstrap:
steps:
- label: ":github: CI"
key: github-actions
plugins:
- github-actions#latest:
workflow: .github/workflows/ci.yml
experimental-runner-user: false
For a custom importer, pass upload --experimental-runner-user=false. The bare --experimental-runner-user form and a plugin value of true remain accepted. The plugin value must be a YAML boolean, not a quoted string.
Disable telemetry
In Buildkite jobs, the importer and runtime send best-effort completion telemetry through the job-authenticated Agent API. Events contain the command, outcome, client version, duration, and bounded diagnostics. Diagnostics can identify a rejected feature with a blocker slug and bounded blocker_detail, such as runner_label and windows-latest. Distinct rejected values remain separate diagnostics even when they share a diagnostic code. Unsupported events use trigger and the event name; path-filter evaluation failures use path_filter and the event name. Runner rejections name a label only when that label caused the rejection. Trust, conflicting-target, and Agent API rejections use runner_policy with a stable reason code instead. These fields are telemetry attribution, not fields in processing-report JSON.
Workflow diagnostics include their message and detail in message, even when the importer exits zero after uploading failing steps for rejected workflows. Messages are normalized to one line and retain at most their first 1,024 UTF-8 bytes; message_truncated is true when text was shortened. workflow_path identifies the root workflow being imported, relative to the checkout when possible. Paths over 1,024 bytes or containing invalid UTF-8 or control characters are omitted rather than shortened. Deduplication includes the bounded message and workflow path, subject to the limit of 20 diagnostics per command.
For an unsuccessful command, events also contain a normalized user-visible error message of at most 1,024 bytes. When a workflow diagnostic attributes the failure, the message is the text of that diagnostic, kept whole so later job output can't displace it. Otherwise it's the final bytes of the command's error output. error_message_truncated says whether part of the message was omitted.
Failed runs also carry a failure phase and a code from a fixed set. The code separates workflow-authored process exits (E_STEP_PROCESS_EXIT) from unsupported-feature rejections (E_UNSUPPORTED_FEATURE) and runtime integrity failures (E_RUNTIME_INTEGRITY), so a failing test suite isn't counted as a compatibility gap. Runtime rejections include the same blocker fields when the runtime can identify the rejected shell or action reference. Secret resolution failures use E_SECRET_UNAVAILABLE, making secret availability independently measurable. Workflow-token, OIDC-token, and cache credential acquisition failures use E_WORKFLOW_TOKEN_UNAVAILABLE, E_OIDC_TOKEN_UNAVAILABLE, and E_CACHE_CREDENTIAL_UNAVAILABLE, including Agent API client timeouts but not caller cancellation. A successful OIDC retry clears the recorded failure for that audience. Unsupported-feature and runtime integrity failures take precedence over token acquisition failures. When the failure code identifies a token or cache credential failure, agent_api_http_status contains the Agent API response status, if it's between 100 and 599. Invalid statuses are omitted without dropping the event. Other unclassified failures keep the unknown code and omit the status.
Buildkite adds organization, pipeline, build, and job identifiers on the server. The client doesn't send workflow or event content, environment variables, command text, or secrets as separate properties. Blocker details come from workflow-authored configuration. Event-derived runner labels and environment expressions are omitted.
Diagnostic messages and error output can include details already printed in the job log, such as workflow paths, action references, expressions, or invalid configuration values. Avoid putting secrets in error messages. Disable telemetry when this diagnostic context must stay inside the job.
Set BUILDKITE_GHA_TELEMETRY_DISABLED=true to disable telemetry. A missing Agent endpoint, job ID, or job token also disables it. Telemetry failures don't change command results.
Bugsnag error reports
In buildkite-gha v0.101.0 and later, when a Bugsnag ingestion key is configured, the client sends best-effort error reports directly to https://notify.bugsnag.com, independently of the Agent API. Tagged buildkite-gha releases embed the key at build time. The BUGSNAG_API_KEY environment variable overrides the embedded key. Without a valid key, no reports are sent. Setting BUILDKITE_GHA_TELEMETRY_DISABLED=true disables both Bugsnag reporting and completion telemetry.
Reports cover unexpected runtime errors, runtime integrity and token-acquisition failures, final artifact and pipeline uploads during the initial workflow import, and authoritative result-publication failures. Deferred-stage and skipped-job uploads aren't instrumented.
The following are excluded from reports:
- Ordinary workflow process exits, including tolerated failures.
- Unsupported features and import validation errors.
- Classified runtime file-command and output validation errors.
- Secret-availability errors.
- Caller cancellation.
- Workflow-owned job and step timeouts.
Agent HTTP client timeouts and cleanup deadlines remain reportable. Excluded branches of joined errors don't suppress independent unexpected failures. Warnings and unhandled panics aren't reported. Container termination failures remain reportable even when the job tolerates the step failure. Reporting doesn't change continue-on-error behavior.
Reports contain the CLI version, error type, stack methods and line numbers, command, failure phase and code, and the applicable Agent API status. Stack filenames are replaced with [REDACTED] to omit checkout and build paths. Development versions use the development release stage, and other versions use production. Standard Go errors have a stack at the reporting boundary, not the original failure site. Wrapped and joined errors retain the first captured stack in depth-first order when available. Command, phase, and failure code are part of the error class, so different categories don't share a group only because they reach the same reporting boundary.
Reports omit raw error messages and causes, job output, diagnostics, workflow and event contents, Buildkite build and job IDs, environment values, and the hostname. Releases before v0.101.1 also included valid Buildkite build and job IDs and full stack filenames. Detailed error text remains in the job log. Automatic session tracking and the process-forking panic handler of the SDK aren't enabled.
Delivery is synchronous, with a 1.5-second network timeout per report, no retries, and no redirects. Reporting failures are silent and don't change command results.