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. actor is the DID that the knot consumer has 77 // already authorized to spawn this work; concretely, the 78 // publisher of the originating sh.tangled.repo record (i.e. 79 // the repo owner). It is guaranteed non-empty: the consumer 80 // never invokes Spawn for an unauthorized trigger, so providers 81 // can treat actor as "the identity to attribute this run to" 82 // without re-checking authorization. trigger is the decoded 83 // record's TriggerMetadata (may be nil; the lexicon doesn't 84 // enforce its presence) and carries the commit/branch/PR data 85 // a real CI provider needs to kick off a build. workflows is 86 // the unmodified slice from the decoded sh.tangled.pipeline 87 // record; implementations should tolerate nil entries and 88 // zero-length names defensively, since the lexicon doesn't 89 // enforce either. 90 Spawn( 91 ctx context.Context, 92 knot string, 93 pipelineRkey string, 94 actor string, 95 trigger *tangled.Pipeline_TriggerMetadata, 96 workflows []*tangled.Pipeline_Workflow, 97 ) 98 99 // Logs returns a channel of log frames for a single workflow run, 100 // identified by the same (knot, pipelineRkey, workflow) tuple 101 // Spawn was invoked with. 102 // 103 // Each LogLine corresponds 1:1 to one frame written by the HTTP 104 // /logs WebSocket handler — the handler marshals the LogLine and 105 // emits it as a single TextMessage so appview clients see the 106 // exact same wire shape they would from the stock Tangled spindle 107 // (whose on-disk log file holds the same records, one per line). 108 // 109 // The returned channel is closed by the provider when the stream 110 // is complete; a closed channel is the only "end of stream" 111 // signal — there is no separate done channel. Implementations 112 // MUST also stop sending and close the channel when ctx is 113 // cancelled, so a disconnecting client doesn't leak a producer 114 // goroutine. 115 // 116 // Backend errors that occur *after* a successful return (e.g. 117 // the upstream Buildkite log socket dying mid-stream) are logged 118 // internally and surface to the consumer as an early channel 119 // close. The same observable behaviour the appview already 120 // handles for a websocket that closes mid-stream. 121 // 122 // Returns ErrLogsNotFound if no logs exist for the requested 123 // tuple. Any other returned error indicates a backend failure 124 // and should surface as a 5xx to the HTTP caller. On a non-nil 125 // error, the channel is nil and there is nothing to drain. 126 Logs( 127 ctx context.Context, 128 knot string, 129 pipelineRkey string, 130 workflow string, 131 ) (<-chan LogLine, error) 132}