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.Client methods needed by the core package — 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 core uses — 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 JobRunner code manages both standard OS subprocesses and Kubernetes sidecar runners by holding a jobProcess field 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 operations JobRunner needs 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 a logger.Logger as a constructor argument.
  • Implementations: *ConsoleLogger (text output with ANSI colors, used in production), Discard (no-op logger for tests). The Printer sub-interface decouples formatting backends (TextPrinter, JSONPrinter, TestPrinter) from the Logger itself.
  • Design quality: Reasonable for a project-internal logger. The WithFields returning Logger enables immutable, scoped loggers (each component can add its own fields without affecting the parent). Fatal calls exitFn, which is injectable for testing. The Notice level (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: Headerf produces section headers (~~~ text), Errorf produces the ^^^ +++ expander that opens collapsed log sections on failure. This is distinct from logger.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 any io.Writer), TestingLogger (wraps testing.T.Logf), StderrLogger (package-level var for convenient use), DiscardLogger (test/no-op).
  • Design quality: Well-designed for its domain. Embedding io.Writer allows shell.Logger to be used directly as a write destination. The OptionalWarningf method 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. StartSpanFromContext returns the appropriate implementation based on the BUILDKITE_TRACING_BACKEND config 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 NoopSpan pattern avoids nil checks at call sites — callers always get a valid Span regardless of configuration. The FinishWithError combinator (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 a TestPrinter during tests without constructing a full fake logger.
  • Implementations: *TextPrinter (ANSI/plain text), *JSONPrinter (structured JSON), TestPrinter (delegates to testing.TB.Logf).
  • Design quality: Follows ISP. One responsibility: format and write one log line. The split between Logger and Printer is 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 concrete api.Client.
  • Implementations: *api.Client.
  • Design quality: Mirrors core.APIClient in 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.Logger embeds io.Writer, making the interface usable directly as an io.Writer sink. 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.APIClient lives in core/, internal/artifact.APIClient lives in internal/artifact/ — not in the api/ package that satisfies them. This is idiomatic Go and enables testing without circular dependencies.
  • stdlib interfaces used: io.Writer (embedded in shell.Logger); context.Context is pervasive in all API method signatures. process.WaitStatus wraps syscall.WaitStatus for cross-platform abstraction.

Key abstractions#

  1. core.APIClient — The primary seam between orchestration and HTTP. Without it, the retry/backoff logic in core would be untestable and directly coupled to the concrete HTTP client. Its existence enables the entire core package to be tested with a simple fake.

  2. agent.jobProcess — The execution backend abstraction that makes the Kubernetes execution model possible without forking the JobRunner code. New execution backends (e.g., a future WASM runner or remote execution protocol) would implement this interface.

  3. logger.Logger — Injected into virtually every significant component. Its WithFields design creates a structured, scoped logging tree that mirrors the component hierarchy (agent → worker → job runner). The Printer sub-interface provides a useful secondary seam for test output capture.

  4. 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.

  5. tracetools.Span — Isolates the two supported tracing backends behind three methods. The NoopSpan eliminates 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: jobProcess allows JobRunner to run jobs via OS subprocesses, Kubernetes sidecars, or any future mechanism without code changes to the orchestration layer.
  • API client abstraction: The consumer-defined APIClient pattern (both in core/ and internal/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.