GitHub Actions compatibility reference
This page describes the GitHub Actions functionality supported by buildkite-gha v0.102.1. It's the production contract for the hosted profile used by upload and the GitHub Actions Buildkite plugin. If a feature isn't listed on this page, treat it as unsupported. To set up a pipeline that runs GitHub Actions workflows, see Run GitHub Actions workflows in Buildkite.
buildkite-gha requires Buildkite agent v3.129 or newer.
Generated jobs run on Linux x86-64, Linux arm64, native macOS arm64, and Windows Server 2022 x86-64. Linux arm64 jobs require an explicit queue. Windows jobs require a compatible Windows queue. The plugin's importer runs on Linux x86-64 or macOS arm64. A custom importer can also run on Linux arm64. Runner labels select a platform. They don't promise GitHub image, toolchain, or Xcode parity. The runtime sets the matching runner.os and runner.arch values, and sets runner.environment to self-hosted on every platform.
Generated Linux jobs use a dedicated runner user and need buildkite-gha v0.13.7 or newer. Use experimental-runner-user: false temporarily if an image can't support the privileged bootstrap.
The GitHub workflow syntax reference describes the original syntax. This page describes the subset that runs on Buildkite Pipelines.
Compatibility policy
buildkite-gha matches the exposed, reproducibly observed behavior of GitHub Actions, including surprising behavior. This applies to all compatibility surfaces, not just triggers. Documentation and specifications guide investigation, but they don't override reproducible observations without a documented intentional exception. Documentation silence isn't proof of rejection, and the buildkite-gha parser, tests, or previous implementation aren't a native oracle.
This page keeps the following distinctions explicit:
- Observed behavior: Records of observed GitHub behavior include the exact declarations, activities, date, method, execution context, and limits.
-
Intentional divergence: A divergence requires a strong explicit rationale. For example,
pull_request_targetis deliberately excluded: its privileged base-repository execution model needs a separate trust boundary from ordinary pull request execution. Merely admitting the event could expose authority to untrusted contributions. Neither the runtime's supported-event set nor the webhook matcher in the backend admits it. -
Not yet implemented: A native capability without a Buildkite Pipelines implementation, such as
repository_dispatch, is a gap, not a security exception or native rejection. - Unresolved: Absent runs, null receipts, and untested combinations don't establish whether GitHub rejected, filtered, or never processed a workflow. This page preserves that uncertainty.
Support matrix
| Status | Meaning | ||
|---|---|---|---|
| Status | ✅ Supported | Meaning | Available through the production plugin path. |
| Status | 🟡 Supported subset | Meaning | Available within the limits shown here. |
| Status | ➖ Accepted, no effect | Meaning | Validation accepts the syntax, but it doesn't change the build. |
| Status | 🚧 Not available in production | Meaning | The compiler or runtime supports it, but production upload blocks it. |
| Status | ❌ Unsupported | Meaning | Rejected or outside the compatibility contract. |
Looking for something else? Browse open compatibility issues.
| Area | Status | Initial release boundary | |||
|---|---|---|---|---|---|
| Area | Workflow and job names | Status | 🟡 Supported subset | Initial release boundary |
name, explicit run-name, and job names are retained. run-name supports expressions over github, inputs, and vars. |
| Area | Triggers and filters under on |
Status | 🟡 Supported subset | Initial release boundary | Buildkite Pipelines creates builds. Upload selects aggregate workflow groups for one effective event. workflow_call is supported for composition. |
| Area | Platforms | Status | 🟡 Supported subset | Initial release boundary | Linux x86-64 labels have local presets. Explicit mappings can select a user-provided Linux arm64 or Windows x86-64 queue. The Agent API can map compatible selectors to Linux, native macOS arm64, or, when Windows routing is enabled, Windows x86-64 targets. Labels don't provide GitHub image, toolchain, or Xcode parity. |
| Area | Jobs and dependencies | Status | ✅ Supported | Initial release boundary | Static dependencies, matrix fan-out and fan-in, results, and bounded outputs. |
| Area | Matrix strategies | Status | 🟡 Supported subset | Initial release boundary | Static matrices, include, exclude, and literal max-parallel. Needs-derived matrices can also read their producer's parallel limit. Maximum 256 instances per job. fail-fast has no effect. |
| Area | Shell steps | Status | 🟡 Supported subset | Initial release boundary | Linux and macOS bash, sh, pwsh, powershell, python, and custom shell templates. PowerShell and MSYS2 on Windows jobs. |
| Area | Conditions and expressions | Status | 🟡 Supported subset | Initial release boundary | GitHub-compatible core operators and direct references to selected contexts. |
| Area | Reusable workflows | Status | 🟡 Supported subset | Initial release boundary | Local, public, and approved private GitHub workflows with static or needs-derived typed inputs and direct job-output mappings. Local calls can inherit or explicitly map Buildkite secret authority. Private access requires a separate importer opt-in and existing Git access. |
| Area | Actions | Status | 🟡 Supported subset | Initial release boundary | Local and public JavaScript and composite actions on Linux, macOS, and Windows jobs. Verified Dockerfile and public prebuilt-image actions on Linux only. |
| Area | Checkout, artifacts, and cache | Status | 🟡 Supported subset | Initial release boundary | Only the audited versions and modes listed on this page. |
| Area | GITHUB_TOKEN |
Status | 🟡 Supported subset | Initial release boundary | One job-bound token for the event repository. Reusable-workflow jobs use the top-level workflow permissions. |
| Area | Other workflow secrets | Status | 🟡 Supported subset | Initial release boundary | Static names in direct jobs and locally inherited or explicitly mapped reusable jobs resolve through the destination job's Buildkite secret authority. |
| Area | Job and service containers | Status | 🟡 Supported subset | Initial release boundary | Linux job containers and broadly compatible service definitions, including explicit registry credentials. |
| Area | Environments and snapshots | Status | 🟡 Supported subset | Initial release boundary | Literal environments on top-level jobs, with required-reviewer approval gates and environment-scoped secret names. Wait timers, branch policies, and custom rules are rejected. Snapshots are accepted with no effect. |
| Area | Variables | Status | 🟡 Supported subset | Initial release boundary | Repository, organization, and environment vars resolve inside a Buildkite job with the GitHub per-position scoping. |
| Area | OIDC | Status | 🟡 Supported subset | Initial release boundary | Host JavaScript and composite actions can request Buildkite OIDC tokens in jobs with id-token: write. |
| Area | Windows jobs | Status | 🟡 Supported subset | Initial release boundary | Windows Server 2022 x86-64 through an explicit queue mapping or enabled automatic routing. No local Windows preset. |
| Area | Other platforms and providers | Status | ❌ Unsupported | Initial release boundary | Windows arm64, Windows Server 2025, macOS x86-64, GitHub Enterprise Server, and unlisted providers. |
| Area | Other GitHub services | Status | ❌ Unsupported | Initial release boundary | No general emulation for Releases, Packages, Checks, deployments, or GitHub artifact APIs. |
Windows jobs
Windows Server 2022 x86-64 jobs are supported within the limits on this page. As on every platform, compatibility gaps might exist. Browse or report compatibility issues. Buildkite hosted Windows queues need separate access. See Configure a Windows runner. Import workflows from a supported Linux or macOS importer. Windows agents run generated jobs only. windows-latest and windows-2022 select Windows x86-64 when explicitly mapped to a queue or enabled through Agent API resolution. They have no local preset and are otherwise rejected, never silently redirected to Linux.
When Windows routing is enabled, the Agent API also maps windows-2025 and standard Depot depot-windows-2025 labels (including -4, -8, -16, -32, and -64) to the same windows-medium queue as windows-latest. It warns that the queue may run Windows Server 2022 instead of 2025, and that Depot hardware, caches, and networking aren't preserved. This fallback requires an existing eligible hosted Windows queue. It doesn't provision Depot runners or promise Server 2025 compatibility. These aliases have no local preset or explicit Windows mapping. Use Agent API resolution rather than a custom-label mapping.
Use a Windows Server 2022 queue with Buildkite agent v3.129 or newer, PowerShell 7 (pwsh), and Git on PATH. Runner labels don't install the GitHub runner image or its tools. runner.os is Windows and runner.arch is X64. See explicit mappings and runtime distributions.
Configure a Windows runner
Choose one route with your pipeline administrator:
-
Explicit mapping: Map the workflow's
windows-latestorwindows-2022label to an existing Windows Server 2022 x86-64 queue in the importing job's cluster. The Agent API must validate that the queue exists and, for a hosted queue, matcheswindows/amd64. See the plugin and CLI examples. -
Automatic routing: Ask the Buildkite Support team at support@buildkite.com to confirm your organization's hosted Windows Medium access and Agent API routing availability. The importing job's cluster needs an active hosted Windows AMD64 Medium queue named
windows-medium. A self-hosted, differently sized, wrong-platform, or other-cluster queue doesn't qualify, even if its name matches.
Neither route creates queues, grants hosted Windows access, or changes trust policy. Keep generated-job queues isolated from protected credentials and earlier jobs, including when using a self-hosted queue. An explicit mapping isn't a way to bypass queue validation. Don't change a Windows-only job to Linux to get past runner admission.
| Diagnostic | Action | ||
|---|---|---|---|
| Diagnostic | No Windows runner target is configured | Action | Configure one of the routes in the previous list. Offline validation has no Windows preset. This failure alone doesn't establish runtime incompatibility. |
| Diagnostic | No hosted Windows queue (missing_queue) |
Action | Ask an administrator to check the importing job's cluster and all automatic-routing queue requirements, or explicitly map a compatible existing queue. |
| Diagnostic | Job doesn't belong to a cluster (no_cluster) |
Action | Ask an administrator to move the pipeline to a cluster containing compatible queues. |
| Diagnostic | Queue not found or platform mismatch | Action | Correct the mapping to an active Windows queue in the importing job's cluster. A Linux queue can't run the Windows runtime. |
| Diagnostic | Incompatible labels | Action | Check the variant and use a single supported Windows label. For windows-latest or windows-2022, this can also mean hosted Windows access is unavailable. Ask the Buildkite Support team to check eligibility and routing rather than assuming Windows is wholly unsupported. |
| Diagnostic | Windows variant has no local mapping | Action | Use windows-latest or windows-2022 only if the job can run on Server 2022 x86-64. The documented Windows 2025 aliases require Agent API resolution and preserve neither Server 2025 nor provider hardware. Native Windows arm64 remains unsupported. |
For support, include the build or job URL, workflow path, requested runs-on, importer version, and complete runner diagnostic. A generic rejection doesn't identify a missing entitlement, queue, or mapping by itself.
Runtime boundaries
The default shell is pwsh. Explicit pwsh and Windows PowerShell (powershell) steps, JavaScript actions, and composite actions are supported within the same action restrictions documented on this page. Use UTF-8 for file commands. Windows PowerShell needs Out-File -Encoding utf8 -Append rather than its default UTF-16 redirection. MSYS2 custom shells are supported. cmd shells, containers, services, Docker actions, custom images, and cache volumes aren't supported on Windows. Native Windows Server 2025 and arm64 execution aren't enabled by these mappings.
Windows environment overlays replace names case-insensitively but preserve the winning spelling, so env: {toolchain: nightly} remains $toolchain in Bash. GITHUB_ENV preserves the last assignment's spelling and value when names differ only by case. Runtime context variables remain protected from step overrides.
How workflows run on Buildkite Pipelines
GitHub Actions combines run creation and workload definition in one file. Buildkite Pipelines keeps them separate.
| GitHub Actions concept | Buildkite Pipelines behavior | ||
|---|---|---|---|
| GitHub Actions concept | Workflow trigger | Buildkite Pipelines behavior | Buildkite integration, schedule, manual build, or API request |
| GitHub Actions concept | Workflow run | Buildkite Pipelines behavior | Existing Buildkite build |
| GitHub Actions concept | Job | Buildkite Pipelines behavior | Buildkite command job |
| GitHub Actions concept | Matrix entry | Buildkite Pipelines behavior | Buildkite command job |
| GitHub Actions concept | needs |
Buildkite Pipelines behavior |
depends_on with verified result transport |
| GitHub Actions concept | Step | Buildkite Pipelines behavior | Runs inside the job compatibility runtime |
No shadow GitHub Actions run is created. Pipelines owns scheduling, logs, retries, cancellation, and status.
Steps remain inside one job because they share a workspace, environment files, action state, and post-action cleanup.
Aggregate workflow upload
The plugin accepts either one workflow path or a non-empty workflows array. GitHub Actions pipeline trigger builds can instead use an empty plugin configuration and the single workflow selected by Buildkite Pipelines. BUILDKITE_GITHUB_WORKFLOW_PATH marks the selection. When present, the plugin prefers the repository, path, and event ref from GITHUB_WORKFLOW_REF, the workflow name or fallback path from GITHUB_WORKFLOW, the event from GITHUB_EVENT_NAME, and the commit from GITHUB_WORKFLOW_SHA. These values must match the repository and checked-out workflow. The Buildkite-prefixed path and event remain compatibility fallbacks, while BUILDKITE_GITHUB_ACTION supplies the event activity. Explicit plugin selection takes precedence over server workflow selection. Without either selection, the plugin fails. The server-selected importer doesn't need a step key. The plugin uploads its artifacts before the dynamic pipeline and scopes retrieval to the importer job ID without using that UUID as a dependency key. Explicit-selector importers still require a step key, and their generated groups depend on it.
Missing or untracked explicitly configured paths warn and are skipped, so removing a workflow doesn't require a simultaneous pipeline configuration change. If every configured path is missing or untracked, the importer succeeds without uploading a pipeline. A missing or untracked pipeline trigger-selected path fails because the server claimed that exact workflow selected the build. Every present path must be a regular, tracked .yml or .yaml file inside the repository. Directories, tracked files missing from the checkout, outside paths, symlinks, and globs fail. A custom importer may upload one explicit regular workflow from outside the repository, or an untracked one, unless it holds a matrix from a job output.
Before assigning workflow identities and job keys, upload converts the paths to canonical form, sorts them, and removes duplicates.
All remaining runnable workflows use one artifact and pipeline transaction:
- Server-selected workflows emit jobs without a workflow group. Explicitly configured workflows retain groups, even when only one workflow is selected.
- Each group is labeled
:github: workflow · <workflow-name>. An unnamed workflow uses its canonical path. A resolved, non-emptyrun-nameappends— <run-name>. - Upload leaves the importer label unchanged.
- Each job publishes a provider check named
<workflow-name-or-path> / <job-id> (<effective-event>). Matrix jobs append their sorted values to the job ID. - GitHub events publish GitHub checks. Origin events publish Origin checks.
- A workflow that doesn't declare the event becomes one top-level skipped step with no plan artifacts.
- An importer annotation links to each workflow skipped by event or filters. It shows configured events for event mismatches and the specific reason for filter mismatches. It also states when every workflow was skipped. If publication fails, upload warns but still succeeds.
Workflow names, group keys, and provider-check names stay the same across events. Only an appended run title can vary. Groups and replacement steps depend on the importer. Their child jobs don't repeat that dependency. Without a group, each step carries the workflow condition and depends on the importer if it has a step key. Deferred uploads retain the initial workflow's grouping choice.
Reusable-only workflow_call files remain available to local callers but don't create groups. Selecting only reusable workflows is an error.
A parse, compilation, or trigger-translation error in an aggregate upload replaces only that workflow with a failing top-level step. The step:
- Is labeled
:github: workflow · <workflow-name-or-path>, with the resolved run title appended when present. - Publishes redacted diagnostics as a job annotation.
- Publishes a
Workflow could not be runprovider check. - Limits the check summary to 65,535 bytes.
- Exits with status 1.
Other workflows continue compiling. An unsupported matrix derived from needs outputs, including one inside a called reusable workflow, fails only its own workflow after the event and repository variables resolve, so it never reports vars values as unavailable or blocks other workflows. Missing or untracked configured paths are omitted before the transaction. Single-workflow parse errors and invalid path states, event-input, admission, artifact, and upload failures still abort the complete transaction. Upload never publishes a partial pipeline.
If a workflow has both a compiler error and a skip reason, the compiler error takes precedence.
Compatibility diagnostics
Diagnostics keep guidance separate from implementation detail:
-
message: Explains the visible cause and what to do. -
detail: Optionally records lower-level context such as resolved commits, adapter boundaries, or supported-version lists.
Text and JSON reports, Buildkite annotations, and generated failure artifacts preserve both fields. GitHub check summaries show only the concise message.
Workflow names and triggers
| Key | Status | Behavior | |||
|---|---|---|---|---|---|
| Key | name |
Status | ✅ Supported | Behavior | Available as github.workflow and used to name generated work. |
| Key | run-name |
Status | 🟡 Supported subset | Behavior | An explicit non-empty value is appended to the workflow group label, when grouped. Compile-time github, inputs, and vars expressions are supported. The importer label, Buildkite build message, and provider-check names don't change. |
| Key | on |
Status | 🟡 Supported subset | Behavior | Doesn't create a Buildkite build. Selects and filters workflows for the effective event as described in this section. |
A workflow name is retained in generated work:
name: CI
An explicit run name adds event-specific presentation without changing static workflow or check identity:
name: Deploy
run-name: Deploy ${{ inputs.target }} by @${{ github.actor }}
When grouped, this produces :github: workflow · Deploy — Deploy production by @octocat. Omitted, blank, or blank-resolving values retain the static group label. On a non-dispatch event, declared dispatch inputs use their typed zero values rather than dispatch-only defaults. A skipped workflow doesn't synthesize dispatch inputs.
Absent properties in a known input set are empty, so expressions such as ${{ inputs.name || 'default' }} work in run names, job names, concurrency groups, and container images. Inputs waiting for job outputs remain unresolved until those outputs are available.
run-name uses repository and organization variables, never environment-scoped variables. Upload resolves those scopes before evaluating applicable workflow names, even when run-name is the workflow's only vars reference, such as run-name: Deploy ${{ vars.TARGET }}.
Buildkite Pipelines controls when a build starts. The trigger declaration controls whether and under which condition the workflow group participates in that existing build:
on:
push:
branches:
- main
Upload selects one effective event, in this order:
- The event in an explicit
--event-pathsnapshot. - The GitHub event name accompanying the reserved Buildkite linked-webhook metadata.
- A Buildkite environment fallback.
The fallback prefers GITHUB_EVENT_NAME, then preserves push, pull_request, workflow_dispatch, and schedule from BUILDKITE_GITHUB_EVENT across rebuilds. It preserves issues and issue_comment only with complete GitHub Actions pipeline trigger workflow path, ref, and SHA identity. Otherwise:
| Buildkite source | Effective event | ||
|---|---|---|---|
| Buildkite source | Pull request build | Effective event | pull_request |
| Buildkite source |
ui or api
|
Effective event | push |
| Buildkite source | schedule |
Effective event | schedule |
| Buildkite source | Any other source, including trigger_job
|
Effective event | push |
An explicit snapshot doesn't consult contradictory live event fields. Linked merge-group data must match the queue refs and commits. Linked release data must match the Buildkite event, action, branch, and tag. Linked issue and comment data must match the Buildkite action, default-branch ref, and repository. Comment payloads may describe either issue or pull request conversations. Review events require the original linked payload and matching immutable workflow identity, pull request number, head SHA, head and base branches, and repository identities. They can't fall back to a synthetic event on rebuilds without that payload.
Pipeline trigger release events also require the original linked payload and complete workflow ref and SHA identity. The repository, selected tag ref, Buildkite branch and tag, and payload tag must agree. The workflow SHA must equal the build's immutable peeled commit. Missing payloads on rebuilds fail explicitly.
Pipeline trigger deployment events require the original linked payload and complete workflow ref and SHA identity. The payload repository and deployment SHA must match the build. The selected branch or tag ref must match deployment.ref. For SHA-only deployments, the build branch is a commit-valued checkout label, the Actions ref is empty, and workflow identity uses <owner>/<repo>/<path>@<sha>. No default branch or current branch tip is substituted. Missing payloads on rebuilds fail explicitly. These checks don't grant token or secret authority.
Pipeline trigger merge groups require the original linked payload and complete workflow ref and SHA identity too. Repository, action, speculative head ref and SHA, and base branch and SHA must agree with the build. Rebuilds without the payload fail explicitly. These builds support tokenless workflows. Hosted merge-queue workflow-token requests remain denied. Declared permissions alone don't cause the compiler to request a token.
With the GitHub Code Access App, Buildkite Pipelines resolves a release tag to its peeled commit before creating the build. Without it, the plugin resolves the symbolic HEAD that Pipelines provides from the checkout as a compatibility fallback. The fallback can't infer a merge group, release, issues, or issue-comment event without linked-webhook data.
The selected event then controls applicability, event-dependent compilation, the group condition, and the provider-check suffix.
| Event | Supported trigger behavior | ||
|---|---|---|---|
| Event | push |
Supported trigger behavior |
branches, branches-ignore, tags, and tags-ignore, including ordered negative patterns in an include list. Branch and tag filters select their corresponding ref kind. Matching paths and paths-ignore can be admitted for linked GitHub branch pushes when the bounded local-diff requirements are met. |
| Event | pull_request |
Supported trigger behavior |
branches and branches-ignore match the base branch. Omitted types, types: null, and types: [] default to opened, synchronize, and reopened, not every activity. Explicitly listed activity types must map exactly to a supported Buildkite source action. Matching paths and paths-ignore can be admitted when the bounded local-diff requirements are met. |
| Event | merge_group |
Supported trigger behavior | Pipeline triggers with a compatible server, and native Buildkite merge queue builds. Native builds require merge queue builds and Merge groups webhook delivery in the pipeline's GitHub settings. branches and branches-ignore match the base branch. Omitted types, types: null, or types: [] selects only checks_requested. Explicit [destroyed] and [checks_requested, destroyed] are supported by the CLI and runtime. Hosted execution requires server activation and compatible runtimes. Unknown types and tag and workflow filters are rejected. paths and paths-ignore are ignored with a warning, matching GitHub, which doesn't evaluate path filters for merge_group events. The ref and SHA identify the speculative queue head, not the base commit. A push to a queue ref is still a push. |
| Event | release |
Supported trigger behavior | Pipeline triggers accept all seven GitHub release activities. Native Buildkite release builds require Additional Webhooks > Releases and Code trigger mode and deliver only published, created, and released. Workflows that select other activities emit W_NATIVE_RELEASE_ACTIVITIES_UNDELIVERED. Bare declarations, types: null, and empty types lists select all activities. Other explicit types preserve exact selection, including a scalar selecting one activity. Branch, tag, and path filters are ignored. Malformed types, unknown activities, and workflow filters are rejected. GitHub doesn't trigger created, edited, deleted, or unpublished for draft releases, and Buildkite Pipelines rejects those deliveries. Pipeline triggers require the GitHub Code Access App to select the workflow at the immutable peeled tag commit. The ref is refs/tags/<tag_name>. The SHA is the server-resolved peeled commit, or the checked-out commit for the native compatibility fallback. Existing hosted release GITHUB_TOKEN policy is unchanged. |
| Event |
deployment, deployment_status
|
Supported trigger behavior | Pipeline triggers with a compatible server, or explicit event snapshots. Bare, array, null, and empty-map declarations are supported. Branch, tag, and path filters are ignored. Activity types and other event filters are rejected. Workflows and checkout use the deployment commit. The ref identifies its branch or tag and is empty for SHA-only deployments. Status states error, failure, in_progress, queued, pending, success, and waiting are supported (GitHub status enum). inactive can't run a workflow. The genuine payload exposes github.event.deployment and github.event.deployment_status, including environment, state, environment_url, log_url, and target_url when present. Use job or step conditions on these values, not types or environment filters. No deployment creation or environment orchestration is added. |
| Event |
create, delete
|
Supported trigger behavior | Branch and tag lifecycle. Branch, tag, and path filters are ignored. Activity types and other filters are rejected. Creation uses the exact ref's resolved commit. Deletion uses the default branch. |
| Event | label |
Supported trigger behavior |
Repository label lifecycle. created, edited, and deleted, all by default. Workflows and checkout use the resolved default branch. |
| Event | fork |
Supported trigger behavior |
Repository forks. Null or empty types accepted. Branch, tag, and path filters ignored. Other filters rejected. Workflows and checkout use the source repository's resolved default branch, not the forkee. |
| Event | public |
Supported trigger behavior |
Repository visibility. Null or empty types accepted. Branch, tag, and path filters ignored. Other filters rejected. Workflows and checkout use the resolved default branch. |
| Event | gollum |
Supported trigger behavior |
Wiki pages. Null or empty types accepted. Branch, tag, and path filters ignored. Other filters rejected. Workflows and checkout use the source repository's resolved default branch, not a wiki commit. |
| Event | page_build |
Supported trigger behavior |
GitHub Pages builds. Null or empty types accepted. Branch, tag, and path filters ignored. Other filters rejected. Workflows and checkout use the resolved default branch, not the Pages build commit. |
| Event | watch |
Supported trigger behavior |
Repository stars. Only started, including with null or empty types. Branch, tag, and path filters ignored. Other filters rejected. Workflows and checkout use the resolved default branch. |
| Event | milestone |
Supported trigger behavior |
Milestone lifecycle. created, closed, opened, edited, and deleted, all by default. Workflows and checkout use the resolved default branch. |
| Event | branch_protection_rule |
Supported trigger behavior |
Branch protection rules. created, edited, and deleted, all by default. Workflows and checkout use the resolved default branch, not the rule's pattern. |
| Event | discussion |
Supported trigger behavior |
Discussions. The 15 supported activities, all by default, including null or empty types. Branch, tag, and path filters ignored. Each event uses its repository's default branch, including separate source and destination events for transfers. |
| Event | discussion_comment |
Supported trigger behavior |
Discussion comments. created, edited, and deleted, all by default, on the source default branch. |
| Event | issues |
Supported trigger behavior | Omitted types, types: null, or types: [] accepts every GitHub Actions issue activity. Nonempty types may contain opened, edited, deleted, transferred, pinned, unpinned, closed, reopened, assigned, unassigned, labeled, unlabeled, locked, unlocked, milestoned, demilestoned, typed, untyped, field_added, and field_removed. Branch, tag, and path filters are ignored. Unknown types and workflow filters are rejected. In a GitHub Actions pipeline trigger build, Buildkite Pipelines selects workflows and the checkout from the latest verified default-branch SHA. Native issue-build settings, branch and path filters, and comment gating don't participate. Existing native Buildkite issue builds remain supported through linked webhook data and retain their own build-creation settings. |
| Event | issue_comment |
Supported trigger behavior | Omitted types, types: null, or types: [] accepts created, edited, and deleted. Nonempty types may contain those activities. Both issue and pull request conversation comments are supported. Branch, tag, and path filters are ignored. Unknown types and workflow filters are rejected. GitHub Actions pipeline trigger builds select workflows and the checkout from the latest verified default-branch SHA and don't inherit native command-word, trusted-commenter, pull-request-only, branch, or path gating. |
| Event | pull_request_review |
Supported trigger behavior | Pipeline triggers support submitted, edited, and dismissed, all by default. Nonempty types selects activities, not review states. Use if: github.event.review.state == 'approved' on a job or step for approval-only execution. |
| Event | pull_request_review_comment |
Supported trigger behavior | Pipeline triggers support inline diff-comment created, edited, and deleted activities, all by default. These are distinct from submitted reviews and issue_comment conversation comments. |
| Event | workflow_dispatch |
Supported trigger behavior | Selected only by an explicit snapshot or authoritative GITHUB_EVENT_NAME or BUILDKITE_GITHUB_EVENT value. Webhook-style branch, tag, type, and workflow filters are unsupported. |
| Event | schedule |
Supported trigger behavior | Selected for Buildkite scheduled builds. Buildkite Pipelines owns cron configuration and doesn't expose which schedule started a build, so every on.schedule workflow is eligible for every Buildkite scheduled build. |
| Event | workflow_call |
Supported trigger behavior | Defines a reusable-workflow interface. A reusable-only local file is available to callers but doesn't become a top-level group. |
| Event | pull_request_target |
Supported trigger behavior | Intentionally unsupported for security reasons. Use pull_request with careful checkout and ref handling instead. Changing the event alone doesn't make untrusted code safe. |
| Event | repository_dispatch |
Supported trigger behavior | Not supported yet. GitHub repository dispatch requests don't start imported workflows through pipeline triggers. |
| Event | workflow_run |
Supported trigger behavior | Not supported yet. Running an imported workflow doesn't create a GitHub Actions run or emit its workflow-run events. Consider Buildkite trigger steps when migrating cross-pipeline orchestration. |
| Event |
check_run, check_suite
|
Supported trigger behavior | Not supported yet by GitHub Actions pipeline triggers. Native Buildkite builds on completed check runs are a separate integration. |
| Event | Any other event | Supported trigger behavior | No Buildkite build source exists, so the trigger can never start a build. It's ignored with a W_TRIGGER_EVENT_UNSUPPORTED warning when the workflow also declares a supported event. A workflow declaring only unsupported events fails event-independent validation and is a skipped step in an uploaded pipeline. |
Supported pull_request activity types are assigned, unassigned, labeled, unlabeled, opened, edited, closed, reopened, synchronize, converted_to_draft, locked, unlocked, enqueued, dequeued, milestoned, demilestoned, ready_for_review, review_requested, review_request_removed, auto_merge_enabled, and auto_merge_disabled.
Both review events accept scalar, array, and map on declarations, including empty maps, types: null, and types: [] for all activities. Branch, tag, and path filters are ignored. Unknown types and workflow filters are rejected. The workflow and checkout use the pull request head SHA, not the synthetic merge commit or an older reviewed commit. The event ref remains refs/pull/<number>/merge. There is no default-branch-only workflow requirement. Use github.event.pull_request.head.ref and .base.ref for branch conditions. CI-skip commit directives don't suppress these events.
Review events require buildkite-gha v0.64.0 or later. Fork pull requests are unsupported. Same-repository review builds retain the pull request contents: read token ceiling even for approving reviews. Buildkite secret and queue policies still apply. The original linked webhook is required, so rebuilds without retained payloads fail explicitly. See the server-selected event contract.
GitHub defines seven release activities: published, unpublished, created, edited, deleted, prereleased, and released. A bare on: release selects all seven. Pipeline triggers accept all seven and apply the GitHub draft-release suppression before starting workflows. Native release builds deliver three and emit W_NATIVE_RELEASE_ACTIVITIES_UNDELIVERED when a workflow also selects unpublished, edited, deleted, or prereleased.
Merge-group destruction
On September 24, 2026, a single-pull-request merge queue experiment observed a successful GitHub Actions run for explicit types: [destroyed] with reason: merged. The completed run's original event, expression values, checkout, and workflow identity agreed on the group head SHA. Omitted, null, or empty types didn't select this event. Other destruction reasons and automatic-cancellation semantics weren't established. The GitHub documentation available at the time of this observation listed only checks_requested. The implementation follows the observed execution rather than treating that documentation as proof of rejection.
The CLI and runtime accept explicitly selected destruction events without a reason restriction. Mixed declarations select either listed activity:
on:
merge_group:
types:
- checks_requested
- destroyed
branches:
- stable
The original payload, group head ref and SHA, and immutable workflow identity remain unchanged after queue-ref deletion. The default checkout fetches the recorded head SHA. It doesn't resolve the deleted ref or substitute the base branch. GitHub object retention after ref removal and hosted execution aren't yet proven.
Pipeline triggers select destroyed workflows, including mixed declarations, only for Buildkite organizations that have this capability enabled. It's off by default. To enable it, contact the Buildkite Support team at support@buildkite.com. The CLI doesn't check this setting. The server's provenance-based cancellation guard applies regardless of the setting and preserves existing cancellation settings and defaults. Existing merge queue token restrictions still apply.
The following sections describe repository events that only pipeline triggers deliver. Each requires the original linked payload and pinned workflow path, ref, and SHA. Unless noted, workflow selection and checkout use the source repository's server-resolved default-branch commit, not stale pipeline or webhook branch metadata. Explicit event snapshots must supply that resolved repository.default_branch. The runtime doesn't resolve it. The original payload remains available through github.event and GITHUB_EVENT_PATH.
Missing, malformed, foreign-repository, or contradictory provenance fails closed, including rebuilds without the original payload. None of these events adds token authority or creates a native GitHub Actions run. Existing repository webhooks may need an additional event subscription. Enabling backend support doesn't update existing hooks.
Repository fork events
fork runs when someone forks the source repository. Scalar, array, null, and empty-map declarations and null or empty types are supported. Branch, tag, and path filters are ignored. Nonempty activity types and other filters are rejected. It doesn't enable pull requests from forks or pull_request_target.
Workflows and checkout never use the forkee's branch or commit. The complete payload, including github.event.forkee, is retained. Explicit snapshots must also supply a full SHA.
Repository visibility events
public runs when a private repository becomes public. Scalar, array, null, and empty-map declarations and null or empty types are supported. Branch, tag, and path filters are ignored. Nonempty activity types and other filters are rejected. The payload must identify the source repository, mark it public, and omit action.
Wiki page events
gollum runs when wiki pages are created or edited. Scalar, array, null, and empty-map declarations and null or empty types are supported. Branch, tag, and path filters are ignored. Nonempty activity types and other filters are rejected.
The payload must include a nonempty pages array with valid names, actions, and wiki commit SHAs. Workflows and checkout never use a wiki commit, and no wiki checkout is added. Page data remains in github.event.pages.
GitHub Pages build events
page_build accepts scalar, array, null, empty-map, and null or empty types declarations. Branch, tag, and path filters are ignored. Nonempty activity types and other filters are rejected.
The payload must include the Pages build ID, commit, and status. Failed builds can trigger workflows. Execution never uses the Pages build commit. Status and error data remain in github.event.build. This doesn't deploy Pages.
Repository star events
watch runs when someone stars the repository (started), not when they subscribe to notifications or remove a star. Scalar, array, null, empty-map, null or empty types, and types: [started] declarations are supported. Branch, tag, and path filters are ignored. Other types and filters are rejected.
Milestone lifecycle events
milestone supports created, closed, opened, edited, and deleted, all by default. Scalar, array, null, empty-map, and null or empty types declarations select all five. Explicit types selects a subset. Branch, tag, and path filters are ignored. Issue and pull request milestoned activities and other filters are rejected.
The payload must include the milestone ID, number, and title, which remain in github.event.milestone.
Branch protection rule events
branch_protection_rule supports created, edited, and deleted, all by default. Scalar, array, null, empty-map, and null or empty types select all three. Explicit types selects a subset. Branch, tag, and path filters are ignored. Other filters are rejected.
The payload's rule ID, name, and repository ID must match the source repository. Workflows and checkout never use the rule's branch pattern. The rule remains in github.event.rule.
Discussion events
discussion supports created, edited, deleted, transferred, pinned, unpinned, labeled, unlabeled, locked, unlocked, category_changed, answered, unanswered, closed, and reopened, all by default. discussion_comment supports created, edited, and deleted. For both events, scalar, array, null, empty-map, and null or empty types select all activities. Explicit types selects a subset. Both events ignore branch, tag, and path filters. Other filters and unknown activities are rejected.
The payload must include the discussion ID, number, and title. Comments require a positive ID and a discussion_id matching the discussion. There is no command-word or trusted-commenter gating. A transfer emits transferred in the source repository and a separate created event in the destination. Each runs that repository's workflows on its own default branch. github.event.discussion and github.event.comment retain the original data. GitHub lists discussion webhooks as public preview.
Repository label lifecycle events
label supports created, edited, and deleted activities, all by default. Scalar, array, null, empty-map, types: null, and types: [] declarations select all three. Explicit types selects a subset. Branch, tag, and path filters are ignored. Issue and pull request labeled actions and other filters are rejected. github.event.label retains the label data.
Branch and tag lifecycle events
create and delete support scalar, array, null, and empty-map declarations. Branch, tag, and path filters are ignored. Activity types and other filters are rejected. These events refer to Git refs, not repository creation or deletion. GitHub doesn't deliver them when more than three tags are created or deleted at once.
create uses the server-resolved commit of the created branch or tag, peeled for annotated tags. delete uses the default-branch commit, never the deleted ref. The deleted ref remains available in github.event.ref. github.ref names the default branch. Explicit snapshots must supply a full SHA, matching repository identity, and, for deletion, the resolved repository.default_branch. The runtime doesn't resolve mutable refs or attest event-time SHAs from these SHA-less webhooks.
Ignored event filters and native evidence
issues, issue_comment, label, release, create, delete, deployment, deployment_status, pull_request_review, and pull_request_review_comment accept but ignore all six branch, tag, and path filter keys. Activity selection, default and null normalization, unknown-key rejection, and provenance and security checks still apply. This doesn't change real push and pull_request path and ref filters or merge_group base-branch filters.
On September 29, 2026, isolated native GitHub Actions workflows each declared one of these filters, plus the matching types: [activity] where shown in the following tables:
| Key | Exact tested value | ||
|---|---|---|---|
| Key | branches |
Exact tested value | ["__lab_never_branch__"] |
| Key | tags |
Exact tested value | ["__lab_never_tag__"] |
| Key | paths |
Exact tested value | ["__lab_never_path__/**"] |
| Key |
branches-ignore, tags-ignore, paths-ignore
|
Exact tested value |
["**"], tested separately |
| Event | Tested activity or context | Declared types
|
|||
|---|---|---|---|---|---|
| Event | issues |
Tested activity or context | opened |
Declared types
|
[opened] |
| Event | issue_comment |
Tested activity or context |
created, issue conversation |
Declared types
|
[created] |
| Event | label |
Tested activity or context |
created, repository label |
Declared types
|
[created] |
| Event | release |
Tested activity or context | published |
Declared types
|
[published] |
| Event |
create, delete
|
Tested activity or context | Tag creation and deletion, not branch lifecycle | Declared types
|
Omitted |
| Event | deployment |
Tested activity or context | Payload action created
|
Declared types
|
Omitted |
| Event | deployment_status |
Tested activity or context | Payload action created, state success
|
Declared types
|
Omitted |
| Event | pull_request_review |
Tested activity or context |
submitted, COMMENT review |
Declared types
|
[submitted] |
| Event | pull_request_review_comment |
Tested activity or context |
created, inline diff comment |
Declared types
|
[created] |
All 60 native filter workflows and their ten unfiltered positive controls succeeded on GitHub Actions. Before this change, the Buildkite backend selected only those controls with runtime v0.91.1 and created no builds for the 60 filter workflows. This was a selection gap, not an intentional difference. Native review runs used synthetic merge commits, while hosted Buildkite review builds used pull request heads.
This evidence doesn't prove branch create or delete, other activities, combined filters, malformed values, or unknown keys. The implementation ignores these keys at the event level, not only for the sampled activity or ref kind. Four separate tags and tags-ignore cases on pull_request.opened and merge_group.checks_requested had no native or hosted runs despite successful unfiltered controls. Their cause remains ambiguous, and Buildkite Pipelines doesn't admit those filters. A quiet observation window doesn't prove per-delivery completion. This experiment ran before the change, so it doesn't prove post-change hosted execution or fleet-wide runtime selection.
Activity defaults and native evidence
For issues, issue_comment, label, release, pull_request_review, pull_request_review_comment, pull_request, and merge_group, these forms select the event's default activities listed in the event table:
| Form | Example declaration | ||
|---|---|---|---|
| Form | Omitted types | Example declaration | on: {pull_request: {}} |
| Form | Literal YAML null | Example declaration | on: {pull_request: {types: null}} |
| Form | Empty sequence | Example declaration | on: {pull_request: {types: []}} |
Defaults aren't an unconditional match: pull request defaults exclude edited and closed, and merge-group defaults exclude destroyed. Other event restrictions still apply. A nonempty types declaration selects its listed supported activities instead. This rule doesn't permit types on events that reject it, such as push. The null normalization for these eight event families requires v0.90.0 or later and a compatible backend. Publishing a runtime doesn't prove that a hosted build selected it.
The following table records GitHub Actions executions observed on September 24 and 29, 2026, not an exhaustive activity matrix. Other default activities listed in the event table follow GitHub's documented defaults and the Buildkite Pipelines implementation. They weren't emitted in this evidence set.
| Date (2026) | Event | Observed activities selected by omitted, null, or empty types | |||
|---|---|---|---|---|---|
| Date (2026) | September 29 | Event | issues |
Observed activities selected by omitted, null, or empty types | opened |
| Date (2026) | September 24 | Event | issue_comment |
Observed activities selected by omitted, null, or empty types | created |
| Date (2026) | September 24 | Event | label |
Observed activities selected by omitted, null, or empty types | created |
| Date (2026) | September 24 | Event | release |
Observed activities selected by omitted, null, or empty types |
published, released, created
|
| Date (2026) | September 24 | Event | pull_request |
Observed activities selected by omitted, null, or empty types | opened |
| Date (2026) | September 29 | Event | pull_request |
Observed activities selected by omitted, null, or empty types | synchronize |
| Date (2026) | September 29 | Event | pull_request |
Observed activities selected by omitted, null, or empty types | reopened |
| Date (2026) | September 24 | Event | pull_request_review |
Observed activities selected by omitted, null, or empty types | submitted |
| Date (2026) | September 24 | Event | pull_request_review_comment |
Observed activities selected by omitted, null, or empty types | created |
| Date (2026) | September 24 | Event | merge_group |
Observed activities selected by omitted, null, or empty types | checks_requested |
The omitted and empty controls selected the same observed activities as null. Explicit controls restricted selection to the configured activities. Explicit [edited] didn't select issues.opened. [synchronize, reopened] selected both pull request activities, while [opened] selected neither. On GitHub, pull request closed selected none of those five declarations, including omitted, null, and empty, during the same experiment. This doesn't test an explicit [closed] declaration.
Earlier controls also distinguish defaults from all activities: pull request edited ran only with explicit [opened, edited], not omitted, null, or empty declarations. The merge-group destruction observation also excluded those default forms.
Buildkite Pipelines rejects quoted 'null' and [null] rather than treating them as default declarations. It also rejects malformed types and unknown activities. These are Buildkite Pipelines rules, not native results established by this matrix. The matrix doesn't establish native handling of mixed valid and unknown lists either. Alternative null spellings (~, NULL, or a blank types:) and YAML aliases aren't covered by this native evidence. The buildkite-gha parser accepts an aliased trigger mapping (for example, issue_comment: *activities referring to types: null), but this doesn't establish GitHub parity or support for every alias shape.
For push and pull request path filters, once the workflow and checkout are verified against the webhook commit, non-path exclusions retain their branch or action conditions even if diff history is unavailable. Identity failures remain errors regardless of those exclusions.
Push path filters
For a linked GitHub branch push, the importer binds the webhook repository, ref, commit range, force state, and complete pushed-commit list to the Buildkite build and local checkout. HEAD and the workflow file must match the pushed commit. The origin repository must match, but its branch tip may have advanced since the webhook arrived.
Normal and force pushes use the GitHub two-dot before..after comparison. For a new branch, the importer uses the parent of the oldest pushed commit only when the complete commit set has one clear, single-parent boundary.
Added, modified, deleted, and type-changed paths can match. Patterns use the same ordered matching as pull requests.
Admission fails when the evidence is unsafe or incomplete, including:
- A deleted ref or non-GitHub repository.
- Missing or shallow history.
- Stale or mismatched repository, ref, checkout, workflow, commit set, or force state.
- Ambiguous new-branch history.
- More than 1,000 pushed commits or 3,000 changed files.
- Renames, combined additions and deletions, malformed Git output, or invalid patterns.
A verified local result with no matching paths produces an explicit skipped workflow step without executing workflow jobs. The importer uses the local diff result. It doesn't reproduce the GitHub 1,000-commit or diff-timeout fallback that can run a workflow without matching changed paths.
Tag pushes don't evaluate path filters, matching GitHub. Explicit and generated event snapshots, and Buildkite environment fallbacks, can't admit push path filters because they aren't linked webhook evidence.
When evaluation fails, the diagnostic distinguishes missing evidence from a Buildkite Pipelines compatibility limit:
| Diagnostic detail | Next step | ||
|---|---|---|---|
| Diagnostic detail | push path filters require linked Buildkite webhook data |
Next step | Use a build triggered by a GitHub push with its original webhook payload. Manual builds and explicit event snapshots can't supply linked evidence. |
| Diagnostic detail |
new-branch push requires complete pushed commit evidence or webhook push requires its commits array
|
Next step | Contact the Buildkite Support team at support@buildkite.com with the build URL and diagnostic detail to investigate the original GitHub webhook and retained evidence. Workflow checkout settings can't supply this evidence. |
| Diagnostic detail | push before commit is unavailable in the local checkout |
Next step | Contact the Buildkite Support team with the build URL and diagnostic detail. The importer has already verified that its checkout is non-shallow. Fetching more branch history isn't a proven fix. |
| Diagnostic detail |
combined added and deleted files require provider rename conformance data or renamed and copied files require provider conformance data
|
Next step | Contact the Buildkite Support team with the build URL and diagnostic detail. This is a Buildkite Pipelines compatibility limit pending GitHub rename-conformance evidence, not an invalid workflow. |
| Diagnostic detail | push path filters require a complete non-shallow checkout |
Next step | If git rev-parse --is-shallow-repository returns true in the importer checkout, ask the agent administrator to fetch full history before import. Workflow actions/checkout runs after filtering and can't fix the importer checkout. If the checkout isn't shallow or the check fails, contact the Buildkite Support team with the build URL and diagnostic detail. |
| Diagnostic detail | push exceeds GitHub's 1000-commit path-filter diff bound |
Next step | Push in batches of at most 1,000 commits. Buildkite Pipelines doesn't reproduce the GitHub run-anyway fallback above this limit. |
| Diagnostic detail | changed paths exceed the importer's 3000-file local evaluation bound |
Next step | Split the push into smaller diffs to stay within the limit. |
Removing paths or paths-ignore is a workaround that changes which workflows run, not a fix for missing evidence or compatibility limits. Unresolved path filters remain errors. Buildkite Pipelines doesn't guess whether a workflow should run.
Pull request path filters
paths and paths-ignore support ordered GitHub patterns. For example, this runs for changes under src, except generated files:
on:
pull_request:
paths:
- "src/**"
- "!src/generated/**"
Before upload, the importer compares the pull request merge base with its head in the local checkout (base...head). The linked webhook must provide full base and head commit SHAs with one common merge base verified as an ancestor of both commits. The checkout and filtered workflow must match the pull request head. The comparison uses those pinned commits, not the current base-branch tip.
Buildkite Pipelines builds the pull request head, not the GitHub synthetic merge. Path evaluation doesn't depend on merge_commit_sha or mergeable, including for conflicting and closed pull requests. Workflows without path filters don't require diff history.
The check uses the checkout's existing Git access for public, private, and fork pull requests. It doesn't call GitHub or use Buildkite if_changed.
| Admitted | Rejected | ||
|---|---|---|---|
| Admitted | A matching added, modified, deleted, or type-changed path | Rejected | Unavailable changed-path evidence |
| Admitted | A copied destination that matches | Rejected | A rename, or a diff containing both additions and deletions |
| Admitted | At most 3,000 changed files from complete local history | Rejected | Missing or shallow history, multiple merge bases, or more than 3,000 files |
| Admitted | Matching webhook, pull request head checkout, and workflow data | Rejected | Unrelated history, mismatched identity or workflow, path or pattern containing a backslash, invalid pattern, or malformed Git output |
A verified local result with no matching paths, including an empty diff or changes all excluded by paths-ignore, produces an explicit skipped workflow step without executing workflow jobs. The unobservable GitHub diff-timeout fallback isn't reproduced.
An unsupported or inexact filter replaces only the affected workflow with a failing step. It never broadens when the workflow runs. The failure identifies unavailable or mismatched evidence, or the local 3,000-file limit. Fetch complete checkout history when commits are missing. Reduce the diff or remove the filter when the file limit is exceeded. The 3,000-file bound follows the GitHub documented evaluation window, not a claim that the GitHub file ordering or diff-timeout fallback is reproduced.
A top-level workflow that doesn't declare the effective event is excluded before event-dependent validation or compilation and represented by one top-level skipped command step. A workflow that declares that event remains represented by a group even when a same-event branch, tag, base-branch, or action condition evaluates false in Buildkite Pipelines. If no directly runnable workflow declares the event, upload succeeds with a skipped-only pipeline.
Workflow syntax
This section covers workflow-level keys beyond names and triggers, which are described in Workflow names and triggers.
Reusable workflows
🟡 Supported subset. Calls may use a local path or a literal GitHub reference such as owner/repository/.github/workflows/ci.yml@v1. A remote reference resolves once per operation to an immutable commit and repository digest. Nested ./.github/workflows/... calls resolve in that pinned repository.
Self-repository calls such as $/.github/workflows/ci.yml select the repository and exact commit containing the calling workflow. They use the same source verification described under self-repository actions, and retain the existing reusable-workflow access, secret-forwarding, cycle, and depth limits.
Verified self calls in the root workflow's repository support secrets: inherit and explicit secret mappings. Nested $/ and ./ calls retain this forwarding scope. An explicit owner/repository/...@ref call leaves the scope, even if it names the same repository and commit; its nested self or local calls cannot restore it. Every forwarding edge still needs its own secrets declaration.
Private references work for the pipeline repository and cross-repository sources available to the importer's existing Git credentials. Enable them with the plugin's default-off private-reusable-workflows field or the matching upload flag. When the Buildkite agent repository-provider credential helper supplies access, Buildkite approves each requested repository. Git access is also used when GitHub's anonymous API quota is exhausted, so a rate limit does not fail an otherwise authorized call. Missing and denied repositories, refs, and paths produce the same error.
✅ Supported:
- Local
./.github/workflows/...paths. - Literal public or approved private references to a
.ymlor.yamlfile directly underowner/repository/.github/workflows/. -
boolean,number, andstringinputs. - Static input values. Caller values may use graph-time
github, matrix, and parent reusable-workflow inputs with the supported operators and pure functions. - String inputs that read
needs.<job>.outputs.<name>, alone or inside a larger value such astype=raw,value=${{ needs.meta.outputs.tag }}or${{ format('{0}-{1}', github.ref_name, needs.meta.outputs.tag) }}. The call must list each job inneeds. Every other part of the value must resolve before jobs run: literals, graph-timegithub,vars, matrix values, static parent inputs, and the supported operators and pure functions. Buildkite Pipelines resolves the verified outputs and renders the value before each flattened callee job runs. - Boolean and number inputs derived from needs outputs through a single expression, such as
deps: ${{ needs.detect.outputs.deps == 'true' }}orcount: ${{ fromJSON(needs.detect.outputs.count) }}. The same dependency and graph-time restrictions apply. Runtime evaluation preserves the declared type and rejects a mismatched result; a raw output string is not a boolean or number. - Forwarding a needs-dependent parent input to a nested call as exactly
${{ inputs.<name> }}, with the same declared type. - Literal defaults and expression defaults over graph-time
githubvalues. - Nested calls up to four levels.
-
secrets: inheritfor repository-local calls. Each nested edge must repeat it. - Explicit repository-local mappings from a declared callee alias to one direct
${{ secrets.NAME }}or${{ secrets['NAME'] }}caller reference. - Required and optional
on.workflow_call.secretsdeclarations. - Caller-visible aggregate results.
- Outputs mapped directly from
jobs.<job>.outputs.<name>. - Call-level
ifover callergithub,inputs, directneeds, and status functions. - Workflow-level concurrency in local and public called workflows, including behind a call-level
if. Groups may use the called workflow's static inputs. Each static call-matrix instance gets its own workflow gate. See Concurrency.
❌ Unsupported:
- Dynamic workflow paths.
- Secret forwarding for public remote calls.
- Literal, compound, dynamic, or non-secret explicit mapping values.
-
needs.<job>.result, wholeneeds.<job>.outputsobjects, or needs values mixed with runtime-only values such asgithub.run_idin inputs. - Combining a needs-dependent parent input with other text in a nested call.
- Dynamic matrices.
- Input defaults that reference
inputs. - Literal or compound output expressions.
Job-level uses, with, and secrets follow these boundaries.
If compilation fails after the complete job graph expands, upload preserves one Buildkite Pipelines item per expanded job. Jobs with compiler errors fail with their own diagnostics, and their dependants are skipped. Independent jobs keep their compiled plans and run normally. The items keep their normal labels, keys, checks, and needs links. A runnable job is never emitted unless every job it needs also has a plan. If compilation fails before the complete graph is known, or the workflow takes a matrix or runner selection from a job output, upload uses one workflow-level failing item instead.
For a local call with secrets: inherit, each flattened callee job requests only the static ordinary secret names referenced by that job or its workflow-authored action inputs. Inheritance is one hop: an omitted nested secrets: inherit removes ordinary secret authority from every job below that edge. It does not affect direct caller jobs or GITHUB_TOKEN.
Explicit mappings must target aliases declared by the called workflow. Every required alias must receive authority; an unmapped optional alias is empty. Nested mappings compose to the original Buildkite secret name and cannot recover an omitted same-named secret. Plans contain aliases and original names, never values. The runtime retrieves each original once, registers its value with both redactors, then projects it to the callee aliases.
${{ secrets.GITHUB_TOKEN }} may be forwarded to a declared alias. The alias remains part of the scoped workflow-token contract and never becomes an ordinary Buildkite secret.
Upload configures Git fallback before validating remote calls. After anonymous access fails, Git fetches a canonical credential-free HTTPS URL with the importer's existing credential configuration. Each Git invocation allows only the HTTPS transport, refuses redirects, verifies TLS, and checks received objects; these values are pinned for the exact repository URL and Git environment variables that would relax them are removed. The fetch stops if inherited url.<base>.insteadOf configuration rewrites the URL. Git pack input and extracted trees keep the anonymous source size and entry limits. Git output is suppressed, terminal prompts and askpass programs are disabled and inherited http.extraHeader and http.cookieFile values are reset so credentials come only from credential helpers, and credential material is never added to plans, generated pipeline YAML, workflow environments, or runtime jobs. Called repositories that may use Git fallback skip the one-hour mutable ref cache and resolve once per operation. Private actions remain unsupported.
A call condition runs in caller scope before static call-matrix expansion. It keeps the implicit success() guard. A false condition skips every flattened descendant, including jobs with if: always(), and exposes skipped with empty outputs to downstream needs. Nested calls evaluate ordered outer-to-inner guards. Callee job results do not change an outer guard. Call conditions cannot use matrix, strategy, callee inputs or needs, steps, env, runner, or secrets.
A call's needs governs its condition and scheduling, but does not appear in the called jobs' needs context. Each called job sees only dependencies it declares within its own workflow; a job without needs sees {} when serialized with toJSON(needs).
A call without if checks only dependency results; conflicting matrix outputs do not affect that implicit success check.
The called workflow declares its inputs and outputs:
on:
workflow_call:
inputs:
target:
type: string
required: true
outputs:
image:
value: ${{ jobs.build.outputs.image }}
jobs:
build:
runs-on: ubuntu-latest
outputs:
image: ${{ steps.image.outputs.value }}
steps:
- id: image
run: echo "value=app-${{ inputs.target }}" >> "$GITHUB_OUTPUT"
The caller can pass a static input:
jobs:
build:
uses: ./.github/workflows/build.yml
with:
target: production
Or defer a string input until a prerequisite publishes its output:
jobs:
call:
needs: prepare
uses: ./.github/workflows/build.yml
with:
target: ${{ needs.prepare.outputs.target }}
Permissions
🟡 Supported subset. Permissions matter only when a job statically references secrets.GITHUB_TOKEN or github.token, or an effective action input default can reach github.token for the event provider.
A workflow-level permissions map can request repository access:
permissions:
contents: read
pull-requests: write
Supported values are read, write, and none. Supported repository permission names are actions, artifact-metadata, attestations, checks, contents, deployments, discussions, issues, packages, pages, pull-requests, security-events, and statuses. The separate id-token permission is also supported.
An omitted map defaults to exactly contents: read when a token is needed. This deterministic default does not inherit GitHub repository or organization settings. Hosted token issuance uses only the top-level map. Job-level repository permission maps do not narrow or expand GITHUB_TOKEN; the separate id-token permission retains job-level behavior. Write access therefore requires an explicit top-level map.
Put permissions: read-all at the top level to apply read access to all 13 supported repository permissions listed above. This shorthand excludes id-token, models, repository-projects, code-quality, metadata, and vulnerability-alerts. The compiler also accepts permissions: write-all, but Buildkite Pipelines rejects it when issuing the token, so jobs in that workflow can't receive GITHUB_TOKEN.
Jobs expanded from reusable workflows use the top-level requesting workflow's repository permissions for GITHUB_TOKEN. Only this immutable top-level map is enforced server-side; permission maps in called workflows do not narrow GITHUB_TOKEN. The separate id-token permission retains called-workflow narrowing. Warnings identify job-level repository maps that differ from the applied top-level permissions and called-workflow maps that would have narrowed the token scope.
To grant repository access, list each required permission at the top level, such as contents: write. Every job that receives GITHUB_TOKEN gets that access. You cannot give different repository permissions to individual jobs. Use top-level read-all only when every job should get read access for every supported repository permission. For write access, list each permission explicitly. An empty top-level permissions map, or one that contains only none, creates no token. Use GitHub's exact permission names.
Environment and defaults
| Key | Status | Behavior | |||
|---|---|---|---|---|---|
| Key | env |
Status | 🟡 Supported subset | Behavior | Workflow, job, and step maps use normal precedence; the most specific value wins. Individual values may use supported interpolation. An entire map cannot be expression-valued. |
| Key | defaults.run.shell |
Status | 🟡 Supported subset | Behavior | Supported at workflow and job level, subject to shell and platform limits. Windows defaults to pwsh, other host jobs to bash, and job containers to sh. |
| Key | defaults.run.working-directory |
Status | 🟡 Supported subset | Behavior | Supported at workflow and job level for workspace-relative paths. |
A job-level value overrides the same workflow-level environment variable:
env:
GOFLAGS: -mod=readonly
jobs:
test:
env:
GOFLAGS: -race
Workflow-level run defaults set the shell and working directory:
defaults:
run:
shell: bash
working-directory: ./src
Concurrency
🟡 Supported subset with different queue behavior. A static group becomes a repository-scoped, case-insensitive Buildkite Pipelines concurrency group. Groups may use supported github fields, static reusable-workflow inputs, and concrete matrix and strategy values at job level. Core operators, fromJSON, and the case-insensitive string functions startsWith, contains, and endsWith are supported when the whole expression resolves during compilation. A needs-derived matrix consumer can also read its producer's outputs for its job group; see Scheduling from matrix-producer outputs. Other runtime needs values remain unsupported.
A workflow can set a group and cancellation expression while a job uses a matrix-derived group:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }}
jobs:
deploy:
concurrency: deploy-${{ matrix.target }}
Called workflows keep workflow-level concurrency around all flattened jobs. Inputs can derive the group from a caller matrix:
on:
workflow_call:
inputs:
target:
type: string
required: true
concurrency: deploy-${{ inputs.target }}
jobs:
plan:
runs-on: ubuntu-latest
steps:
- run: ./plan
apply:
needs: plan
runs-on: ubuntu-latest
steps:
- run: ./apply
jobs:
deploy:
strategy:
matrix:
target:
- staging
- production
uses: ./.github/workflows/deploy.yml
with:
target: ${{ matrix.target }}
Nested called workflows keep nested gates. Jobs within one called workflow remain parallel except for their declared needs and job-level concurrency. Jobs or nested called workflows that reuse an enclosing workflow group remain unsupported.
Calls with needs are supported when their prerequisites have no concurrency or statically disjoint groups. The compiler checks transitive prerequisites, matrix instances, and nested reusable-workflow groups. A prerequisite sharing the called workflow's group, an unresolved group, or a cycle involving gate dependencies and the ordered concurrency queues of Buildkite Pipelines is rejected. For example, needs: prepare is supported for a called workflow in group deploy when prepare has no concurrency or uses a separate group such as build.
The opening gate waits for every external prerequisite, including failed or skipped jobs, so runtime conditions still decide whether the call runs. Pipelines reserves the gate's queue position at pipeline upload, before those prerequisites finish; GitHub admits the called workflow afterward. A later call or build using the group can therefore wait behind an earlier call whose prerequisites are unfinished. W_REUSABLE_WORKFLOW_CONCURRENCY_QUEUED_BEFORE_PREREQUISITES reports this difference once per root call. Mutual exclusion is preserved. This analysis covers the generated pipeline, not dependencies introduced by other pipelines or later manual uploads.
A call with if keeps the called workflow's gate unless its condition is already false. Pipelines reserves the group at pipeline upload, before the runtime evaluates the condition:
| Call condition at compile time | Gate | Condition diagnostic | |||
|---|---|---|---|---|---|
| Call condition at compile time | True, such as github.event_name == 'pull_request' on a pull request |
Gate | Emitted | Condition diagnostic | None |
| Call condition at compile time | False, such as the same condition on a push | Gate | Omitted; the jobs skip without entering the group, as on GitHub | Condition diagnostic | None |
| Call condition at compile time | Runtime-dependent, such as vars.DEPLOY == 'true'
|
Gate | Emitted; a skipped call still waits for the group and holds it until its jobs finish | Condition diagnostic | W_REUSABLE_WORKFLOW_CONCURRENCY_ENTERED_BEFORE_CALL_CONDITION |
Pipelines queues every waiting entry. It does not replace GitHub's existing pending entry. The queue key is unsupported.
A workflow with workflow-level concurrency cannot hold a matrix from fromJSON(needs.<job>.outputs.<name>); see Matrices from job outputs.
When workflow-level cancel-in-progress is true, Pipelines warns and leaves superseded builds running. The same behavior applies when an expression resolves to true and when a called workflow sets it. Job-level cancel-in-progress is unsupported.
In an explicit-workflow pipeline, to cancel earlier running builds on the same branch, turn on Cancel Intermediate Builds under pipeline Settings > Builds. This setting works by branch, not by concurrency group. Leave it disabled for server-side dispatch, where it can cancel sibling workflow builds from the same event.
Cancel the whole Buildkite build rather than one job when a workflow-level concurrency gate is active.
Job syntax
Job configuration
| Key | Status | Behavior | |||
|---|---|---|---|---|---|
| Key | name |
Status | ✅ Supported | Behavior | Labels may use static github, reusable-workflow inputs, and concrete matrix and strategy values. |
| Key | needs |
Status | ✅ Supported | Behavior | Accepts a string or list of static job IDs. Matrix fan-out and fan-in are automatic. |
| Key | runs-on |
Status | 🟡 Supported subset | Behavior | Explicit mappings are authoritative. The Agent API returns a complete target for every other selector and can return a fallback warning annotation. The local preset accepts ubuntu-latest, ubuntu-24.04, ubuntu-22.04, and macos-latest. Labels are case-insensitive. Expressions may use concrete matrix and strategy values or values from a job output to resolve to an accepted label or label list. |
| Key | if |
Status | 🟡 Supported subset | Behavior | Runs before the job starts. See Conditions. |
| Key | outputs |
Status | 🟡 Supported subset | Behavior | Maps step outputs for consumption through needs. A job may publish 64 outputs of up to 1 KiB each. Ambiguous matrix output values stop the job with an error. |
| Key |
env, defaults.run
|
Status | 🟡 Supported subset | Behavior | Uses the workflow-level behavior. |
| Key | timeout-minutes |
Status | 🟡 Supported subset | Behavior | Accepts literal timeouts up to 360 minutes. Expressions are rejected. |
| Key | continue-on-error |
Status | ✅ Supported | Behavior | Accepts literal Boolean values or expressions that produce a Boolean. A tolerated failure remains visible as a Buildkite Pipelines soft failure and reports success through downstream needs. |
| Key | environment |
Status | 🟡 Supported subset | Behavior | Requires GitHub environment access at compile time. See Deployment environments. |
| Key | snapshot |
Status | ➖ Accepted, no effect | Behavior | Custom image creation is not implemented. |
Job names can interpolate matrix values:
jobs:
test:
name: Test Go ${{ matrix.go }}
A job can depend on another job by ID:
jobs:
build:
# ...
test:
needs: build
Runner labels can resolve from a matrix value:
runs-on: ${{ matrix.os }}
A job-level condition can combine a branch check with status:
if: github.ref == 'refs/heads/main' && success()
Job outputs can pass a step output to a dependent job:
jobs:
build:
runs-on: ubuntu-latest
outputs:
image: ${{ steps.image.outputs.value }}
steps:
- id: image
run: echo "value=app:latest" >> "$GITHUB_OUTPUT"
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- run: echo "${{ needs.build.outputs.image }}"
Results and outputs come from verified producer manifests. Retrying one producer can make selection ambiguous; retry the whole build.
A job with continue-on-error: true, or an expression that resolves to true, stops ordinary steps after a failure, runs eligible failure and always steps plus post-actions, publishes its outputs, and reports success through needs.<job>.result. The generated Buildkite Pipelines job returns reserved status 78 for the tolerated workflow failure and soft-fails only that status, so the failure remains visible without blocking dependent jobs. Job timeout expiry remains cancelled and is never tolerated.
Runner labels are case-insensitive. Runner aliases such as macOS-latest use the same target as macos-latest. Local Linux presets use the corresponding Noble or Jammy hosted-toolchains image with default Buildkite agent targeting. An explicit immutable image overrides the preset for a configured profile.
An explicit mapping overrides automatic runner selection. It declares that the selector runs on Linux x86-64, except for known Linux arm64 labels (ubuntu-24.04-arm, ubuntu-22.04-arm, and labels ending in -arm64 or -aarch64), known macOS labels, and Windows labels. Linux arm64 requires an explicit queue and rejects images; this does not provision or enable a Buildkite hosted ARM queue. macOS and Windows also reject images. During import, the job-scoped Agent API checks that the configured queue exists in the job's cluster and that a hosted queue matches the declared OS and architecture. Self-hosted queues are checked for existence only; their operators remain responsible for the agents' platform. Successful validation preserves the configured queue, image, and cache. For every other selector, the job-scoped Agent API owns compatibility and returns the complete queue, platform, and host environment. The importer applies that target verbatim and publishes returned fallback warnings as annotations.
For eligible Namespace-backed Linux queues, the backend selects native GitHub Actions images through opaque Buildkite agent tags, such as agents: {queue: linux-medium, nsc-gha-image: ubuntu-24.04}, instead of a step-level image. The backend owns eligibility and label mapping; the CLI does not interpret provider tag names. macOS and Windows do not use this path. Immutable image targets remain supported for other agents, explicit mappings, heuristic fallbacks, and older backends. A workflow's jobs.<job>.container.image remains a separate job container executed inside the selected host environment.
When the Agent API rejects a selector, the importer reports the server's reason at the job's runs-on in the workflow diagnostics annotation instead of falling back to a built-in preset, so a cluster without the expected hosted queue fails before pipeline upload with the cluster and queue named:
| Rejection | Meaning | ||
|---|---|---|---|
| Rejection | missing_queue |
Meaning | The labels are compatible, but the job's cluster has none of the hosted queues they need. Create the named queue or configure an explicit runner mapping. |
| Rejection | incompatible_labels |
Meaning | The selector is unsupported, or hosted Windows access is unavailable. See Configure a Windows runner before changing Windows labels. |
| Rejection | no_cluster |
Meaning | The job is not in a cluster, so no hosted queue can be selected. |
| Rejection | queue_not_found |
Meaning | The explicitly configured queue is not active in the job's cluster. Correct the mapping or create the queue. |
| Rejection | queue_platform_mismatch |
Meaning | The configured hosted queue has a different OS or architecture. Map the label to a compatible queue. |
Windows rejections preserve the server's reason and add setup or variant guidance for incompatible selectors. Unknown rejection codes render the server message with generic mapping guidance.
Imports using explicit mappings require job-scoped Agent API credentials and a server that acknowledges configured-target validation. If validation is unavailable, the import stops before uploading jobs rather than trusting an unchecked mapping. Imports without explicit mappings still warn and fall back to built-in presets when the Agent API cannot be reached.
Explicit mappings can also attach one Buildkite hosted cache volume to generated jobs. This configuration is outside the GitHub workflow and does not change workflow syntax or action inputs.
Offline validate and compile do not check queues against the cluster. validate --profile hosted has no job-scoped API and admits only the local ubuntu-latest, ubuntu-24.04, ubuntu-22.04, and macos-latest presets.
Runners from job outputs
🟡 Supported subset. A job without a matrix can select its runner from one declared output of a prerequisite with exactly one static instance:
plan:
runs-on: ubuntu-latest
outputs:
runner: ${{ steps.select.outputs.runner }}
steps:
- id: select
run: echo 'runner=["ubuntu-22.04"]' >> "$GITHUB_OUTPUT"
build:
needs: plan
runs-on: ${{ fromJSON(needs.plan.outputs.runner) }}
steps:
- run: echo ready
A plain output can supply a single label with runs-on: ${{ needs.plan.outputs.runner }}. Label templates, label lists, and expressions using the existing pure functions are also supported. Every output reference, including in an unselected branch, must name the same producer output through needs.<job>.outputs.<name>. A reusable workflow may receive that value through a string input and forward the input unchanged to another workflow. Its caller's output retains its original producer binding; it does not become part of the callee's needs context.
The initial upload creates :github: runs-on · build, with check build (runs-on), and reports W_RUNS_ON_DEFERRED. This step reads the verified output, recompiles the workflow, and uploads build and every job that transitively depends on it. Runner mappings, live Agent API resolution, platform availability, admission, and token authority apply as for static jobs. Runner fallback warnings appear as annotations on the deferred step. The output supplies scheduling data only, never workflow source or credential authority.
Runner selection uses the same deferred upload rules for snapshots, action locks, graph budgets, downstream ownership, skipped producers, and retries. The producer output is limited to 1 KiB. Independent runner and matrix continuations may coexist, but a component containing runner selection cannot join or start another deferred stage. Matrix-only components retain their supported joins and chained stages. A dynamic-runner consumer cannot also declare a matrix. Workflow-level concurrency is unsupported on the root or on a reusable workflow containing or depending on a deferred job. This does not add dynamic environments or concurrency values. Missing outputs, invalid label types, and rejected runner targets fail the deferred step without uploading its jobs.
Use validate or compile --format ir-json to inspect the deferred graph; compile --format pipeline cannot emit it. See Expand a matrix inside the build.
Deployment environments
A job-level environment needs GitHub environment configuration at compile time. Inside a Buildkite job, upload and compile resolve each declared environment automatically through the job-scoped Agent API (github-actions/environments). The Buildkite backend reads the environment's protection rules, secret names, and variables from GitHub with its own credentials, restricted to the pipeline's configured repository, and returns only that snapshot: no GitHub token and no secret value ever reaches the importer. This is the only environment access path; there is no GitHub token option. Resolution is GitHub.com-only, so GitHub Enterprise Server repositories cannot declare environments. Any resolution failure fails the compile instead of degrading to an unprotected default, and environments are only resolved when a workflow declares them. One upload resolves all of its distinct environments in one batched request. The backend owns the resolution limits (at most 20 environments per request plus per-job and per-App-installation hourly budgets) and its rejection fails the compile with the error from the backend.
Outside a Buildkite job, compile has no environment access, so workflows that declare environments fail to compile with an error naming the job. validate --profile hosted reports the same failure as a diagnostic.
| Environment feature | Behavior | ||
|---|---|---|---|
| Environment feature | Literal environment name, with or without url
|
Behavior | ✅ Supported on top-level workflow jobs. Expression names and reusable-workflow jobs are rejected. |
| Environment feature | Required reviewers | Behavior | 🟡 One Buildkite Pipelines block step per workflow and environment gates the affected jobs. Any user who can unblock the pipeline can approve; GitHub reviewer lists, prevent_self_review, and administrator bypass are not enforced. |
| Environment feature | Environment secrets | Behavior | 🟡 Referenced secret names defined in the environment resolve to the Buildkite secret <ENVIRONMENT>_<NAME>. Other names resolve unchanged. Values stay in Buildkite secrets; only names are read from GitHub. |
| Environment feature | Environment variables | Behavior | 🟡 ${{ vars.NAME }} resolves in runner-evaluated fields of jobs that declare the environment, over repository and organization variables. Job if and compile-time fields never see environment variables. See Repository and organization variables. |
| Environment feature | Wait timers | Behavior | ❌ Rejected at compile time. |
| Environment feature | Deployment branch policies | Behavior | ❌ Rejected at compile time. |
| Environment feature | Custom deployment protection rules | Behavior | ❌ Rejected at compile time. |
| Environment feature |
environment.url, deployment records |
Behavior | ➖ Accepted, no effect. Builds do not create GitHub deployments or deployment statuses. |
Secret DEPLOY_KEY in environment production resolves to Buildkite secret PRODUCTION_DEPLOY_KEY: the prefix is the upper-cased environment name with every character outside A-Z, 0-9, and _ replaced by _, and a leading _ added when the name starts with a digit (environment 1st gives _1ST_DEPLOY_KEY). Distinct environments keep distinct values under Buildkite secret access policies. Generated keys must be keys that Buildkite secrets can store: compilation fails when a key would exceed 255 characters or begin with BK or BUILDKITE, so rename such environments or secrets.
The approval gate is a block step with blocked_state: running, so jobs without a gate keep running while approval is pending. The gate appears whenever the workflow compiles, even if the gated job's own condition would skip it. Matrix instances of one job share one gate. Gated jobs cannot be retried manually; run a new build for a fresh approval.
Environment configuration is read once per compile, so changes on GitHub apply to the next build. Buildkite OIDC tokens do not carry a GitHub environment claim.
Repository and organization variables
${{ vars.NAME }} reads configuration variables by scope. Inside a Buildkite job, upload and compile read the event repository's repository and organization variables through the job-scoped Agent API (github-actions/variables) when any applicable workflow, a reusable workflow it calls, or an input default of an action it uses references vars; workflows without a vars reference make no request, and neither do events from providers other than GitHub.com. One upload makes at most one request, whether or not a workflow declares an environment. The Buildkite backend reads the variables from GitHub with its own credentials, restricted to the pipeline's configured repository, and bounds the response (500 repository and 1000 organization names, 48 KiB per value, 256 KiB combined). Its rejection, rate limit (10 requests per job per hour), or GitHub outage fails the compile of every workflow that references vars with the error from the backend and any Retry-After delay; other workflows still upload. Variable-resolution failures use diagnostic code E_VARIABLE_RESOLUTION; other processing-environment failures continue to use E_ENVIRONMENT.
If the backend returns only GitHub variables could not be resolved, the CLI cannot identify the cause. Contact the Buildkite Support team at support@buildkite.com with the build URL so they can investigate the server-side failure. When the backend provides a specific policy, permission, installation, or repository-access error, the CLI preserves it.
A backend without the endpoint, or an organization that has opted out, returns 404, which resolves both scopes as empty rather than failing the compile. A repository and organization that define no variables resolve the same way. Either way every vars name evaluates to an empty string, in compile-time fields too, so runs-on: ${{ vars.FAILOVER_RUNNER || 'ubuntu-latest' }} selects ubuntu-latest. Outside a Buildkite job, compile has no variable source: runtime references evaluate to empty strings, and compile-time fields that reference vars fail to compile.
Each job's plan carries the scopes as organization_vars, repository_vars, and, for jobs that declare an environment, environment_vars. The compiler and runtime use these scopes:
| Position |
vars context |
||
|---|---|---|---|
| Position |
jobs.<id>.if and reusable-workflow call if
|
vars context |
Repository over organization variables. GitHub evaluates these before the job's environment applies, so environment variables are never visible here. Check environment variables in a step if. |
| Position | Job env, defaults.run, outputs, service env and credentials, every step field, and action input defaults |
vars context |
Environment over repository over organization variables. |
| Position | Compile-time fields (run-name, runs-on, strategy, concurrency, job names, container images, reusable-workflow inputs) |
vars context |
Repository over organization variables. Without a source, such as compile outside a Buildkite job, a reference fails to compile. environment names must stay literal. See Compile-time expressions. |
The GitHub variable reference documents environment-over-repository-over-organization precedence, but also says environment-level variables arrive after job start and "won't overwrite variables in the env and vars contexts." The table records the behavior of Buildkite Pipelines. The server-side scope merge in GitHub remains unconfirmed by a native run; runner source establishes evaluation order, not that merge.
Names match case-insensitively, and a higher scope replaces a lower scope's name spelled differently. A name no scope defines evaluates to an empty string, as on GitHub; it is not a compile error. Dynamic access such as vars[matrix.name], vars.*, and toJSON(vars) reads the same per-position context. Matrix instances share their job's environment variables; jobs without an environment, including every reusable-workflow job, see repository and organization variables only. GITHUB_TOKEN and secret authority planning never resolves vars: a job or step gated by if: vars.PUBLISH == 'true' keeps its token and secret requests whatever the variable's value, because every condition keeps vars for the runtime to evaluate, and a step input such as ${{ vars.ENABLED == 'yes' && github.token || '' }} requests the token and fails under permissions: {} as it does without variables.
deploy:
runs-on: ${{ vars.RUNNER }}
if: vars.DEPLOY_ENABLED == 'true'
environment: production
env:
AWS_REGION: ${{ vars.AWS_REGION }}
steps:
- if: vars.TIER == 'gold'
run: echo "$AWS_REGION"
Values are plain configuration, not secrets: they are stored in the build's job plan artifacts and are visible to anyone who can read build artifacts, and compile --format ir-json prints both scopes in the IR whenever the workflow references vars. A value a compile-time field uses also appears wherever that field does: a job name or matrix value becomes a step label in the pipeline YAML, and a runner label that cannot be mapped is quoted in the compile diagnostic. Runtime references, processing reports, and resolution errors never carry values. See Variables in the security guide.
Matrix strategies
| Key | Status | Behavior | |||
|---|---|---|---|---|---|
| Key | matrix |
Status | 🟡 Supported subset | Behavior | Literal rows. Authored values and expression-valued definitions can use compile-time github, event, reusable-workflow inputs, and fromJSON values. A whole matrix can be fromJSON(needs.<job>.outputs.<name>); see Matrices from job outputs. |
| Key |
include, exclude
|
Status | 🟡 Supported subset | Behavior | Literal combinations or expressions that resolve to arrays of objects during compilation. A whole include list can be fromJSON(needs.<job>.outputs.<name>). |
| Key | max-parallel |
Status | 🟡 Supported subset | Behavior | Literal value on ordinary job matrices, or a limit from the matrix producer's output. Reusable-workflow call matrices with more than one instance are rejected because flattening cannot preserve invocation-level parallelism. |
| Key | fail-fast |
Status | ➖ Accepted, no effect | Behavior | A failed matrix entry does not cancel its siblings. |
Each expanded job retains four scalar strategy values, including jobs in called workflows and matrices expanded from job outputs:
| Property | Value | ||
|---|---|---|---|
| Property | job-index |
Value | Zero-based position in the final expanded rows, after exclusions and includes. It follows expansion order, not sorted matrix keys. |
| Property | job-total |
Value | Number of final expanded rows. |
| Property | fail-fast |
Value | Configured Boolean, or true when omitted. This reports the setting; it does not enable cancellation. |
| Property | max-parallel |
Value | Configured limit, or the number of final expanded rows when omitted. |
A job without a matrix has index 0, total 1, and default parallel limit 1. Values follow the runner's matrix expansion and singleton defaults. See Runtime interpolation for admitted fields.
A strategy can combine parallelism, static matrix values, and exclusions:
strategy:
max-parallel: 2
matrix:
go:
- "1.25"
- "1.26"
os:
- ubuntu-22.04
- ubuntu-24.04
exclude:
- go: "1.25"
os: ubuntu-24.04
Whole matrices, dimensions, and include or exclude lists can use static JSON:
strategy:
matrix:
os: ${{ fromJSON(inputs.OPERATING_SYSTEMS) }}
include: ${{ fromJSON(github.event.inputs.extra_jobs) }}
exclude: ${{ fromJSON(github.event.matrix_exclusions) }}
Static matrices on reusable-workflow calls compose with static matrices in called workflows, including nested calls. Each call instance receives its concrete matrix values and each called workflow receives its declared inputs before its matrix expands.
A job may expand to at most 256 instances.
Matrices from job outputs
🟡 Supported subset. A matrix whose whole matrix or whole include list is exactly fromJSON(needs.<job>.outputs.<name>) expands after the producing job runs:
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) }}
publish:
needs: build
runs-on: ubuntu-latest
The initial upload creates plan and one deferred step, :github: matrix · build, with check build (matrix). That step waits for plan, reads the verified matrix output, expands it with the static-matrix rules, recompiles only build and the jobs that transitively need it, such as publish, and uploads them with the initial workflow's grouping. Every other job is uploaded once, up front. The deferred step reports W_MATRIX_DEFERRED at compile time and leaves the deferred jobs not-evaluated in processing reports, so validate and compile --format ir-json show the graph shape while compile cannot render the pipeline YAML for the workflow. See Expand a matrix inside the build.
Deferred matrices can join. Each matrix owns every downstream job, including dependencies introduced by reusable-workflow inputs and call conditions. When two downstream sets intersect, they merge under one deferred step; merging continues through indirect intersections. A job with needs: [build, test] gives the deferred build and test matrices one owner, which uploads both branches and their join once. Sharing only a producer does not merge otherwise independent matrices. Prerequisites outside the downstream sets stay in the initial upload. The owner waits for all its matrix producers before expanding any branch.
Deferred matrices can chain. When a matrix reads its rows from a job that is itself deferred, the component expands in stages:
package:
needs: build # build is the deferred matrix above
runs-on: ubuntu-latest
outputs:
targets: ${{ steps.targets.outputs.targets }}
deploy:
needs: package
runs-on: ${{ matrix.runner }}
strategy:
matrix:
include: ${{ fromJSON(needs.package.outputs.targets) }}
release:
needs:
- publish
- deploy
runs-on: ubuntu-latest
The initial upload still creates one deferred step for the component, :github: matrix · build, which owns build, package, publish, deploy, and release. After plan runs, that step expands build, uploads package and publish, and adds the next stage's step, :github: matrix · deploy, which waits for package and uploads deploy and release. Each stage recompiles the workflow with the rows the stages before it resolved and uploads only the jobs whose rows now exist, to any depth. W_MATRIX_DEFERRED names the matrices that later stages expand.
The expanded jobs are identical to the jobs a literal matrix with the same rows produces: same step keys, labels, checks, needs, and outputs. Row values reach runs-on, name, if, env, and steps exactly as static matrix values do, and nothing else. runs-on: ${{ matrix.runner }} resolves through the importer's explicit runner mappings or the same Agent API resolution as static jobs, with the same admission, capability, and token rules, so a producer cannot select an unmapped queue or widen permissions. Separate declared outputs can supply the consumer's scheduling values within that subset's limits. The deferred step also reads remote reusable workflows and actions the way the importer did, including through Git when private-reusable-workflows is enabled.
Limits and rejected shapes:
- The matrix producer must run on Linux or macOS: its queue also runs the deferred upload, and Windows importers are unsupported. Expanded jobs may target an explicitly enabled Windows queue when its runtime is provided.
- The producer must be a job with exactly one instance, so it has no matrix or a matrix that expands to one row, and its output must be declared in
outputs. The output value is JSON of at most 1 KiB, the job output limit, so a large matrix must stay compact. Rows are objects of scalar values with at most 64 properties each; the expansion honors the 256-instance and 1,024-job limits. - The deferred uploads of a workflow share the jobs the 1,024-job limit leaves after the jobs uploaded up front, in equal parts: with four static jobs and two independent deferred components, each component may upload at most 510 jobs, counting every root's rows and each downstream instance once. The share is recorded at upload time, so the deferred steps together cannot grow the build past the limit whatever the producers publish. In a chained component each stage passes the share it did not use to the next stage. A producer output that needs more than the share fails the deferred step before it uploads anything, and a share too small for the jobs a deferred step already promises fails the workflow with
E_MATRIX_INVALIDat upload time. - An output that is missing, not JSON, the wrong shape, has zero rows, exceeds a limit, or names a runner that fails compilation or admission fails the deferred step, and the dependent jobs never run. The step prints the compile diagnostics.
- A called workflow with workflow-level
concurrencycan neither hold a needs-derived matrix nor need a deferred job. A workflow with its own workflow-levelconcurrencycannot hold a needs-derived matrix: the group is released when the jobs of the initial upload finish, before the jobs a deferred step adds. These fail the workflow withE_MATRIX_INVALIDat upload time. - The step keys of the deferred step of every stage, of each consumer's placeholder, of every statically known instance of a deferred dependent, and of the approval gate of every environment a deferred job declares are reserved at upload time. A key that collides with another job's key or gate, such as a static or deferred job
build-matrixnext to a needs-derived matrixbuild, a deferred job whose static matrix has duplicate rows, or a job whose ID equals an approval gate key, fails the workflow withE_MATRIX_INVALIDbefore anything runs, instead of failing the later upload or letting a job stand in for an approval gate. Only the keys the deferred jobs will take are reserved: a reusable-workflow jobcall.publishwith a matrix does not block a static jobcall-publish. - The deferred step reads the workflow again from a checkout of the build commit, so the workflow must be a tracked file inside the repository whose content at that commit (
BUILDKITE_COMMIT, orHEADwhen the agent did not resolve it) is the content the importer compiled. A custom importer that uploads one workflow from outside the repository, an untracked or only staged file, or a file edited since the commit fails the upload when that workflow holds a needs-derived matrix, before anything runs. - Deferred jobs that deploy to a protected environment use the same approval gate as static jobs. When no static job created the gate, the first deferred step to upload creates it and later deferred steps reference it. A step whose upload was rejected because another step created a shared gate in the meantime uploads again, referencing that gate and still creating the gates only it uses.
- The importer records the workflow event once per upload, as one artifact shared by every deferred step, so many needs-derived matrices do not multiply the artifact size.
- When a producer fails, is skipped, or is cancelled, the deferred step uploads skipped placeholders for the deferred jobs: one for the consumer, one for each matrix a later stage would have expanded, and one per statically known matrix instance of each dependent, under the keys and check names a static expansion would use. Dependents with
if: always()are skipped as well; this subset does not run descendants of an unknown matrix. In a merged component, only that root's downstream jobs are skipped; healthy branches still expand, and a shared join is skipped once. In a chained component, a producer that does not succeed skips only the jobs from its stage on; the jobs earlier stages uploaded stay as they are. Missing or invalid result manifests fail the deferred step without uploading jobs, even when another producer did not succeed. They are never treated as skips. - The deferred step recompiles the workflow from the checkout at the build commit with the event, variables, runner mappings, OIDC settings, and rows of earlier stages the importer or the step before it recorded, and requires the result to reproduce the jobs the earlier uploads created, to leave the workflow's other deferred matrices exactly as recorded, to defer the next stage exactly as the initial upload promised, and to compile every deferred job from the workflow source the importer recorded for it: the same file content and, for a reusable workflow from another repository, the same commit. Any difference, such as a reusable-workflow tag that now resolves to another commit, fails the step with instructions to retry the whole build.
- The initial upload resolves the actions the deferred jobs use, so an action that cannot be resolved fails the workflow before anything runs, and records each resolved commit and source digest in the stage record. The deferred jobs use those revisions: a public action tag that moves between the initial upload and the deferred step does not change them, and a local action whose files changed in the checkout fails the step.
- Repository and organization variables are resolved once, at upload time, when any job of the workflow reads
varsin the workflow file or in an action it uses, deferred jobs included. The scopes are recorded in the stage record, so a deferred job whose action defaults an input to${{ vars.REGION }}sees the same value a static job would, and the deferred step never requests variables itself. - A workflow that holds a needs-derived matrix is never uploaded job by job. When any of its jobs fails compilation, such as a job whose local action is missing, the whole workflow is replaced with one failing step,
E_PIPELINE_GENERATIONnames the deferred jobs, and no job of the workflow runs. A per-job upload would keep only the static jobs and drop the deferred steps, so the build could pass without them. Without deferred uploads, only the failed job is replaced and independent jobs run; see Reusable workflows. - Retrying the deferred step is safe: a replayed upload is rejected by Buildkite Pipelines because its step keys already exist, and the step then confirms the earlier upload. Each later stage re-reads earlier producers' verified manifests and compares them with the result digests recorded when their matrices expanded. A missing manifest or a changed result, including a new attempt with identical output, fails before any upload; it is not a skip. Retry the whole build after retrying a producer. See Results, retries, and cancellation.
The detail line of E_MATRIX_INVALID says why a reference is not supported. Matrices derived from steps or from anything other than exactly one fromJSON(needs.<job>.outputs.<name>) remain unsupported.
Scheduling from matrix-producer outputs
A job whose matrix comes from a producer output can read other declared outputs of that same ordinary, single-instance producer for scheduling:
build:
needs: plan
runs-on: ubuntu-latest
strategy:
max-parallel: ${{ fromJSON(needs.plan.outputs.parallel) }}
matrix:
include: ${{ fromJSON(needs.plan.outputs.matrix) }}
steps:
- run: ./build "${{ matrix.target }}"
parallel must contain a JSON integer from 1 through 256. The generated limit applies only to this job's matrix in this Buildkite build. See the complete parallelism example.
Alternatively, set concurrency: ${{ needs.plan.outputs.group }} on the matrix job. The group can be a template with direct needs.<job>.outputs.<name> references and supported pure functions, but no other expression contexts. It must resolve to a nonempty string and uses the same repository-scoped, case-insensitive group mapping as a static group. All instances use that group and run one at a time; adding max-parallel does not raise this limit. See the complete group example.
The deferred upload step waits for every job in the workflow's initial upload, not just the producer, before uploading the consumer and its complete downstream graph. It holds no concurrency slot. This conservative delay avoids ordered-queue edges blocking unfinished static prerequisites. Missing, ambiguous, or invalid scheduling outputs fail the deferred upload. Retrying it verifies the scheduling attributes as well as the job-plan digests.
This support requires a real needs-derived matrix. Scheduling on static matrices, non-matrix jobs, downstream dependents, or reusable-workflow calls remains unsupported, as do projected called-workflow outputs and outputs from other producers. The scheduling consumer's deferred component must have only one matrix and one expansion stage: it cannot join or chain another needs-derived matrix. Independent components and components without output-derived scheduling retain the join and chaining support described in Matrices from job outputs. Workflow-level concurrency remains rejected, including called-workflow gates anywhere in the workflow. This does not change gate lifetimes or weaken the early-release rejection described in Matrices from job outputs. The existing retry and cancellation rules still apply; this does not add cancel-in-progress support.
Containers and services
🟡 Supported subset. Linux jobs support job containers and GitHub-compatible services. Runner-mapped Buildkite cache volumes are not supported for jobs that set container.
A typical PostgreSQL service works without Buildkite-specific syntax:
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: test
ports:
- 5432
options: >-
--health-cmd pg_isready
--health-interval 2s
--health-timeout 5s
--health-retries 10
Job containers support image, env, ports, volumes, and options. Services support image, credentials, env, ports, volumes, options, command, and entrypoint. Container env keys must start with a letter or underscore and can otherwise contain letters, digits, underscores, or dots. For example, an Elasticsearch service can use discovery.type: single-node.
- Job container images can use compile-time
github,inputs,strategy, andmatrixvalues. A null or exactly empty evaluated image runs the job on the host, including object-form containers, without applying containerenvorports. For example,container: ${{ matrix.target.container }}selects host execution when the matrix entry omitscontainer. Other results must be strings containing valid image references; whitespace-only results are invalid. Literal images must be non-empty. Secrets,needs, step outputs, and whole or dynamic contexts are unsupported. - Service fields can use compile-time
github,inputs,strategy, andmatrixvalues or runtimeneedsoutputs, including fallback expressions such as${{ needs.build.outputs.image || 'redis:7' }}. An empty evaluated image skips the service. - Service
envalso accepts job environment values and named secrets, including${{ format('{0}-{1}', env.DATABASE, needs.build.outputs.suffix) }}and${{ needs.build.outputs.password || secrets.DB_PASSWORD }}. It evaluates after jobenvand prerequisite outputs, before standard step environment variables are added. Entries read the job environment, not sibling service entries. Named secrets remain required and redacted even in an unused fallback. Dynamic secret access, directgithub.token,runner,job, step outputs, andhashFiles()remain unsupported here; this does not broaden other service fields. - Service
envresolvesvarsat runtime with the job's variable scopes. Direct references, separate expression regions such as${{ env.NAME }}-${{ vars.VALUE }}, and mixed expressions such as${{ format('{0}-{1}-{2}', env.NAME, needs.build.outputs.name, vars.VALUE) }}all use the same snapshot. Other service fields except credentials retain compile-timevarsvalues. - A complete non-credential service map can use
${{ fromJSON(needs.build.outputs.services || '{}') }}. The argument supports needs-output expressions and pure functions. Declare credentials statically so the compiler can prove their secret authority. - Credentials accept values and expressions using
github,vars,secrets,env,needs, knownmatrixvalues, or scalarstrategyproperties, including${{ needs.auth.outputs.password || secrets.REGISTRY_PASSWORD }}. Matrix values resolve per expanded job, including deferred matrices and reusable jobs. Both${{ format('{0}:{1}', matrix.user, secrets.REGISTRY_PASSWORD) }}and${{ matrix.user }}:${{ secrets.REGISTRY_PASSWORD }}work;varsretains its runtime scope. Prerequisite results and outputs resolve before service setup. Ordinary secrets remain in the job's required inventory even in an unused fallback. Dynamic secret names and unsupported contexts fail even in unreachable branches; this does not add workflow-input support. Passwords pass todocker loginthrough standard input. Authentication uses a private per-job Docker configuration and never reads ambient Docker credentials. - Job container volumes accept
DESTINATIONfor an anonymous volume orSOURCE:DESTINATION[:ro|rw]for a named volume or bind mount.DESTINATIONmust be absolute.SOURCEmust be a Docker volume name or absolute host path. A job can define 128 unique declarations. Expressions are unsupported. - Job container options pass through to
docker create, except--network,--net, and--entrypoint, including their--flag=valueforms. Options split into arguments without a shell. Double quotes group arguments; single quotes are ordinary characters. Expressions, line breaks, NUL bytes, and values over 65,536 bytes are unsupported. - Service Docker options pass through except
--networkand its--netaliases, which GitHub Actions does not support. Options can grant privileges, mount host paths, publish ports, and change resource settings. - Service named, anonymous, and absolute bind volumes are supported.
- A job can define 32 services. Each service can define 256 environment entries and 128 ports or volumes.
Service env admission follows the GitHub context table. On 2026-09-30, source inspection confirmed that the GitHub runner evaluates services after job env with the same expression context, populated from server-supplied contexts. See the variable-scope evidence and limits. Startup runner and job contexts remain unimplemented despite their documented GitHub admission.
Matrix credential admission follows the same context table, checked on 2026-09-30. Credential compilation rejects direct github.token; use secrets.GITHUB_TOKEN under the existing token authorization rules.
Implicit GHCR authentication is unsupported; provide explicit credentials. Mutable tags resolve at job start. Use a digest when image immutability matters. Job container images must provide sh and run the mounted self-contained Linux runtime executable.
Each job uses a private Docker bridge network. Container jobs reach services by service name. Host jobs use declared published ports; omitted host ports are assigned dynamically. The job.services.<service> context exposes id, network, and ports.
Service IDs can contain dots, for example s3.docker.test. Use bracket indexing for these keys: ${{ job.services['s3.docker.test'].ports[6379] }}. IDs pass unchanged to network aliases and context keys; s3.docker.test and s3-docker-test remain distinct. Static declarations and expression-generated service maps use the same validation: at most 255 ASCII characters, starting with a lowercase letter or underscore, followed by lowercase letters, digits, underscores, hyphens, or dots. These are the limits that Buildkite Pipelines supports, not a claim that GitHub rejects every other spelling.
On 2026-10-05, a GitHub-hosted run on ubuntu-latest confirmed this behavior on GitHub, both on the host and in a digest-pinned Redis job container. Two services named s3.docker.test and s3-docker-test retained distinct IDs, bracket-indexed id, network, and ports, and separate published ports. The bracket-indexed id and network step condition evaluated true, and the context checks ran rather than being skipped. Asymmetric Redis SET and GET checks inside the job container verified that the DNS aliases reached different services. This observation covers dotted service IDs, not uppercase IDs, other punctuation, or maximum-length DNS names.
A service with a Docker health check must become healthy before steps run. A service without one is ready after it starts. Failures include bounded status, health, port, and log diagnostics.
Cleanup removes the job container, emits masked and bounded service logs, then removes services in declaration order, the network, owned volumes, and private Docker configuration. Unless options specifies --volume-driver, volumes newly created for job container.volumes receive a unique ownership label before container creation, so cleanup can recover them after failed or cancelled creation. Pre-existing named volumes retain their labels and are not removed. Docker removes anonymous volumes attached through job options, a custom driver, or the image when removing the container. Named volumes supplied only through options or created by a custom driver remain because their ownership is ambiguous. Service volumes retain mount-based tracking. Cleanup does not delete unrelated volumes to recover unattributable resources. Remaining owned resources fail the job. Docker resources are not a security or resource-isolation boundary: the hosted queue must isolate the whole job and enforce host CPU, memory, disk, and network limits. See the security model.
macOS and Windows jobs reject containers, services, Docker actions, and Docker capability.
Step syntax
This section describes how workflow step keys, commands, actions, and workflow commands run on Buildkite Pipelines.
Step configuration
| Key | Status | Behavior | |||
|---|---|---|---|---|---|
| Key |
name, id
|
Status | ✅ Supported | Behavior | Use id to read outputs or target background work. IDs must be unique within a job. |
| Key | if |
Status | 🟡 Supported subset | Behavior | May use step status, step outputs, env, and service ports in addition to job-condition contexts. |
| Key | env |
Status | 🟡 Supported subset | Behavior | Values override job and workflow values and may use supported expressions, including fallbacks. |
| Key | continue-on-error |
Status | ✅ Supported | Behavior | Accepts Boolean literals or expressions that produce a Boolean. A failure records outcome: failure and conclusion: success, then the job continues. |
| Key | timeout-minutes |
Status | 🟡 Supported subset | Behavior | Accepts literal numbers or expressions that produce a number greater than 0 and at most 360. |
Tolerated step failures, including expression failures before a process starts, produce masked warnings in the job log and the workflow warning annotation. This also applies to composite child steps and JavaScript pre hooks. The warning doesn't change the step's conclusion. If a composite action's main step is skipped, its tolerated pre hook warnings appear before result publication.
A step can continue after failure and expose its outcome to a later condition:
- id: test
run: go test ./...
continue-on-error: true
- if: steps.test.outcome == 'failure'
run: ./scripts/report-failure
Commands and actions
🟡 Supported subset. Use bash, sh, pwsh, powershell, python, or a custom shell template on Linux or macOS. Custom templates use command [options] {0} [more-options]. The {0} placeholder receives a temporary script path. Arguments can use single quotes, double quotes, and backslash escapes. They don't expand shell syntax.
Install the interpreter on the runner or in the job container, and add it to PATH. PowerShell, R, and Julia aren't installed automatically. Named pwsh and powershell shells use the corresponding executable without fallback.
Named PowerShell shells run UTF-8 .ps1 temporary scripts with $ErrorActionPreference = 'stop' and propagate the last native command's exit status. PowerShell custom templates receive a .ps1 script but control their own error behavior. GITHUB_ENV, GITHUB_OUTPUT, GITHUB_STATE, and GITHUB_PATH files ignore a leading UTF-8 byte order mark (BOM). BOMs within values remain unchanged.
Windows defaults to pwsh. Linux and macOS retain their existing bash default. cmd remains unsupported. If the shell name is known before the job starts, the workflow fails before an agent starts the job. A shell expression that needs a runtime value is checked before its step starts. If the expression resolves to an unsupported shell, the step fails.
Native Windows executable paths can use drive-absolute backslashes, such as D:\cygwin\bin\bash.exe '{0}'. Quote executable paths containing spaces: "C:\Program Files\PowerShell\7\pwsh.exe" -File {0}. The runner must provide the executable at that path. A single backslash after the drive prefix selects literal separators for the executable only. Templates with an escaped first backslash keep their escape rules. Argument escaping is unchanged.
On Windows, msys2/setup-msys2@v2 can install the msys2 {0} custom shell, including through defaults.run.shell. The runtime resolves its .cmd wrapper from the step's PATH, including earlier GITHUB_PATH updates. The runtime passes an extensionless, unmodified script and the Windows working directory to the wrapper. MSYS2 supplies its login environment, error handling, and path conversion.
Windows .cmd custom shells must forward arguments directly, without CALL or delayed expansion. Arguments can contain spaces, single quotes, and metacharacters such as %, !, &, and ^. Arguments containing double quotes, line breaks, NUL, or a trailing backslash are rejected. These limits don't apply to the script contents. Native executable templates retain their existing argument handling.
Working directories must stay inside the workspace.
A shell step can specify its shell and workspace-relative working directory:
- name: Test
shell: bash
working-directory: ./src
run: go test ./...
Use an interpreter installed by an earlier step or included in the job image:
- shell: pwsh
run: '"greeting=héllo" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append'
- shell: Rscript {0}
run: print("R script")
- shell: julia --color=yes {0}
run: println("Julia script")
- shell: bash -l {0}
run: conda info
A uses step may call a supported local or public action. Action inputs under with may use supported expressions, including fallbacks. Direct workflow uses: docker://... actions are rejected. Prebuilt-image declarations belong in locked action metadata.
Local actions must exist in the event repository when the workflow is compiled. An earlier step can't create a local action with actions/checkout, an artifact download, or a command. Use a public owner/repository/path@ref action instead.
At execution, local actions require both their action tree and the local workflow bytes to match the plan. Checking out an older commit with a different workflow fails this check even if the action itself is unchanged. $/ actions don't depend on the checkout and retain their containing source commit.
Action steps can call public and local actions:
- uses: actions/checkout@v7
- uses: ./.github/actions/build
with:
target: production
Self-repository actions
🟡 Supported subset. uses: $/.github/actions/build downloads the action from the repository and exact commit containing the workflow, without actions/checkout. Inside a remote composite action, $/ selects that action's repository and commit, not the caller's. uses: $/ selects an action at the repository root. Existing ./ action paths remain checkout-relative.
Inside a remote composite selected with @v1, nested $/ actions expose v1 as github.action_ref, while their source stays pinned to the resolved commit. A $/ action called directly by a workflow or from a local composite exposes the containing workflow's commit, not its requested branch or tag. The runtime doesn't populate GITHUB_ACTION_REF. Pass github.action_ref through a step's env when a command needs it.
For local workflow input, compile, upload, and event-specific validation treat the event repository and commit as candidates, not proof of workflow identity. Before resolving $/, the compiler fetches the same workflow path at that commit and requires its bytes to match the supplied file. This also applies to self references inside local reusable workflows. A fetched remote reusable workflow supplies its own immutable identity instead. Modified local workflow bytes fail verification. No Git HEAD or workspace fallback is used. Synthetic validate --event input has no source identity and reports self-repository resolution as indeterminate. Use an exact --event-path snapshot.
Self-repository actions retain the public-action access boundary, immutable tree verification, capability planning, and nested-action limits. A privately fetched reusable workflow doesn't grant access to private actions in that tree. Self paths must be repository-relative, without traversal, backslashes, or an explicit @ref.
Background and parallel steps
✅ Supported. The background, wait, wait-all, cancel, and parallel controls are supported. At most ten background steps run at once inside a job. Use wait: <id> for selected steps, wait-all: for all active work, or parallel: for a fixed group.
Background work can be canceled by step ID:
steps:
- id: server
run: ./scripts/start-server
background: true
- run: ./scripts/test
- cancel: server
A parallel group runs a fixed set of child steps together:
steps:
- parallel:
- run: ./scripts/lint
- run: ./scripts/test
Outputs, environment changes, and failures become visible at the covering wait. Remaining work is joined before post-action cleanup. These controls aren't supported inside a composite action.
Environment files
✅ Supported. The runtime supports GITHUB_OUTPUT, GITHUB_ENV, GITHUB_PATH, GITHUB_STATE, and GITHUB_STEP_SUMMARY. Multi-line values are supported. NODE_OPTIONS can't be set through GITHUB_ENV.
Workflow commands
| Command | Status | Behavior | |||
|---|---|---|---|---|---|
| Command |
add-mask, stop-commands
|
Status | ✅ Supported | Behavior | Standard command behavior. |
| Command |
warning, error
|
Status | ✅ Supported | Behavior | Creates Buildkite build annotations. |
| Command |
group, endgroup
|
Status | ✅ Supported | Behavior | Creates linear log sections. |
| Command | Debug and matcher commands | Status | ➖ Accepted, no effect | Behavior | Consumed without presentation behavior. |
| Command |
notice, command echo control, other legacy commands |
Status | ❌ Unsupported | Behavior | Not implemented. |
The total job summary is limited to 1 MiB.
Expressions and contexts
Three expression modes intentionally support different syntax.
| Syntax | Conditions | Runtime interpolation | Other compile-time expressions | ||||
|---|---|---|---|---|---|---|---|
| Syntax |
!, &&, `\ |
Conditions | \ | Runtime interpolation |
,==,!=,<,<=,>,>=` |
Other compile-time expressions | ✅ Supported |
| Syntax |
always(), success(), failure(), cancelled()
|
Conditions | ✅ Without arguments | Runtime interpolation | ❌ Unsupported | Other compile-time expressions | ❌ Unsupported |
| Syntax |
startsWith(), contains(), endsWith(), format(), join(), toJSON(), fromJSON(), case()
|
Conditions | ✅ Supported | Runtime interpolation | 🟡 Listed workflow fields | Other compile-time expressions | 🟡 When the result resolves fully |
| Syntax | hashFiles() |
Conditions | 🟡 Workflow and composite step if and JavaScript lifecycle conditions |
Runtime interpolation | 🟡 Workflow and composite step fields | Other compile-time expressions | ❌ Unsupported |
Conditions
Job and step if conditions use GitHub-style:
- Truthiness and loose numeric coercion
- Case-insensitive string comparison
- Operand-returning
&&and|| - Primitive conversion for string functions
- Array search with
contains() - Lazy
case()evaluation with 3–255 odd-numbered arguments and Boolean predicates
A missing property in an available github or matrix context evaluates to null. An unavailable context is an error. Unlisted functions are unsupported. hashFiles() accepts 1–255 literal or direct-reference arguments in step and JavaScript action lifecycle conditions.
Conditions support computed object indexes, numeric array indexes, whole matrix and needs objects, step-scoped steps, and .* projections.
- Missing and out-of-range indexes evaluate to null.
- Projections omit missing children.
- A later wildcard flattens one collection level.
- The equivalent
[*]spelling is unsupported by the parser. - Whole or dynamic
github, except event-rooted access, and wholeinputsandstrategyremain unsupported.
| Context | Job if
|
Step if
|
|||
|---|---|---|---|---|---|
| Context |
github.actor, github.base_ref, github.event_name, github.head_ref, github.ref, github.ref_name, github.ref_type, github.repository, github.repository_owner, github.sha, github.workflow_ref, github.workflow_sha
|
Job if
|
✅ Yes | Step if
|
✅ Yes |
| Context |
runner.os, runner.arch, runner.environment
|
Job if
|
✅ Yes | Step if
|
✅ Yes |
| Context | runner.temp |
Job if
|
❌ No | Step if
|
✅ Yes |
| Context | runner.debug |
Job if
|
❌ No | Step if
|
✅ Yes, always absent |
| Context |
needs.<job>.result, needs.<job>.outputs.<name>
|
Job if
|
✅ Yes | Step if
|
✅ Yes |
| Context | matrix.<name> |
Job if
|
✅ Yes | Step if
|
✅ Yes |
| Context | Scalar strategy properties |
Job if
|
❌ No | Step if
|
✅ Workflow steps only |
| Context | vars.<name> |
Job if
|
✅ Yes, repository and organization variables | Step if
|
✅ Yes, environment over repository over organization variables |
| Context |
inputs.<name> and computed input indexes |
Job if
|
✅ Yes | Step if
|
✅ Yes |
| Context |
steps.<id>.outcome, steps.<id>.conclusion, steps.<id>.outputs.<name>
|
Job if
|
❌ No | Step if
|
✅ Yes |
| Context | env.<name> |
Job if
|
❌ No | Step if
|
✅ Yes |
| Context |
job.services IDs, networks, and ports, including bracket-indexed service IDs |
Job if
|
❌ No | Step if
|
✅ Yes |
| Context |
github.event, including direct, projected, and dynamically indexed properties |
Job if
|
✅ Yes | Step if
|
✅ Yes |
| Context |
secrets and other contexts |
Job if
|
❌ No | Step if
|
❌ No |
Before runtime validation, the compiler reduces event-backed conditions from the immutable snapshot. Resolvable github.event expressions become literals. For whole or runtime-selected event access, the plan retains a marker and digest for the build's shared event payload artifact.
Every branch is validated first, so short-circuiting can't hide an unsupported function, context, or matrix type.
Reusable-workflow call conditions use the same operators and status functions but only the caller contexts listed in Reusable workflows. The runtime evaluates their ordered guards before the called job's own condition.
Runtime interpolation
The || operator selects its right operand when the left is falsy: false, zero, an empty string, or null. The operator doesn't recover from evaluation or secret-retrieval errors, grant access to an unavailable context, or make a literal-only field accept expressions. Each field retains its context and result-type restrictions.
These step fields support the operators and pure functions listed above:
-
run,env,with, andname - Explicit
shellandworking-directory -
continue-on-errorandtimeout-minutes
These fields support computed indexes and projections over available matrix, inputs, env, vars, and runner values. toJSON(needs) serializes only direct dependencies, with each job's result and outputs object (empty when there are no outputs). Transitive dependencies aren't included. Computed and projected needs access and computed, whole, and projected steps access remain unsupported. Reading an unavailable background output is an error.
Before creating a job plan, the compiler resolves scalar github.event.* values and event-dependent parts of otherwise runtime expressions.
Missing event members become null. Template interpolation renders null as an empty string. Event values can't introduce new ${{ ... }} regions. A job that still needs whole, projected, or dynamically indexed github.event access loads the digest-verified event artifact uploaded by the exact importer job. This preserves the original event for runtime use and retries without duplicating it across immutable plans. Jobs with an event file also load this artifact, even without event expressions.
Job-level expressions support the same operators and pure functions with these field-specific contexts:
| Field | Contexts | ||
|---|---|---|---|
| Field | continue-on-error |
Contexts |
github, needs, strategy, matrix, vars, inputs
|
| Field | env |
Contexts |
github, needs, strategy, matrix, vars, secrets, inputs
|
| Field | defaults.run |
Contexts |
github, needs, strategy, matrix, env, vars, inputs
|
| Field | outputs |
Contexts |
github, needs, strategy, matrix, job, runner, env, vars, secrets, steps, inputs
|
Workflow-level env values use the job env expression rules except for strategy. Fallbacks such as ${{ github.head_ref || github.ref_name }} are supported. Workflow-level defaults.run remains limited to direct context references.
Workflow step fields and the composite step run, env, with, shell, and working-directory fields support hashFiles() and the listed operators and pure functions. Job-level fields, action input defaults, and composite output metadata can't call hashFiles(). A composite action can expose a hash through a step output instead.
The listed workflow step fields and job fields accept the four scalar strategy properties, including static bracket access such as ${{ strategy['job-index'] }}. These fields can combine strategy with runtime values, for example ${{ format('{0}-{1}', strategy.job-index, steps.build.outputs.version) }}. Whole, projected, and computed strategy access remains unsupported. This doesn't add strategy to job if, workflow-level fields, reusable-call with, or action-authored metadata, including composite steps.
Admission follows the GitHub context table, checked against the runner schema and language-services schema. The GitHub documentation and language-services schema disagree on caller with admission. That field is unchanged.
Job outputs support service IDs, networks, and published ports through job.services, such as ${{ job.services.redis.ports[6379] }}. Other job fields remain unsupported. The listed job-level fields also support toJSON(needs), with the same direct-dependency scope and access restrictions as step fields.
Expression-valued continue-on-error must produce a Boolean. Expression-valued timeout-minutes must produce a number greater than 0 and at most 360.
Direct github.token references are step-only. Step runtime fields also support the exact, case-insensitive call toJSON(github). This call serializes the retained context listed below, including token, with sorted keys and two-space indentation.
The compiler treats that call as a token reference, so normal permissions, admission, and redaction apply. Composite steps can consume an already authorized context, but composite metadata can't grant token authority. A tokenless context is an error.
Job-level fields and action input defaults can't call toJSON(github). Bare, projected, or dynamically indexed github, and passing the whole context to another function, remain unsupported. These limits don't apply to access rooted at github.event.
runner.os and runner.arch resolve to Linux/X64, Linux/ARM64, macOS/ARM64, or Windows/X64. runner.environment resolves to self-hosted. GitHub assigns this value to runners registered outside GitHub, including managed providers. Buildkite agents are in the same class, whether they're Buildkite hosted agents or run on your own infrastructure.
After runner setup, step runtime fields and job outputs can also use runner.temp, which resolves to the canonical temporary directory exposed as RUNNER_TEMP. Those fields, action lifecycle conditions, and action metadata input defaults can use runner.debug. Buildkite Pipelines has no equivalent step-debug mode, so the property is absent: direct interpolation is empty and comparison with the GitHub enabled value 1 is false. Other runner fields and compile-time positions that require runner identity are unsupported. job.check_run_id defaults, including the static indexed spelling, resolve to an empty string because Pipelines doesn't create a GitHub check run. Other job identity fields remain unsupported.
A runtime interpolation can read a verified upstream output directly:
run: echo "${{ needs.build.outputs.image }}"
The runtime retains this bounded github context:
| Fields | Behavior | ||
|---|---|---|---|
| Fields |
actor, event_name, ref, repository, sha
|
Behavior | Event identity from the compiled plan. |
| Fields | repository_owner |
Behavior | Derived from repository. |
| Fields | server_url |
Behavior | Identifies the event repository provider. |
| Fields | job |
Behavior | Workflow job ID. |
| Fields | workflow |
Behavior | Workflow name, or its path when unnamed. |
| Fields |
head_ref, base_ref
|
Behavior | Pull request source and target branches. Empty for other events. |
| Fields | ref_name |
Behavior | Ref without refs/heads/, refs/tags/, or refs/pull/. Pull request refs use <number>/merge or <number>/head. |
| Fields | ref_type |
Behavior |
branch for branch and pull request refs. tag for tag refs. SHA-only deployment and deployment_status events also use branch, while ref and ref_name remain empty. |
| Fields | action_path |
Behavior | Composite action directory inside composite steps. Empty elsewhere. |
| Fields |
action_repository, action_ref
|
Behavior | Remote composite repository and requested ref. Empty for local composites and outside composite steps. |
| Fields | workspace |
Behavior | The workspace directory: the fixed job-container mount for container jobs, the host checkout directory otherwise. Exposed as GITHUB_WORKSPACE. |
| Fields |
run_id, run_number, run_attempt
|
Behavior | Buildkite Pipelines build identity: the build ID, the build number, and the retry count plus one. Exposed as GITHUB_RUN_ID, GITHUB_RUN_NUMBER, and GITHUB_RUN_ATTEMPT. Referencing them outside a Buildkite build is an error, and GitHub run URLs or API calls built from them don't resolve because no GitHub Actions run exists. |
| Fields | token |
Behavior | Available only in an authorized step expression. |
| Fields | event |
Behavior | The immutable, digest-verified event payload, loaded for runtime event expressions or an event file. |
This isn't the full GitHub context.
hashFiles() evaluates when its step field is consumed, so it sees files from earlier steps such as checkout. The with and env of a JavaScript action can be evaluated for pre, then evaluated again for main, including inside a composite action. Composite patterns use the job workspace, not the action directory or the step's working-directory. The same hashing limits and filesystem boundaries apply to workflow and composite steps.
Patterns apply in order. ! excludes matches. A later positive pattern can add them back. Directory matches include descendants, hidden files match normally, and overlapping patterns hash each path once. Matching is case-insensitive only on Windows. An empty match returns an empty string, not an error or warning, matching GitHub behavior. For example, a key such as npm-${{ hashFiles('package-lock.json') }} becomes npm- if the lockfile is missing. When the file is required, reject an empty digest before using the cache:
# Run after checkout. These steps also work inside a composite action.
- id: lockfile
shell: sh
env:
HASH: ${{ hashFiles('package-lock.json') }}
run: |
if [ -z "$HASH" ]; then
echo "package-lock.json was not found; check checkout order and the hashFiles pattern" >&2
exit 1
fi
printf 'digest=%s\n' "$HASH" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v5
with:
path: ~/.npm
key: npm-${{ runner.os }}-${{ steps.lockfile.outputs.digest }}
Hashing failures reach the job log with step and field context. Composite errors include the child step number, and env and with errors also name the binding. Direct calls in composite output metadata explain the restriction and suggest exposing a step output. Step or job cancellation and deadline messages identify hashing as the interrupted operation.
On Linux, literal paths use direct lookups. macOS and Windows enumerate directory names to preserve platform-specific matching. Each positive pattern searches below its literal directory prefix, then walks recursively from its first wildcard. For example, packages/service/*.go searches under packages/service, while packages/s*/value searches under packages. Negative patterns filter matches but don't prune traversal, since later patterns can re-include files. The entry limit counts inspected entries, including non-matching entries, rather than the size of the workspace.
Each call has an execution budget covering traversal, matching, sorting, hashing, and verification. An earlier step or job deadline still applies. Cancellation is checked between operations, and can't interrupt a blocked filesystem call. Entry-limit and execution-budget errors list the positive patterns being searched and recommend more specific paths to reduce traversal and hashing. The budget is shared across those patterns.
For each file, hashFiles() calculates SHA-256 over its contents. The function then hashes the concatenated binary digests in lexical path order. GitHub Runner doesn't specify glob traversal order, so a multi-file digest can differ when the GitHub order isn't lexical.
Patterns can't be absolute or contain .. segments or ASCII control characters. Hashing stays inside the workspace and doesn't enter symlinked directories. A matched symlink or other non-regular file fails the step.
GitHub Runner can hash a file symlink and has an optional symlink-following mode. This runtime deliberately does neither.
Event file
When upload receives a linked webhook or an explicit --event-path snapshot, GITHUB_EVENT_PATH points to a job-scoped JSON file containing its complete payload object, not the snapshot wrapper. JSON formatting may differ from the original. The reduced fallback synthesized from Buildkite environment variables doesn't create this file.
The file is available to shell steps, JavaScript pre, main, and post hooks, nested composites, and Docker actions. Job containers and Docker actions receive a read-only mount with a container-local path. The file lives outside the checkout and writable runner temp, survives through post hooks, and is removed at job teardown. Job and step environment overrides can't replace the runtime path. github.event_path expressions aren't supported.
- run: jq -e '.issue.number == 42' "$GITHUB_EVENT_PATH" >/dev/null
Event retention follows the event payload security boundary.
Compile-time expressions
Matrices, runner labels, names, concurrency groups, retained runtime templates, and event-backed conditions can use statically known github, event, and matrix values.
Compile-time github fields are actor, base_ref, event_name, head_ref, ref, ref_name, ref_type, repository, repository_owner, sha, and workflow. Expressions can use computed indexes, numeric array indexes, and .* projections when the complete result resolves during compilation.
Whole or dynamic github access remains unsupported unless it's rooted at github.event. Event-backed runtime expressions can combine reducible event parts with values supported by their runtime surface. Action references in uses must remain static and can't use github.event.
Actions
This section describes which action sources and runtimes are supported, and how common GitHub-authored actions run on Buildkite Pipelines.
Action sources and runtimes
| Action type | Status | Boundary | |||
|---|---|---|---|---|---|
| Action type | Local ./... action |
Status | 🟡 Supported subset | Boundary | Source tree is digest-locked and verified again. |
| Action type | Public owner/repo[/path]@ref action |
Status | 🟡 Supported subset | Boundary | Resolved to an exact commit and digest. |
| Action type | Private action | Status | ❌ Unsupported | Boundary | No private action source access. |
| Action type | JavaScript action | Status | ✅ Supported | Boundary | Declares node16, node20, or node24. |
| Action type | Composite action | Status | 🟡 Supported subset | Boundary | Nested shell steps and locked local or public actions. Supported shells, including expression-backed custom templates, for run. Literal continue-on-error. |
| Action type | Docker action | Status | 🟡 Supported subset | Boundary | Verified local or public Dockerfile or prebuilt-image action on Linux with optional bounded runs.args. Rejected on macOS and Windows, including through a composite action. |
| Action type | Direct workflow uses: docker://... action |
Status | ❌ Unsupported | Boundary | Rejected during validation. |
| Action type | Top-level action metadata env
|
Status | ➖ Accepted, no effect | Boundary | Any valid YAML value is discarded. The value isn't evaluated, injected, retained in plans, or used to request secrets or tokens. |
Mutable public refs are resolved during upload, then locked to a commit. The importer lazily requests one Buildkite action-source token and reuses it across all workflow roots and nested composite actions. This token authenticates only public metadata requests for repositories other than the credential repository. The credential repository and codeload requests remain anonymous. If token issuance is unavailable during rollout, resolution safely falls back to anonymous GitHub API access. Exact lowercase commit SHAs need no GitHub API lookup. Complete source trees are verified again at runtime.
Authenticated resolution shares successful repository visibility checks within each compilation. Later compilations recheck visibility when resolving uncached refs. When GitHub rate-limits requests, the resolver suppresses new API requests against the affected authenticated or anonymous budget until the retry deadline, or for one minute if GitHub supplies none. Already running requests may finish. Cached refs and exact commit pins remain usable. Rate-limit suppression stays local to that resolver and isn't written to source caches.
Nested calls from a repository-local composite must be local. Public composites may call local children or other public actions. Every child is resolved and locked.
Prebuilt-image actions declare docker:// in action metadata, not in a workflow step:
name: Check formatting
inputs:
path:
default: .
runs:
using: docker
image: docker://ghcr.io/example/formatter@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
entrypoint: /usr/local/bin/formatter
args:
- ${{ inputs.path }}
steps:
- uses: example/formatter-action@v2
with:
path: .
The compiler locks the action source that declares the image. At job start, the runtime pulls each declared image anonymously through an empty private Docker configuration. Action metadata has no registry-credential field, so private images and ambient Docker credentials are unsupported. Digest references are supported. Mutable tags resolve when the job starts and can drift between jobs. Use a digest when image immutability matters.
Direct workflow syntax such as uses: docker://alpine:3.20 remains unsupported. This syntax doesn't have the locked action-source provenance used by metadata-declared images.
Dockerfile actions require exact runs.image: Dockerfile. Optional runs.args must be an ordered YAML string array. Each item becomes one argument after the image name, without a shell. Empty strings, whitespace, and shell metacharacters remain literal. Omitted or empty args keep the image CMD. Any non-empty array replaces CMD while preserving the image ENTRYPOINT. The optional runs.entrypoint of a prebuilt-image action overrides the image ENTRYPOINT.
Items in args may contain literals and expressions using action inputs, operators, and the supported pure functions, such as ${{ inputs.name || 'default' }}. Other contexts, credentials, status functions, and hashFiles() are rejected, including in unreachable branches. Invocation inputs and metadata defaults resolve before args evaluation. Substituted values remain literal arguments. The compiler stores args as action-authored sites in the normalized job program. The runtime verifies the locked action tree but doesn't parse its metadata again.
Dockerfile actions can't declare explicit entrypoints. Docker actions can't declare pre or post lifecycle or request credentials, volumes, arbitrary options, or privileged mode. An argument such as --privileged remains a container argument. It can't become a Docker option.
Action metadata parsing remains strict for every other unknown top-level field and for unknown nested fields. The inert top-level env exception doesn't replace workflow or action-step environments, populate runs.env, or add GITHUB_TOKEN authority.
| Action declaration | Runtime | ||
|---|---|---|---|
| Action declaration | node16 |
Runtime | Managed Node 16.20.2, with one end-of-job deprecation warning. |
| Action declaration | node20 |
Runtime | Managed Node 24.18.0. |
| Action declaration | node24 |
Runtime | Managed Node 24.18.0. |
The pre, main, and post phases, inputs, outputs, state, and LIFO post ordering are supported. Other Node declarations are rejected.
JavaScript action pre-if and post-if metadata uses the condition operators, status functions, pure functions, and hashFiles() described in Conditions. Lifecycle conditions can read direct properties from workflow inputs, env, github, job.services, matrix, runner, and steps, and direct or dynamic github.event properties. Other contexts and dynamic or whole-context access return an error. An empty lifecycle condition always runs and doesn't receive an implicit success() guard.
The pre-if conditions use the status and action-scoped environment available when preparation reaches the action. The post-if conditions run during job teardown and use the final job status and environment, including GITHUB_ENV changes from main. Root action posts also see final workflow step state. Nested composite actions retain their isolated step context. Cancellation remains distinct from failure, and posts keep LIFO order.
Repository archive symlinks
Repository archive extraction omits relative symlinks whose targets are extracted regular files or directories inside the repository. Extraction never creates or follows the aliases. Actions that need those alias paths remain unsupported. This allows unused repository fixtures and tooling aliases, not general symlink compatibility. Source digests describe the extracted tree without aliases.
Dangling links, link chains, absolute or escaping targets, hardlinks, special files, and duplicate or case-colliding paths remain rejected. An alias can't be an ancestor of another archive entry, in either archive order. Archive size, entry-count, and path limits apply even when aliases are omitted.
For example, the TruffleHog archive at e283608 contains .claude/skills/dep-updates and .codex/skills/dep-updates, both pointing to ../../.cursor/skills/dep-updates. That directory and its SKILL.md are present, and the action uses neither alias.
On Linux, GitHub Runner 2.337.0 extracts remote actions with tar -xzf, then recursively copies directories, following directory aliases into real copied directories. Buildkite Pipelines deliberately omits the alias paths instead. This preserves the files an action reads, not the full tree semantics of the GitHub runner. On Windows, GitHub Runner uses ZIP extraction, which this comparison doesn't cover.
Checkout action
🟡 Supported subset. Immutable commits captured from frozen upstream tags, main, master, and releases/v1 through releases/v6 snapshots are admitted. The snapshot includes historical development and release commits across v1 through v7. These known releases identify the principal contracts:
| Release | Commit | ||
|---|---|---|---|
| Release | v1.2.0 | Commit | 50fbc622fc4ef5163becd7fab6573eac35f8462e |
| Release | v2.8.0 | Commit | 0717577d45739eb3c851188b29f50ed6c0b2194e |
| Release | v3.7.0 | Commit | a37ce9120846195fa4ece8f58b268e6043cb2f26 |
| Release | v4 | Commit | 11d5960a326750d5838078e36cf38b85af677262 |
| Release | v5 | Commit | fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 |
| Release | v6 | Commit | d23441a48e516b6c34aea4fa41551a30e30af803 |
| Release | v7.0.0 corpus pin | Commit | 9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 |
| Release | v7.0.1 | Commit | 3d3c42e5aac5ba805825da76410c181273ba90b1 |
Every resolved immutable commit uses the native adapter. The upstream JavaScript doesn't run. Commits in the frozen snapshots retain their exact inputs, full-history default, and outputs. For example, early v2 commits reject later v2 inputs, and v4.0 and v4.1 commits don't expose the ref and commit outputs.
An immutable commit absent from the snapshots uses the stable v7.0.1 contract as a compatibility fallback. Compilation emits one W_CHECKOUT_UNKNOWN_COMMIT_FALLBACK warning for each distinct unknown commit. This higher-risk fallback can differ from the upstream manifest of the commit, but it doesn't widen the native adapter: repository, ref, path, credentials, and every other input still use the restrictions below. Known snapshotted commits never use the fallback. Compilation emits W_CHECKOUT_LEGACY_RELEASE for v1.2.0 and v2.8.0 to nudge an upgrade to v4 or later.
The frozen refs and per-commit profiles are refreshed when buildkite-gha is updated. Only commits reachable from the selected upstream tags and branches at that time receive exact profiles. Other valid resolved SHAs continue to use the fallback.
Buildkite Pipelines runs v1.2.0 like v1 and v2.8.0 like v2, and warns about their differences from v4 and later. Neither release sets the ref or commit outputs added in v4.2.0. v1.2.0 also fetches full history by default when fetch-depth is omitted. Upgrade only if your workflow needs those outputs or different v1 history behavior. Otherwise, keep the current version.
The adapter checks out a detached commit or static branch from the event repository at the workspace root or a clean nested directory. The adapter uses Buildkite repository-provider Git credentials when the job provides them. Otherwise, the adapter fetches anonymously. Credentials are scoped to the Git commands that fetch repository, LFS, or submodule data and are never persisted.
Checkout input restrictions don't block a whole job whose if condition is statically false after reusable inputs and matrix values resolve. The skipped job and locked action remain in the plan so the job can publish a skipped result. Step-level and runtime-dependent conditions remain conservative and validate the adapter inputs.
An explicit input is accepted only when the exact snapshotted contract declares it, or when the v7.0.1 fallback contract declares it for an unknown commit. The following value restrictions then apply:
| Input | Supported values | ||
|---|---|---|---|
| Input | repository |
Supported values | Omitted, or the event owner/repo. |
| Input | ref |
Supported values | Omitted, empty, a lowercase 40-hex commit, or a static branch in the event repository. A direct github.sha or needs.<job>.outputs.<name> expression must resolve at runtime to the exact event SHA. |
| Input | token |
Supported values | Omitted only. |
| Input |
ssh-key, ssh-known-hosts
|
Supported values | When declared by the commit: omitted or empty. Otherwise omitted. |
| Input | ssh-strict |
Supported values | When declared by the commit: omitted or true. Otherwise omitted. |
| Input | ssh-user |
Supported values | When declared by the commit: omitted or git. Otherwise omitted. |
| Input | persist-credentials |
Supported values | When declared by the commit: omitted or false. Otherwise omitted. |
| Input | path |
Supported values | Omitted, empty, or a clean relative directory without a .git path segment. The resolved path stays inside the workspace and can't traverse symbolic-link parents. |
| Input | clean |
Supported values | Omitted, true, or false. The root workspace must be empty, or the selected path must be absent. Existing-directory reuse is unsupported, so false differs only by matching workflows that select a fresh target. |
| Input | filter |
Supported values | When declared by the commit: omitted, empty, or one Git partial-clone filter without control characters. Otherwise omitted. |
| Input | sparse-checkout |
Supported values | When declared by the commit: omitted, empty, or up to 1,000 non-empty patterns totaling at most 1 MiB. Otherwise omitted. |
| Input | sparse-checkout-cone-mode |
Supported values | When declared by the commit: omitted, true, or false. Otherwise omitted. |
| Input | fetch-depth |
Supported values | Omitted or a nonnegative integer. 0 fetches full history. Historical runner-plugin commits fetch full history when omitted. |
| Input | fetch-tags |
Supported values | When declared by the commit: omitted, true, or false. Otherwise omitted. |
| Input | show-progress |
Supported values | When declared by the commit: omitted, true, or false. Otherwise omitted. |
| Input | lfs |
Supported values | Omitted, true, or false. true requires Git LFS in the job image. |
| Input | submodules |
Supported values | Omitted, false, true, or recursive. Whitespace is trimmed and casing is ignored. |
| Input | set-safe-directory |
Supported values | When declared by the commit: omitted or true. Otherwise omitted. |
| Input | github-server-url |
Supported values | When declared by the commit: omitted, empty, or https://github.com. Otherwise omitted. |
| Input | allow-unsafe-pr-checkout |
Supported values | When declared by the commit: omitted or false. Otherwise omitted. |
The ref and commit outputs are available when the selected exact or fallback contract declares them. Exact contracts follow the action manifest of each commit. The v7.0.1 fallback exposes both outputs. Upstream added both outputs in v4.2.0.
The false value and omission don't run submodule commands. The true value runs native Git for direct children, and recursive includes nested children. Relative URLs and fetch-depth follow native Git behavior. Public and private GitHub submodules are supported under the repository access of the job. External HTTPS submodules are anonymous. git@github.com: URLs are rewritten to HTTPS. Other SSH and non-HTTPS transports are unsupported.
Sparse checkout applies blob:none automatically unless filter is explicit. Cone mode treats each line as a directory. Non-cone mode uses Git ignore-style patterns. LFS configures repository-local filters before fetch without installing push or locking hooks. A regular checkout fetches the LFS objects of the selected revision before checkout. Sparse checkout lets Git LFS download only materialized paths.
- uses: actions/checkout@v7
with:
path: sources/application
filter: blob:limit=1m
fetch-depth: 20
show-progress: false
- uses: actions/checkout@v7
with:
path: sources/docs
sparse-checkout: |
docs
schemas
- uses: actions/checkout@v7
with:
lfs: true
sparse-checkout: |
/*.md
/assets/
!/assets/archive/
sparse-checkout-cone-mode: false
See the security model for credential, Git, and job-isolation boundaries.
Alternate repositories, tags, non-event dynamic commits, GitHub Enterprise Server, credential persistence, and existing-directory reuse remain unsupported. Commit and branch checkouts remain detached and confined to the event repository.
Upload artifact action
🟡 Supported subset. Resolved commits in the frozen upstream release and main snapshots use a native Buildkite ZIP adapter. These principal releases remain named compatibility points:
| Release | Commit | ||
|---|---|---|---|
| Release | v1.0.0 | Commit | 3446296876d12d4e3a0f3145a3c87e67bf0a16b5 |
| Release | v2.3.1 | Commit | 82c141cc518b40d92cc801eee768e7aafc9c2fa2 |
| Release | v3.2.1 | Commit | ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5 |
| Release | v4.6.0 | Commit | 65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 |
| Release | v4.6.2 | Commit | ea165f8d65b6e75b540449e92b4886f43607fa02 |
| Release | v5.0.0 | Commit | 330a01c490aca151604b8cf639adc76d48f6c5d4 |
| Release | v6.0.0 | Commit | b7c566a772e6b6bfb58ed0dc250532a479d7789f |
| Release | v7.0.1 | Commit | 043fb46d1a93c77aae656e7c1c64a875d1fc6a0a |
Each snapshotted commit retains the inputs, outputs, hidden-file default, and v1 path behavior declared by its upstream contract. For example, v4.0.0 accepts compression-level but rejects the later overwrite and include-hidden-files inputs, and exposes artifact-id without the later artifact-digest. The v3.2.2 and v3.2.2-node20 commits remain unsupported because upstream publishes them only as GitHub Enterprise Server security backports and deprecates them on github.com.
An immutable commit absent from the snapshot uses the stable v7.0.1 contract as a compatibility fallback. Compilation emits one W_UPLOAD_ARTIFACT_UNKNOWN_COMMIT_FALLBACK warning for each distinct unknown commit. The fallback can differ from the upstream manifest of the commit, but it doesn't widen the native adapter or execute upstream JavaScript. Malformed commits remain unsupported. Compilation emits W_UPLOAD_ARTIFACT_LEGACY_RELEASE for the principal v1 through v3 releases to recommend v4 or later.
The frozen tags, branches, and per-commit profiles are refreshed when buildkite-gha is updated. Only manifests whose inputs and outputs fit the bounded adapter are recorded. Other valid resolved SHAs continue to use the fallback.
| Input | Supported values | ||
|---|---|---|---|
| Input | name |
Supported values | Required by v1 runner-plugin contracts. Later contracts default to artifact. |
| Input | path |
Supported values | Required. v1 runner-plugin contracts accept one literal file or directory. Later contracts accept literal paths or bounded *, ?, character-class, and ** file globs. |
| Input | if-no-files-found |
Supported values | When declared: warn, error, or ignore. The v1 runner-plugin contract fails when its literal path is missing and uploads an empty existing directory. |
| Input | retention-days |
Supported values | When declared: nonnegative integer. Advisory only. |
| Input | compression-level |
Supported values | When declared: 0 through 9. |
| Input | overwrite |
Supported values | When declared: omitted or false. |
| Input | include-hidden-files |
Supported values | When declared: GitHub Actions boolean, default false. Earlier contracts without this input retain hidden paths. |
| Input | archive |
Supported values | When declared: omitted or true. |
Native jobs accept workspace-relative or absolute paths on the execution platform, including Windows drive paths such as C:\build\dist\*.zip. An absolute glob such as /tmp/baipp/dist/* archives paths relative to its literal search root (package.whl, not tmp/baipp/dist/package.whl). Multiple selections use their least common ancestor as the archive root.
Job-container uploads require workspace-relative paths. The native adapter rejects absolute paths at runtime rather than interpreting a container path in the host filesystem. This restriction doesn't apply to native jobs that only use service containers.
Unsupported path forms include exclusions, symlinks, traversal, UNC and drive-relative paths, alternate data streams, braces, extglobs, leading glob comments, and special files. Selections on different Windows volumes can't share an archive root. At most 32 path roots may be selected. Contracts that declare include-hidden-files exclude hidden path segments unless explicitly enabled.
An artifact may contain at most 10,000 files. buildkite-gha doesn't impose a source or ZIP byte limit. The Buildkite agent and configured artifact storage enforce their limits. A job may publish 64 artifacts.
Downloads verify the recorded archive size and digest before staging every member. File-count, path, format, and filesystem limits protect extraction. There is no separate fixed expansion-byte policy.
The adapter sets artifact-id and artifact-digest only when the snapshotted or fallback contract declares them. artifact-url remains empty because no GitHub run-scoped URL exists. Merge, raw upload, overwrite, and effective retention control are unsupported.
Download artifact action
🟡 Supported subset. These root actions/download-artifact actions use the same producer-bound ZIP mode:
| Release | Commit | ||
|---|---|---|---|
| Release | v4.3.0 | Commit | d3f86a106a0bac45b974a628896c90dbdf5c8093 |
| Release | v5.0.0 | Commit | 634f93cb2916e3fdff6788551b99b062d0335ce0 |
| Release | v6.0.0 | Commit | 018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 |
| Release | v7.0.0 | Commit | 37930b1c2abaa49bbe596cd826c3c89aef350131 |
| Release | v8.0.0 | Commit | 70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 |
| Release | v8.0.1 | Commit | 3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c |
An unknown lowercase 40-hex immutable commit uses the stable v8.0.1 contract as a compatibility fallback. Compilation emits one W_DOWNLOAD_ARTIFACT_UNKNOWN_COMMIT_FALLBACK warning for each distinct unknown commit. The fallback can differ from the upstream manifest of the commit, but it doesn't widen the native adapter or execute upstream JavaScript. Malformed commits remain unsupported.
| Input | Supported values | ||
|---|---|---|---|
| Input | name |
Supported values | Exact name, mutually exclusive with pattern. Runtime expressions are allowed. |
| Input | pattern |
Supported values | Bounded artifact-name glob. Requires merge-multiple: true. Runtime expressions are allowed. |
| Input | path |
Supported values | Optional literal workspace-relative path. |
| Input | merge-multiple |
Supported values | Omitted or false with name. Required true with pattern. |
| Input | v8 skip-decompress
|
Supported values | Omitted or false. |
| Input | v8 digest-mismatch
|
Supported values | Omitted or error. |
Artifacts must come from verified direct needs producers. Exact-name lookup must find one unique artifact. A bounded pattern may select and deterministically merge up to 64 distinct names. The shipped pattern contract accepts *, ?, character classes, and **. Brace alternation remains unsupported.
When artifacts contain the same exact member path, the later artifact by name wins. All matched archives are validated and staged before the destination changes. Artifact ID, all-artifact, cross-run, cross-repository, raw, REST, and non-merged pattern modes are unsupported.
Only ZIP archives produced by the supported upload adapter are accepted. Digest or ZIP validation failure is fatal. The download-path output is supported.
Cache action
Workflow-level and job-level cache-mode accept exactly read, write, write-only, and none. Jobs inherit the workflow mode unless they override it. For example, workflow cache-mode: write with job cache-mode: read selects read-only client behavior for that job. Modes on reusable-workflow calls or in workflows declaring workflow_call aren't supported.
The compiled plan carries the effective mode. JavaScript action pre, main, and post phases receive it as ACTIONS_CACHE_MODE, overriding workflow or action environment values. Omitting both keys preserves existing behavior. The runtime doesn't choose a trigger-based default.
| Mode | Client restore | Client save | |||
|---|---|---|---|---|---|
| Mode | read |
Client restore | Allowed | Client save | Skipped |
| Mode | write |
Client restore | Allowed | Client save | Allowed |
| Mode | write-only |
Client restore | Skipped | Client save | Allowed |
| Mode | none |
Client restore | Skipped | Client save | Skipped |
Cache mode isn't a security boundary
Cache mode is best-effort client behavior. It doesn't change cache-token issuance or widen the existing provenance-based authorization of the server. Direct service requests remain subject to that authorization, not the requested mode. Clients that ignore ACTIONS_CACHE_MODE don't apply these additional restrictions.
The audited upstream commit 3edfce9 bundles @actions/cache 6.2.0, which logs and skips disallowed operations. The v6.1.0 release listed below and older bundled clients ignore the variable. A skipped restore is a cache miss, so fail-on-cache-miss: true can still fail the action. Docker actions don't receive the compiled mode.
🟡 Supported subset. Immutable commits captured from frozen upstream tags and the main and releases/v5 branches are admitted when their root, restore, and save bundles all speak the cache-v2 protocol the Buildkite Results service implements. The snapshot covers historical development and release commits from v3.4.0 and v4.2.0 onward, including untagged main commits. The admitted release commits run their stock cache-v2 clients. These principal releases are named in diagnostics:
| Release | Commit | Node | @actions/cache |
||||
|---|---|---|---|---|---|---|---|
| Release | v3.4.0 | Commit | f4b3439a656ba812b8cb417d2d49f9c810103092 |
Node | 16 | @actions/cache |
4.0.0 |
| Release | v3.4.2 | Commit | 387e18722e6ff315b24a3b8b071feddd27b7bf7e |
Node | 16 | @actions/cache |
4.0.1 |
| Release | v3.4.3 | Commit | 2f8e54208210a422b2efd51efaa6bd6d7ca8920f |
Node | 16 | @actions/cache |
4.0.2 |
| Release | v3.5.0 | Commit | 6f8efc29b200d32929f49075959781ed54ec270c |
Node | 16 | @actions/cache |
4.1.0 |
| Release | v4.2.0 | Commit | 1bd1e32a3bdc45362d1e726936510720a7c30a57 |
Node | 20 | @actions/cache |
4.0.0 |
| Release | v4.2.1 | Commit | 0c907a75c2c80ebcb7f088228285e798b750cf8f |
Node | 20 | @actions/cache |
4.0.1 |
| Release | v4.2.2 | Commit | d4323d4df104b026a6aa633fdb11d772146be0bf |
Node | 20 | @actions/cache |
4.0.2 |
| Release | v4.2.3 | Commit | 5a3ec84eff668545956fd18022155c47e93e2684 |
Node | 20 | @actions/cache |
4.0.3 |
| Release | v4.2.4 | Commit | 0400d5f644dc74513175e3cd8d07132dd4860809 |
Node | 20 | @actions/cache |
4.0.5 |
| Release | v4.3.0 | Commit | 0057852bfaa89a56745cba8c7296529d2fc39830 |
Node | 20 | @actions/cache |
4.1.0 |
| Release | v5.0.0 | Commit | a7833574556fa59680c1b7cb190c1735db73ebf0 |
Node | 24 | @actions/cache |
5.0.0 |
| Release | v5.0.1 | Commit | 9255dc7a253b0ccc959486e2bca901246202afeb |
Node | 24 | @actions/cache |
5.0.1 |
| Release | v5.0.2 | Commit | 8b402f58fbc84540c8b491a91e594a4576fec3d7 |
Node | 24 | @actions/cache |
5.0.3 |
| Release | v5.0.3 | Commit | cdf6c1fa76f9f475f3d7449005a359c84ca0f306 |
Node | 24 | @actions/cache |
5.0.5 |
| Release | v5.0.4 | Commit | 668228422ae6a00e4ad889ee87cd7109ec5666a7 |
Node | 24 | @actions/cache |
5.0.5 |
| Release | v5.0.5 | Commit | 27d5ce7f107fe9357f9df03efb73ab90386fccae |
Node | 24 | @actions/cache |
5.0.5 |
| Release | v5.1.0 | Commit | caa296126883cff596d87d8935842f9db880ef25 |
Node | 24 | @actions/cache |
5.1.0 |
| Release | v6.0.0 | Commit | 2c8a9bd7457de244a408f35966fab2fb45fda9c8 |
Node | 24 | @actions/cache |
6.0.1 |
| Release | v6.1.0 | Commit | 55cc8345863c7cc4c66a329aec7e433d2d1c52a9 |
Node | 24 | @actions/cache |
6.1.0 |
The v3 releases use managed Node 16 and emit its standard deprecation warning. Node 20 declarations run with managed Node 24. Every admitted bundle selects cache v2 from ACTIONS_CACHE_SERVICE_V2, uses ACTIONS_RESULTS_URL and a job-scoped runtime token, and preserves the root restore and post-save lifecycle and separate entry points. A non-routable ACTIONS_CACHE_URL satisfies the legacy availability gate. Cache traffic still uses ACTIONS_RESULTS_URL. Their tar with zstd-or-gzip archive versioning is compatible across releases.
On Windows jobs, cache actions use a restricted tool path: Git\usr\bin and zstd under the Windows Program Files directory, then Windows System32. Install Git for Windows and zstd there for GNU tar with zstd compression. Without zstd, the upstream client uses gzip. Without the tar provided by Git, the client can use Windows System32 tar. Cross-OS archives require GNU tar and zstd. The runtime obtains installation paths from Windows, replaces tool-selection environment variables case-insensitively, and excludes workflow PATH and GITHUB_PATH additions. Keep these installation directories outside workflow write access. This doesn't enable Buildkite cache volumes.
The snapshot admits a commit only when every bundle it runs selects cache v2 and embeds one @actions/cache client version of 4.0.0 or later. Commits before v3.4.0 and v4.2.0 bundle cache-v1 clients and are absent. v3.4.1 is snapshotted but excluded because its upstream release warns that it was published with an incorrect SHA.
A resolved commit outside the snapshot doesn't run. actions/cache runs upstream JavaScript with a job-scoped cache token, so an unaudited bundle could act on the cache service. Instead, the compiler runs the newest principal release for the requested major version (v5 or v5.2.0 runs v5.1.0), or v6.1.0 when the ref names no admitted major, a branch, or a bare commit. The plan records the requested ref and the substitute commit, and compilation emits one W_CACHE_UNKNOWN_COMMIT_SUBSTITUTED warning per distinct resolved commit:
actions/cache@v6 resolved to commit <resolved-commit>, which is not in the frozen actions/cache snapshot admitted to the Buildkite cache-v2 service. The audited v6.1.0 release (55cc8345863c7cc4c66a329aec7e433d2d1c52a9) runs instead. Pin actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 to remove this warning.
The substitute must resolve to its recorded commit, or compilation fails. Substitution keeps floating v3 through v6 refs working when upstream publishes a release after the last snapshot refresh, and it also covers pre-cache-v2 releases, withdrawn v3.4.1, and pinned unknown commits. The substitute is a different upstream bundle from the one requested, so pin a listed commit to run an exact release. The frozen refs and per-commit profiles are refreshed when buildkite-gha is updated.
JavaScript and Docker actions with compatible bundled cache clients also receive job-bound cache-v2 credentials when the service is available. Root invocations of actions/setup-node, actions/setup-java, actions/setup-python, actions/setup-go, and actions/setup-dotnet use a subprocess-scoped synthetic GITHUB_SERVER_URL when the real host would make their clients select cache v1. Each allowlisted action is audited, including its bundled dependencies, to confirm that GITHUB_SERVER_URL affects only caching behavior and isn't load-bearing for any request the action makes. The workflow expression context retains the real server URL. Ordinary run steps and native action adapters don't receive cache credentials.
Repositories, credentials, and GitHub services
This section describes which repositories jobs can access, and how GitHub tokens, secrets, OIDC, and other GitHub services work on Buildkite Pipelines.
Repositories
| Source | Status | Boundary | |||
|---|---|---|---|---|---|
| Source | Public GitHub event repository | Status | ✅ Supported | Boundary | No additional boundary. |
| Source | Private GitHub event repository | Status | 🟡 Supported subset | Boundary | Buildkite must authorize repository-provider Git credentials. Buildkite issues these credentials only to jobs on Buildkite hosted agents. |
| Source | Internal or private Origin event repository | Status | 🟡 Supported subset | Boundary |
BUILDKITE_REPO must be the exact https://origin.cursor.com/git/<namespace>/<repository>.git URL of the pipeline. Buildkite must authorize repository-provider Git credentials. |
| Source | Alternate repository in actions/checkout
|
Status | ❌ Unsupported | Boundary | Not available. |
| Source | Public GitHub action | Status | 🟡 Supported subset | Boundary | Subject to the action boundaries. |
| Source | Private reusable workflow | Status | 🟡 Supported subset | Boundary | Same-repository or explicitly approved cross-repository source. Resolved by the importer only. |
| Source | Private action | Status | ❌ Unsupported | Boundary | No private action source access. |
| Source | GitHub Enterprise Server or an unlisted provider | Status | ❌ Unsupported | Boundary | Not available. |
GitHub token
🟡 Supported subset. A job requests a short-lived GITHUB_TOKEN for the event repository when the job:
- Statically references
secrets.GITHUB_TOKENorgithub.token. - Uses an action whose effective input default can reach
github.tokenfor the event provider.
A github.server_url == 'https://github.com' guard skips the token branch for an Origin repository. Native adapters ignore upstream input defaults, so actions/checkout alone doesn't request a token.
The permissions of the top-level workflow set the scope. Token issuance needs the pipeline's Allow workflow-authorized GitHub access tokens setting, which is off by default for existing pipelines and on for pipelines created with GitHub Actions setup mode. Tokens are issued only to running command jobs on Buildkite hosted agents in github.com pipelines that use the full-access GitHub repository provider. If the setting is off, the runtime links to the pipeline repository setting. If an enabled request is rejected, the runtime instead asks you to check the top-level permissions and the access of the Buildkite GitHub App to the event repository. If those checks don't find the cause, contact the Buildkite Support team at support@buildkite.com and include the build URL shown in the error.
In buildkite-gha v0.102.1 and later, workflow-token acquisition retries temporary failures: Agent API 503 Service Unavailable responses, and 429 Too Many Requests responses with a valid Retry-After header. Acquisition makes at most three attempts within 45 seconds, bounded by job cancellation and the job deadline. Each attempt has a 15-second limit. Retry delays use 1-second and 2-second bases, plus random jitter up to the base. A valid Retry-After value, in seconds or as an HTTP date, sets the minimum delay before jitter. If the delay would exhaust the remaining time, acquisition fails without waiting.
Other statuses, transport errors, and invalid successful responses aren't retried. This includes 400 Bad Request responses. Action-source token requests and checkout don't retry. Each retry uses the same repository, workflow, and permissions. Buildkite Pipelines checks authorization and the per-job request limit for every attempt.
Buildkite Pipelines reads that policy from the pipeline repository at the immutable build commit. The workflow must be a simple .yml or .yaml file directly under .github/workflows/.
- Omitted permissions mean exactly
contents: read. - GitHub repository and organization defaults aren't inherited.
-
read-allbecomes an explicit 13-scope read map.write-allis rejected and creates no token. - Write access needs an explicit, non-empty top-level map.
- An empty map, or scopes that all resolve to
none, creates no token. - Job-level repository permission maps don't change the scope.
Compilation warns when job permissions differ from the applied top-level map.
Reusable-workflow jobs receive the top-level repository permissions of the requesting workflow. Pipelines doesn't inspect called-workflow maps for GITHUB_TOKEN, so those maps can't narrow it. The separate id-token permission still supports called-workflow narrowing. Compilation warns when a called policy would have narrowed the repository token.
Pull requests and their triggered or rebuilt descendants have a contents: read ceiling. Merge-queue builds and their descendants can't request a token. GitHub Enterprise Server is unsupported. The backend verifies provenance and remains authoritative.
A job can request read-only repository access:
permissions:
contents: read
jobs:
inspect:
runs-on: ubuntu-latest
steps:
- env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh api repos/{owner}/{repo}
Restrict write tokens to trusted code
The server restricts pull requests, merge queues, and their descendants. For other builds, job binding doesn't establish that an arbitrary commit is trusted. Restrict who can create builds and enable write tokens only when branch builds run trusted code.
The token isn't part of the initial job environment. Workflow-authored github.token references are step-only and use the same token as secrets.GITHUB_TOKEN. Effective action input defaults can also use the token. Automatic ambient GITHUB_TOKEN is unsupported.
Other secrets and OIDC
🟡 Supported subset. Direct jobs can use static ${{ secrets.NAME }} references. Local reusable-workflow jobs can inherit or explicitly map declared secret aliases.
The compiler records names, not values, in the destination job plan. At runtime, the job calls buildkite-agent secret get NAME and registers the value with both redactors before use. Missing or denied secrets fail without printing the secret or agent error. The job annotation explains how to create or migrate the secret, check its access policy, or retry a temporarily unavailable secret service. To copy GitHub Actions repository secrets into Buildkite secrets, see Migrate GitHub Actions secrets.
These are Buildkite destination-job secrets, not GitHub repository, environment, event, or fork-scoped secrets. Buildkite secret access policies are the authorization boundary. Code in the same job can also call buildkite-agent secret get. Jobs with a GitHub environment resolve environment-defined secret names through the <ENVIRONMENT>_<NAME> naming convention. The values remain ordinary Buildkite secrets.
GITHUB_TOKEN stays on its separate workflow-token contract and can't be replaced by an ordinary Buildkite secret.
Unsupported secret uses include:
- Dynamic, whole-context, filtered, or projected access
- Conditions and other compile-time expressions
- Remote reusable-workflow secret forwarding
- Literals, compound expressions, or references through
needs,vars,env,inputs, or arbitrarygithubproperties in explicit mappings
Action metadata can't add secret authority to a plan. A secret used only by an optional action input becomes an empty value unless another field requires it.
Jobs with id-token: write expose the GitHub Actions getIDToken() contract to host JavaScript actions, including those called by composite actions. The endpoint mints a Buildkite OIDC token for the requested audience. Cloud identity providers must trust the Buildkite issuer and claims, not the GitHub ones. For an AWS example, see Use OIDC with AWS.
id-token: read, id-token: none, and omitted permissions don't expose the endpoint.
The plugin can apply additional Buildkite OIDC settings to every mint:
plugins:
- github-actions#latest:
workflow: .github/workflows/deploy.yml
oidc:
claims:
- organization_id
aws-session-tags:
- organization_slug
- pipeline_id
subject-claim: pipeline_id
claims and aws-session-tags accept non-empty lists. subject-claim accepts one non-empty immutable claim name. The Agent API owns the claim vocabulary and rejects unsupported names when the job mints a token.
Plugin OIDC configuration doesn't grant id-token: write. A job without that permission receives no endpoint.
The endpoint variables are scoped to each host action lifecycle invocation. Shell steps, Docker container actions, and actions running in job containers don't receive them. Container actions that call getIDToken() fail with its missing endpoint variable diagnostic.
GitHub services
❌ Unsupported beyond the integrations listed above. The runtime of an action may still require unsupported GitHub services. Buildkite Pipelines provides no GitHub Packages, Releases, Checks, or deployment service emulation beyond the documented integrations.
Runtime behavior and limits
This section describes the default environment, runner tools, result handling, and limits of generated jobs.
Default environment
The runtime sets GITHUB_REF, GITHUB_REF_NAME, and GITHUB_REF_TYPE from the corresponding github context fields. Shell steps and actions receive the same values. Workflow, job, step, action, and GITHUB_ENV entries can't override these process variables.
The runtime sets GITHUB_WORKFLOW to the top-level name of the workflow. If the workflow has no name, the runtime uses the repository-relative workflow path. GITHUB_WORKFLOW_REF identifies that top-level workflow as <owner>/<repo>/<path>@<event-ref>, and GITHUB_WORKFLOW_SHA is the event commit. Jobs expanded from local, public, or private reusable workflows retain this caller identity. Workflow and step environment entries can't override these values.
Runner tools
Linux tools come from the selected host environment. macOS and Windows agents must provide tools used by shell steps. Runner labels don't guarantee GitHub image parity. The runtime sets RUNNER_OS and RUNNER_ARCH to Linux/X64, Linux/ARM64, macOS/ARM64, or Windows/X64, and RUNNER_ENVIRONMENT to self-hosted. Workflow and step environment entries can't override these values.
On Linux, RUNNER_TOOL_CACHE is job-private unless the backend requests the hosted tool cache. An explicit tool_cache: true selects /opt/hostedtoolcache and requires it to exist. false uses the job-private cache without changing the preinstalled tools of the host. Native Namespace targets currently return false. When the field is absent, immutable image targets retain their existing /opt/hostedtoolcache behavior, including local hosted-toolchains presets and explicitly configured images.
On native Namespace runners (nsc-gha-image), the Docker CLI routes docker build to a buildx builder with the remote driver, so docker build -t app . && docker run app would fail with No output specified with remote driver. At job start, the runtime adds default-load=true to the stored single-node buildx builders of the runner that use the remote, docker-container, or kubernetes driver, so plain docker build loads the image into the Docker daemon as on GitHub-hosted runners. Explicit --push or --output flags still control the output.
Builders with more than one node are left unchanged, because buildx rejects the load that default-load implies when a build spans nodes, and an output-less multi-platform build that ran cache-only would fail. The job log names each skipped builder, and plain docker build still needs --load there. The node count of nsc-remote depends on the Namespace tenant. Problems print one warning and never fail the job.
On macOS, the bootstrap creates /Users/runner/hostedtoolcache, makes it owned and writable by the agent user, and selects it as RUNNER_TOOL_CACHE. This requires non-interactive sudo and preserves the fixed installation prefix used by actions such as ruby/setup-ruby. Agents must still provide compatible native dependencies for downloaded tools. macOS container images are unsupported.
On Windows, RUNNER_TOOL_CACHE is job-private. Windows container images are unsupported.
Results, retries, and cancellation
- A runtime-skipped Actions job remains successful in Buildkite Pipelines, appends
(skipped)to its job label, and publishes a logicalskippedresult for downstream imported jobs. - Retry the whole build if a producer result or artifact becomes ambiguous. A deferred matrix step may be retried on its own. Retrying its producer job after the matrix expanded requires a new build.
- Cancellation targets the complete process tree. Linux and macOS send
SIGINT,SIGTERMafter 7.5 seconds, thenSIGKILLafter another 2.5 seconds. Windows terminates the process tree through a Job Object without a signal grace period. - Summary, annotation, or skipped-label publication failure produces a warning and doesn't change a completed job result.
Key limits
| Item | Limit | ||
|---|---|---|---|
| Item | Matrix instances per job | Limit | 256 |
| Item | Reusable workflow nesting | Limit | 4 levels |
| Item | Workflow file | Limit | 1 MiB |
| Item | Jobs after reusable workflow expansion | Limit | 1,024 |
| Item | Nested local action depth | Limit | 10 levels |
| Item | Background steps active at once | Limit | 10 |
| Item | Job outputs | Limit | 64 |
| Item | Output value | Limit | 1 KiB |
| Item | Job or step timeout | Limit | 360 minutes |
| Item | Artifacts per job | Limit | 64 |
| Item | Files per uploaded artifact | Limit | 10,000 |
| Item | Uploaded source data or ZIP | Limit | No buildkite-gha limit. Subject to Buildkite agent and storage limits. |
| Item | Job summary | Limit | 1 MiB |
| Item |
hashFiles() patterns |
Limit | 255 per call, 1 KiB each, 64 KiB total |
| Item |
hashFiles() inspected entries |
Limit | 1,000,000 per call |
| Item |
hashFiles() execution budget |
Limit | 30 seconds per call |
| Item |
hashFiles() matched files |
Limit | 10,000 per call |
| Item |
hashFiles() selected bytes |
Limit | 1 GiB per call |
Validation
Check syntax, static graph construction, and every declared trigger without an event:
buildkite-gha validate .github/workflows/ci.yml
This result is event-independent and doesn't claim hosted admission.
Apply the same profile as production upload:
buildkite-gha validate \
--profile hosted \
--event-path .buildkite/events/current.json \
.github/workflows/ci.yml
Use --event instead of --event-path to evaluate the hosted profile with a generated minimal snapshot. See Validate a workflow for supported events and representative payloads, including deployment events. Generated snapshots are compatibility test inputs, not proof of every activity or equivalents to real payloads. The --event and --event-path options are mutually exclusive.
Use --all-events to evaluate every declared supported event separately. Its processing-report/v3 output preserves the event-independent result and the v2 report of each generated event. Aggregate admission means every generated snapshot was admitted. It doesn't cover other payload shapes. A context-required result means compilation and hosted-policy checks passed, but generated inputs can't measure a supported admission path, such as push or pull-request path filters without linked webhook and local diff evidence. This result doesn't claim admission.
The results mean:
- Compilable: Syntax, declared triggers, and the static job graph can be translated.
- Not applicable: The workflow doesn't declare the selected event, so upload would skip it without compiling it.
- Admitted: Resolved actions and generated plans pass production policy.
- Context required: A supported admission path needs evidence that this validation input doesn't provide.
- Runtime-proven: Repository tests or hosted evidence have executed the behavior.
Admission doesn't execute arbitrary action code. An admitted action may still depend on an unsupported GitHub service.
See Use the buildkite-gha CLI for event snapshots, JSON reports, compilation, and direct upload.