package main // Provider is the abstraction over "the thing that turns a Tangled // pipeline trigger into pipeline.status events". It exists so the rest // of tack can stay agnostic to whether a given trigger is dispatched to // Buildkite, run by a stub for testing, or anything else we plug in later. import ( "context" "errors" "time" "tangled.org/core/api/tangled" ) // LogLine is the on-the-wire shape of a single log frame emitted by a // Provider. It mirrors tangled.org/core/spindle/models.LogLine on the // JSON level without importing the upstream package — that package // transitively pulls in git, vault, redis and a few hundred other // modules just to expose a handful of types we don't otherwise need. // // The JSON tags here MUST stay byte-compatible with the upstream // struct: the appview's log proxy decodes against the upstream type, // so any tag drift breaks the appview's renderer. type LogLine struct { Kind string `json:"kind"` Content string `json:"content"` Time time.Time `json:"time"` StepId int `json:"step_id"` Stream string `json:"stream,omitempty"` StepStatus string `json:"step_status,omitempty"` StepKind string `json:"step_kind,omitempty"` StepCommand string `json:"step_command,omitempty"` } // LogKind / StepStatus enum values, matching the upstream constants // (LogKindData, LogKindControl, StepStatusStart, StepStatusEnd) on // the wire. Use these instead of bare strings so we don't drift. const ( LogKindData = "data" LogKindControl = "control" StepStatusStart = "start" StepStatusEnd = "end" ) // ErrLogsNotFound is returned by Provider.Logs when the requested // (knot, pipelineRkey, workflow) tuple has no recorded logs — either // because that workflow never ran on this spindle, or because the // provider has since dropped it. The CI log subscription retries this while // a workflow is active to cover asynchronous provider startup. var ErrLogsNotFound = errors.New("logs not found") // Provider dispatches a Tangled pipeline trigger to whatever backend // actually runs the workflows, and exposes per-workflow log retrieval. // // Implementations are responsible for publishing // sh.tangled.pipeline.status records back through whatever channel // they were constructed with. type Provider interface { // Spawn kicks off a pipeline run for every workflow in workflows. // // It MUST be non-blocking: the caller is the eventconsumer worker // that's shared across all knot subscriptions, so per-pipeline // work has to live on its own goroutine. A typical implementation // fans out into a goroutine per workflow and returns immediately. // // ctx is the consumer's app-scoped context (lives until shutdown, // not just for the duration of one event). Implementations are // expected to honour cancellation: in-flight runs should wind // down without issuing further publishes once ctx is done. // // knot is the knot hostname the trigger arrived on; it's the // authority half of the pipeline ATURI that pipeline.status // records reference. pipelineRkey is the trigger record's rkey // on that knot. actor is the DID that the knot consumer has // already authorized to spawn this work; concretely, the // publisher of the originating sh.tangled.repo record (i.e. // the repo owner). It is guaranteed non-empty: the consumer // never invokes Spawn for an unauthorized trigger, so providers // can treat actor as "the identity to attribute this run to" // without re-checking authorization. trigger is the decoded // record's TriggerMetadata (may be nil; the lexicon doesn't // enforce its presence) and carries the commit/branch/PR data // a real CI provider needs to kick off a build. workflows is // the unmodified slice from the decoded sh.tangled.pipeline // record; implementations should tolerate nil entries and // zero-length names defensively, since the lexicon doesn't // enforce either. Spawn( ctx context.Context, knot string, pipelineRkey string, actor string, trigger *tangled.Pipeline_TriggerMetadata, workflows []*tangled.Pipeline_Workflow, ) // Logs returns a channel of log frames for a single workflow run, // identified by the same (knot, pipelineRkey, workflow) tuple // Spawn was invoked with. // // Each LogLine is translated to one sh.tangled.ci.subscribePipelineLogs // control or data event by the XRPC subscription handler. // // The returned channel is closed by the provider when the stream // is complete; a closed channel is the only "end of stream" // signal — there is no separate done channel. Implementations // MUST also stop sending and close the channel when ctx is // cancelled, so a disconnecting client doesn't leak a producer // goroutine. // // Backend errors that occur *after* a successful return (e.g. // the upstream Buildkite log socket dying mid-stream) are logged // internally and surface to the consumer as an early channel // close. The same observable behaviour the appview already // handles for a websocket that closes mid-stream. // // Returns ErrLogsNotFound if no logs exist for the requested // tuple. Any other returned error indicates a backend failure. On a // non-nil error, the channel is nil and there is nothing to drain. Logs( ctx context.Context, knot string, pipelineRkey string, workflow string, ) (<-chan LogLine, error) }