Stitch any CI into Tangled
105
fork

Configure Feed

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

at main 132 lines 5.7 kB view raw
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}