feat(buildrunner): add Buildkite BuildRunner implementation#190
Conversation
Implements buildrunner.BuildRunner and buildrunner.Factory backed by the Buildkite CI platform, under submitqueue/extension/buildrunner/buildkite. Design: - Trigger/Cancel return promptly (per the async/sync contract in the build-runner RFC): both enqueue work on a buffered channel and a background worker contacts Buildkite, keeping the orchestrator's queue loops decoupled from Buildkite availability. - The in-memory build-ID -> Buildkite-ref map is a pure latency cache, not the source of truth. The SQ build ID is stamped into the Buildkite build's metadata at create time; Status and Cancel re-derive the ref via a metadata-filtered build lookup on cache miss, so they remain correct after a process restart that empties the cache (no orphaned Accepted-forever builds). - The background worker retries transient create/cancel failures with backoff. If a submission exhausts its retries, the build is recorded as a submission failure and Status reports terminal Failed (with the reason in BuildMetadata["error"]) rather than polling Accepted forever. - mapState collapses Buildkite states into BuildStatus; unrecognised states map to the non-terminal Unknown (not Failed), so an unknown state does not terminally fail a batch. - Each job is dispatched to its own goroutine so a retrying submit does not head-of-line block other builds. Errors are returned plain (classification left to the controller, per core/errs). Includes config knobs (queue sizes, submit timeout, retry attempts/backoff, test overrides) and unit tests covering trigger/status/cancel, cache-miss recovery, retry-then-succeed, retry-exhaustion-fails, and state mapping. Known limitation: a process restart while a trigger job is still buffered (never submitted to Buildkite) leaves the build Accepted with nothing to recover; catching that belongs to an orchestrator-level Accepted deadline, which is out of scope for this change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| // constructed by Factory.For for each queue and bound to a single pipeline. | ||
| type Config struct { | ||
| // APIToken is the Buildkite personal or agent API token. Required. | ||
| APIToken string |
There was a problem hiding this comment.
should this be left to be injected through http transport? similar to changeprovider & mergechecker model?
| QueueName string | ||
|
|
||
| // Branch is the target branch builds run against (e.g. "main"). Required. | ||
| Branch string |
There was a problem hiding this comment.
do we need this when have QueueName as the main thing being passed around? should underlying imp infer things based on that queue however they configure or store..rather than we passing something as queueName uniquely represents the repo+target
| TriggerQueueSize int | ||
|
|
||
| // CancelQueueSize is the buffer capacity of the async cancel channel. | ||
| // Defaults to 256. | ||
| CancelQueueSize int |
There was a problem hiding this comment.
i think we can get rid of buffers
| // MaxSubmitAttempts is the number of times the background worker tries a | ||
| // Buildkite create/cancel call before giving up, to ride out transient | ||
| // failures. Defaults to 5. | ||
| MaxSubmitAttempts int | ||
|
|
||
| // SubmitBackoff is the base delay between background-worker retries; the | ||
| // delay grows linearly with the attempt number. Defaults to 1s. | ||
| SubmitBackoff time.Duration |
There was a problem hiding this comment.
should we just accept a retry-policy instead?
|
|
||
| // BaseURL overrides the Buildkite API base URL | ||
| // (default: "https://api.buildkite.com/v2"). Intended for testing. | ||
| BaseURL string |
There was a problem hiding this comment.
can this be encoded into http transporter as well?
| // OrgSlug is the Buildkite organisation slug (URL segment after | ||
| // buildkite.com/). Required. | ||
| OrgSlug string | ||
|
|
||
| // PipelineSlug is the Buildkite pipeline that runs builds for this queue. | ||
| // Resolved by Factory from its Pipelines map. Required. | ||
| PipelineSlug string |
There was a problem hiding this comment.
can these be in the transporter itself?
|
|
||
| // For returns a BuildRunner bound to the Buildkite pipeline configured for | ||
| // cfg.QueueName. Returns an error if no pipeline is configured for the queue. | ||
| func (f Factory) For(cfg buildrunner.Config) (buildrunner.BuildRunner, error) { |
There was a problem hiding this comment.
I was hoping it's implemented outside of the extension, basically in example server we would define this and let implementer decide however they want to wire the implementation per queue, so say one queue could use buildkite, another one can use gh-actions
Why?
buildrunnerextension defined theBuildRunner/Factorycontract and shipped only anoopstub. To run real CI verification in the build stage of the orchestrator pipeline, we need a concrete backend. Buildkite is the first real one. This implementation also has to honor the constraints indoc/rfc/submitqueue/build-runner.md:What?
Adds a Buildkite-backed
BuildRunner+Factoryundersubmitqueue/extension/buildrunner/buildkite/.availability (per the RFC's async/sync table).
meta_dataat create time;StatusandCancelre-derive the ref via a metadata-filtered build lookup on cache miss, so they stay correct after a restart that empties the cache (no Accepted-forever orphans on the post-submitpath).
Statusreturns terminalFailed(reason inBuildMetadata["error"]) instead of polling Accepted forever.mapStatecollapses Buildkite states intoBuildStatus; unrecognised states map to non-terminalUnknown(notFailed) so an unknown state never terminally fails a batch.URL).
Known limitation (out of scope): a restart while a trigger job is still buffered (never submitted to Buildkite) leaves the build Accepted with nothing to recover (TBD on if we want the orchestrator to handle this)
Test Plan
Unit tests cover trigger/status/cancel, payload + metadata stamping, cache-miss recovery (Status and Cancel), retry-then-succeed, retry-exhaustion→Failed, and full state mapping. All local CI-parity checks pass:
The Buildkite REST calls are exercised against an
httptestserver (no live Buildkite dependency); the worker is driven synchronously in tests viadrainTrigger/drainCancelto avoid goroutine timing races.