Buildkite Agent — Interfaces#
Interface catalog#
core.APIClient#
- Package:
github.com/buildkite/agent/v3/core - File:
core/api_client.go - Methods:
AcquireJob(context.Context, string, ...api.Header) (*api.Job, *api.Response, error) Connect(context.Context) (*api.Response, error) Disconnect(context.Context) (*api.Response, error) FinishJob(context.Context, *api.Job, *bool) (*api.Response, error) Register(context.Context, *api.AgentRegisterRequest) (*api.AgentRegisterResponse, *api.Response, error) StartJob(context.Context, *api.Job) (*api.Response, error) UploadChunk(context.Context, string, *api.Chunk) (*api.Response, error) - Purpose: Defines the subset of
api.Clientmethods needed by thecorepackage — the agent lifecycle operations. Used as the seam between the retry/orchestration layer (core) and the concrete HTTP client (api). - Implementations:
*api.Client(the only real implementation); test doubles in unit tests. - Design quality: Well-segregated. Exactly the 7 methods
coreuses — nothing more. Follows Go’s “accept interfaces, return concrete types” philosophy by defining the interface in the consuming package (core), not the providing one (api).
agent.jobProcess#
- Package:
github.com/buildkite/agent/v3/agent - File:
agent/job_runner.go(line 139) - Methods:
Done() <-chan struct{} Started() <-chan struct{} Interrupt() error Terminate() error Run(ctx context.Context) error WaitStatus() process.WaitStatus - Purpose: Abstracts the job execution backend. The same
JobRunnercode manages both standard OS subprocesses and Kubernetes sidecar runners by holding ajobProcessfield that is assigned at construction time. - Implementations:
*process.Process(standard subprocess execution),*kubernetes.Runner(Unix-socket sidecar protocol for K8s multi-container jobs). - Design quality: Clean and minimal. The channel-based
Done()/Started()pattern enables non-blocking lifecycle observation. The interface is exactly the operationsJobRunnerneeds to manage a running job — start, stop, observe, inspect exit status. This is the primary extension point for new execution backends.
logger.Logger#
- Package:
github.com/buildkite/agent/v3/logger - File:
logger/log.go(line 43) - Methods:
Debug(format string, v ...any) Error(format string, v ...any) Fatal(format string, v ...any) Notice(format string, v ...any) Warn(format string, v ...any) Info(format string, v ...any) WithFields(fields ...Field) Logger SetLevel(level Level) Level() Level - Purpose: Project-wide structured logging abstraction. Provides printf-style levelled logging plus structured field attachment (
WithFields). Every major component receives alogger.Loggeras a constructor argument. - Implementations:
*ConsoleLogger(text output with ANSI colors, used in production),Discard(no-op logger for tests). ThePrintersub-interface decouples formatting backends (TextPrinter,JSONPrinter,TestPrinter) from theLoggeritself. - Design quality: Reasonable for a project-internal logger. The
WithFieldsreturningLoggerenables immutable, scoped loggers (each component can add its own fields without affecting the parent).FatalcallsexitFn, which is injectable for testing. TheNoticelevel (above Info, below Warn) is non-standard — a Buildkite-specific addition for important informational messages.
shell.Logger#
- Package:
github.com/buildkite/agent/v3/internal/shell - File:
internal/shell/logger.go(line 15) - Methods:
io.Writer Printf(format string, v ...any) Headerf(format string, v ...any) // Buildkite "~~~ section" headers Commentf(format string, v ...any) // "# comment" lines Errorf(format string, v ...any) // "🚨 Error: ..." + "^^^ +++" expander Warningf(format string, v ...any) OptionalWarningf(id, format string, v ...any) Promptf(format string, v ...any) // "$ command" shell prompt display - Purpose: Bootstrap-specific output interface. The methods map to Buildkite’s log rendering protocol:
Headerfproduces section headers (~~~ text),Errorfproduces the^^^ +++expander that opens collapsed log sections on failure. This is distinct fromlogger.Logger— it is for the job’s visible output stream, not agent-level diagnostic logging. - Implementations:
*WriterLogger(production, writes formatted ANSI or plain text to anyio.Writer),TestingLogger(wrapstesting.T.Logf),StderrLogger(package-level var for convenient use),DiscardLogger(test/no-op). - Design quality: Well-designed for its domain. Embedding
io.Writerallowsshell.Loggerto be used directly as a write destination. TheOptionalWarningfmethod with a warning ID is a thoughtful addition for user-suppressible warnings. The interface tightly couples to Buildkite’s log format — intentionally domain-specific.
tracetools.Span#
- Package:
github.com/buildkite/agent/v3/tracetools - File:
tracetools/span.go(line 50) - Methods:
AddAttributes(map[string]string) FinishWithError(error) RecordError(error) - Purpose: Unified distributed tracing abstraction that hides the difference between Datadog (via OpenTracing), OpenTelemetry, and no-op tracing.
StartSpanFromContextreturns the appropriate implementation based on theBUILDKITE_TRACING_BACKENDconfig value. This prevents tracing-backend logic from scattering across the codebase. - Implementations:
*OpenTracingSpan(wraps Datadog’s opentracing span),*OpenTelemetrySpan(wraps OTel trace.Span),*NoopSpan(all methods empty — the default when no backend is configured). - Design quality: Minimal and effective. Three methods is exactly the right granularity for the agent’s tracing use cases. The
NoopSpanpattern avoids nil checks at call sites — callers always get a validSpanregardless of configuration. TheFinishWithErrorcombinator (record + end) reduces repetitive error-handling boilerplate.
logger.Printer#
- Package:
github.com/buildkite/agent/v3/logger - File:
logger/log.go(line 124) - Methods:
Print(level Level, msg string, fields Fields) - Purpose: Single-method backend for
ConsoleLogger. Decouples the formatting/output concern from the level-filtering and field-accumulation concern. Allows injecting aTestPrinterduring tests without constructing a full fake logger. - Implementations:
*TextPrinter(ANSI/plain text),*JSONPrinter(structured JSON),TestPrinter(delegates totesting.TB.Logf). - Design quality: Follows ISP. One responsibility: format and write one log line. The split between
LoggerandPrinteris a clean two-layer design.
internal/artifact.APIClient#
- Package:
github.com/buildkite/agent/v3/internal/artifact - File:
internal/artifact/api_client.go - Methods: (subset of artifact-related API calls:
CreateArtifacts,UpdateArtifacts,SearchArtifacts,GetArtifact,DeleteArtifact,UploadArtifact, etc.) - Purpose: Defines the API methods needed by the artifact subsystem, following the same consumer-defined interface pattern as
core.APIClient. Decouples artifact upload/download logic from the concreteapi.Client. - Implementations:
*api.Client. - Design quality: Mirrors
core.APIClientin intent and style. Keeps the artifact package independently testable with a small mock surface.
Interface patterns#
- Size distribution: Interfaces are small to medium.
logger.Logger(9 methods) is the largest.core.APIClient(7 methods),jobProcess(6 methods),shell.Logger(7+embedded),tracetools.Span(3 methods),logger.Printer(1 method). Average ~5 methods; no “god interfaces.” - Embedding:
shell.Loggerembedsio.Writer, making the interface usable directly as anio.Writersink. This is the only embedding observed among the key interfaces. - Implicit satisfaction: All interfaces follow Go’s implicit implementation model. The defining convention is consumer-side definition:
core.APIClientlives incore/,internal/artifact.APIClientlives ininternal/artifact/— not in theapi/package that satisfies them. This is idiomatic Go and enables testing without circular dependencies. - stdlib interfaces used:
io.Writer(embedded inshell.Logger);context.Contextis pervasive in all API method signatures.process.WaitStatuswrapssyscall.WaitStatusfor cross-platform abstraction.
Key abstractions#
core.APIClient— The primary seam between orchestration and HTTP. Without it, the retry/backoff logic incorewould be untestable and directly coupled to the concrete HTTP client. Its existence enables the entirecorepackage to be tested with a simple fake.agent.jobProcess— The execution backend abstraction that makes the Kubernetes execution model possible without forking theJobRunnercode. New execution backends (e.g., a future WASM runner or remote execution protocol) would implement this interface.logger.Logger— Injected into virtually every significant component. ItsWithFieldsdesign creates a structured, scoped logging tree that mirrors the component hierarchy (agent → worker → job runner). ThePrintersub-interface provides a useful secondary seam for test output capture.shell.Logger— Domain-specific to Buildkite’s log rendering protocol. The interface encodes the agent’s output conventions (headers, prompts, expandable errors) making bootstrap execution output consistent and testable without parsing ANSI escape sequences.tracetools.Span— Isolates the two supported tracing backends behind three methods. TheNoopSpaneliminates nil-guard boilerplate at every instrumented call site. This pattern is worth emulating in any project that must support multiple observability backends or “no-op by default” behavior.
Interface-driven extensibility#
The project uses interfaces sparingly but precisely. The two main extension points are:
- Execution backend:
jobProcessallowsJobRunnerto run jobs via OS subprocesses, Kubernetes sidecars, or any future mechanism without code changes to the orchestration layer. - API client abstraction: The consumer-defined
APIClientpattern (both incore/andinternal/artifact/) makes the agent’s core logic independently testable from the HTTP layer and theoretically swappable (e.g., for a mock CI API in integration tests).
The tracing system (tracetools.Span) is a third extensibility point: adding a new tracing backend requires implementing three methods and adding a case in StartSpanFromContext.
There is no formal plugin system beyond the already-existing “plugin” concept in the bootstrap (which is a shell-script mechanism, not a Go interface). All extensibility visible at the Go interface level is internal and test-oriented rather than user-facing.