swiftscream/swift-buildkite-pipeline
An ergonomic Swift DSL for generating [Buildkite](https://buildkite.com/platform/) pipelines.
Installation
Add BuildkitePipeline as a dependency in your Package.swift:
.package(url: "https://github.com/SwiftScream/swift-buildkite-pipeline.git", from: "0.1.0")Then add the product to your target dependencies:
.product(name: "BuildkitePipeline", package: "swift-buildkite-pipeline")Quick Start
import BuildkitePipeline
let pipeline = Pipeline {
GlobalEnv("CI", "1")
DefaultAgent(queue: "macos")
NotifySlack("#ci")
Group("Tests") {
Step("Unit Tests") {
Command("swift test")
}
}
Step("Lint") {
Command("swift-format lint .")
}
}
.priority(20)
let yaml = try pipeline.toYAML()
print(yaml)Executable Generator Pattern
For the common case of a Swift executable that prints pipeline YAML to stdout:
import BuildkitePipeline
@main
struct MyPipeline: PipelineGenerator {
init() {}
var pipeline: Pipeline {
Pipeline {
Group("Tests") {
Step("Unit Tests") {
Command("swift test")
}
}
Step("Lint") {
Command("swift-format lint .")
}
}
}
}PipelineGenerator provides a default main() that:
- uses your
pipeline - renders YAML
- writes it to stdout
You can also configure top-level pipeline settings directly in Pipeline { ... }:
GlobalEnv(...)DefaultAgent(...)Metadata(...)NotifyEmail(...),NotifySlack(...),NotifyWebhook(...)- command-step notify helpers:
StepNotifySlack(...),StepNotifyGitHubCheck(),StepNotifyGitHubCommitStatus()
And pipeline-level priority as a modifier:
.priority(...)
Async Content
PipelineGenerator.pipeline is declared as get async throws, so you can also use async work directly in the property getter:
import BuildkitePipeline
@main
struct DynamicPipeline: PipelineGenerator {
init() {}
var pipeline: Pipeline {
get async throws {
let modules = try await discoverChangedModules()
return Pipeline {
for module in modules {
Step("Test \(module)") {
Command("swift test --filter \(module)")
}
.key("test-\(module)")
}
}
}
}
}DSL Examples
Command Step With Agents/Env/Retry
let tests = StepKey("tests")
let pipeline = Pipeline {
Step("Tests") {
Command("swift test --parallel")
Agent(queue: "macos")
Agent("xcode", "15.4")
Env("SWIFT_VERSION", "6.0")
Env("CI", "1")
Plugin("docker", ref: "v5.9.0")
StepNotifySlack("#ci-step")
}
.key(tests)
.softFail(exitStatuses: [1])
.timeoutInMinutes(30)
.automaticallyRetry(limit: 2)
.manualRetry(permitOnPassed: true)
Wait()
.dependsOn(tests)
}Dependencies are wired with StepKey for safer references:
let build = StepKey("build")
Step("Build") {
Command("swift build")
}
.key(build)
Step("Test") {
Command("swift test")
}
.dependsOn(build)
Step("Report") {
Command("swift run report")
}
.dependsOn(build, allowingFailure(StepKey("flaky-non-blocking-check")))Fragment Chaining With Explicit Outputs
For modular sections, compose dependency graphs with .then:
@PipelineFragmentBuilder
var lintAndTest: PipelineFragment {
Step("Lint") {
Command("swift-format lint .")
}
.then {
Step("Test") {
Command("swift test")
}
.then {
Step("Upload Coverage") {
Command("bash .buildkite/upload-coverage.sh")
}
}
.setOutput() // expose `Test` as this fragment's dependency output
}
}
let pipeline = Pipeline {
lintAndTest.then {
Step("Deploy") {
Command("bash .buildkite/deploy.sh")
}
}
}Each .then advances the fragment's current outputs to the fragment you just appended. That means successive chains build on each other:
lintAndTest
.then {
Step("A") {
Command("echo a")
}
}
.then {
Step("B") {
Command("echo b")
}
}In this example, A depends on Test, and B depends on A.
If multiple steps should all depend on the same upstream outputs, place them in the same .then block:
lintAndTest.then {
Step("A") {
Command("echo a")
}
Step("B") {
Command("echo b")
}
}In this case, both A and B depend on Test.
Shared Defaults For A Step Section
Use Steps(template:) with StepTemplate to apply shared defaults to multiple steps:
let lintTemplate = StepTemplate {
Agent(queue: "ios2")
Env("XCODE_SCHEME", "Westfield")
}
.timeoutInMinutes(15)
let pipeline = Pipeline {
Steps(template: lintTemplate) {
Step(":crossed_fingers: XcodeGen") {
Command(".buildkite/xcodegen-lint")
}
Step(":crossed_fingers: SwiftGen") {
Command(".buildkite/swiftgen-lint")
}
Step(":crossed_fingers: SwiftLint") {
Command(".buildkite/swiftlint")
}
.timeoutInMinutes(30) // step-local value wins over template default
}
}Template semantics:
- Template values are applied first, then step-local values.
- Step-local values override conflicts (for example duplicate env keys).
- Additive values (plugins, notifications, artifact paths) are prepended from template and then extended by the step.
Trigger Step With Build Payload
let pipeline = Pipeline {
Trigger("deploy-pipeline")
.label("Deploy")
.key("deploy")
.asynchronous()
.build(
branch: "main",
commit: "HEAD",
message: "Triggered from BuildkitePipeline",
env: ["RELEASE": "true"],
metadata: ["service": "api"]
)
}Block/Input Step
let pipeline = Pipeline {
Block("Release to production?") {
TextField(key: "release_note", text: "Release note", required: true)
SelectField(
key: "env",
select: "Environment",
options: [
Option("Staging", value: "staging"),
Option("Production", value: "production")
],
required: true
)
}
.key("release-gate")
}Rendering
let yaml = try pipeline.toYAML()
let json = try pipeline.toJSON()Design Notes
- DSL-facing declarations are separate from the underlying serializable model.
- The library prefers typed value types/enums for bounded schema areas.
- One-of fields are modeled with enums where practical (
command,depends_on,soft_fail, retry modes). - Custom encoding is limited to cases where Buildkite shape requires it (step union, plugin objects, wait null encoding).
- The structure is intentionally extensible for future schema additions.
Schema Coverage
This package is inspired by Buildkite’s schema repository:
Implemented coverage includes:
- Step kinds: command, wait, block/input, trigger, group
- Common attributes:
label,key,if,branches,depends_on,allow_dependency_failure - Command-focused attributes:
command,plugins,agents,env,artifact_paths,parallelism,priority,timeout_in_minutes,retry,soft_fail,matrix,notify,concurrency,concurrency_group,concurrency_method - Trigger build payload:
branch,commit,message,env,meta_data
Current Limitations / TODO
This first release focuses on common production patterns. Remaining partial areas include:
- Additional notification backends and richer notify payload variants
- Wider plugin option shape helpers (currently generic JSON-like options)
- Full fidelity for all less-common schema branches in
schema.json - Expanded validation utilities (e.g. local semantic checks before serialization)
- More fixture parity with every valid pipeline sample from the schema repository
Testing
Run tests with:
swift testThe suite covers:
- basic pipeline serialization
- command, wait, block/input, trigger, and group step encoding
- env/agents/plugins
- retry/soft_fail/depends_on/matrix/notify
- snapshot-style YAML assertions
AI Agent Documentation
For repository architecture, conventions, and extension guidance for coding agents, see AGENTS.md.
Package Metadata
Repository: swiftscream/swift-buildkite-pipeline
Default branch: master
README: README.md