Stitch any CI into Tangled
2

Configure Feed

Select the types of activity you want to include in your feed.

1package main 2 3// Provider is the abstraction over "the thing that turns a Tangled 4// pipeline trigger into pipeline.status events". It exists so the rest 5// of tack can stay agnostic to whether a given trigger is dispatched to 6// Buildkite, run by a stub for testing, or anything else we plug in later. 7 8import ( 9 "context" 10 "errors" 11 "time" 12 13 "tangled.org/core/api/tangled" 14) 15 16// LogLine is the on-the-wire shape of a single log frame emitted by a 17// Provider. It mirrors tangled.org/core/spindle/models.LogLine on the 18// JSON level without importing the upstream package — that package 19// transitively pulls in git, vault, redis and a few hundred other 20// modules just to expose a handful of types we don't otherwise need. 21// 22// The JSON tags here MUST stay byte-compatible with the upstream 23// struct: the appview's log proxy decodes against the upstream type, 24// so any tag drift breaks the appview's renderer. 25type LogLine struct { 26 Kind string `json:"kind"` 27 Content string `json:"content"` 28 Time time.Time `json:"time"` 29 StepId int `json:"step_id"` 30 Stream string `json:"stream,omitempty"` 31 StepStatus string `json:"step_status,omitempty"` 32 StepKind string `json:"step_kind,omitempty"` 33 StepCommand string `json:"step_command,omitempty"` 34} 35 36// LogKind / StepStatus enum values, matching the upstream constants 37// (LogKindData, LogKindControl, StepStatusStart, StepStatusEnd) on 38// the wire. Use these instead of bare strings so we don't drift. 39const ( 40 LogKindData = "data" 41 LogKindControl = "control" 42 StepStatusStart = "start" 43 StepStatusEnd = "end" 44) 45 46// ErrLogsNotFound is returned by Provider.Logs when the requested 47// (knot, pipelineRkey, workflow) tuple has no recorded logs — either 48// because that workflow never ran on this spindle, or because the 49// provider has since dropped it. The HTTP /logs handler translates 50// this into a 404 *before* upgrading to WebSocket so the appview's 51// dialer sees a real HTTP error rather than an immediate close. 52var ErrLogsNotFound = errors.New("logs not found") 53 54// Provider dispatches a Tangled pipeline trigger to whatever backend 55// actually runs the workflows, and exposes per-workflow log retrieval. 56// 57// Implementations are responsible for publishing 58// sh.tangled.pipeline.status records back through whatever channel 59// they were constructed with. 60type Provider interface { 61 // Spawn kicks off a pipeline run for every workflow in workflows. 62 // 63 // It MUST be non-blocking: the caller is the eventconsumer worker 64 // that's shared across all knot subscriptions, so per-pipeline 65 // work has to live on its own goroutine. A typical implementation 66 // fans out into a goroutine per workflow and returns immediately. 67 // 68 // ctx is the consumer's app-scoped context (lives until shutdown, 69 // not just for the duration of one event). Implementations are 70 // expected to honour cancellation: in-flight runs should wind 71 // down without issuing further publishes once ctx is done. 72 // 73 // knot is the knot hostname the trigger arrived on; it's the 74 // authority half of the pipeline ATURI that pipeline.status 75 // records reference. pipelineRkey is the trigger record's rkey 76 // on that knot. trigger is the decoded record's TriggerMetadata 77 // (may be nil — the lexicon doesn't enforce its presence) and 78 // carries the commit/branch/PR data a real CI provider needs to 79 // kick off a build. workflows is the unmodified slice from the 80 // decoded sh.tangled.pipeline record; implementations should 81 // tolerate nil entries and zero-length names defensively, since 82 // the lexicon doesn't enforce either. 83 Spawn( 84 ctx context.Context, 85 knot string, 86 pipelineRkey string, 87 trigger *tangled.Pipeline_TriggerMetadata, 88 workflows []*tangled.Pipeline_Workflow, 89 ) 90 91 // Logs returns a channel of log frames for a single workflow run, 92 // identified by the same (knot, pipelineRkey, workflow) tuple 93 // Spawn was invoked with. 94 // 95 // Each LogLine corresponds 1:1 to one frame written by the HTTP 96 // /logs WebSocket handler — the handler marshals the LogLine and 97 // emits it as a single TextMessage so appview clients see the 98 // exact same wire shape they would from the stock Tangled spindle 99 // (whose on-disk log file holds the same records, one per line). 100 // 101 // The returned channel is closed by the provider when the stream 102 // is complete; a closed channel is the only "end of stream" 103 // signal — there is no separate done channel. Implementations 104 // MUST also stop sending and close the channel when ctx is 105 // cancelled, so a disconnecting client doesn't leak a producer 106 // goroutine. 107 // 108 // Backend errors that occur *after* a successful return (e.g. 109 // the upstream Buildkite log socket dying mid-stream) are logged 110 // internally and surface to the consumer as an early channel 111 // close. The same observable behaviour the appview already 112 // handles for a websocket that closes mid-stream. 113 // 114 // Returns ErrLogsNotFound if no logs exist for the requested 115 // tuple. Any other returned error indicates a backend failure 116 // and should surface as a 5xx to the HTTP caller. On a non-nil 117 // error, the channel is nil and there is nothing to drain. 118 Logs( 119 ctx context.Context, 120 knot string, 121 pipelineRkey string, 122 workflow string, 123 ) (<-chan LogLine, error) 124}