Stitch any CI into Tangled
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. workflows is the unmodified slice from the
77 // decoded sh.tangled.pipeline record; implementations should
78 // tolerate nil entries and zero-length names defensively, since
79 // the lexicon doesn't enforce either.
80 Spawn(
81 ctx context.Context,
82 knot string,
83 pipelineRkey string,
84 workflows []*tangled.Pipeline_Workflow,
85 )
86
87 // Logs returns a channel of log frames for a single workflow run,
88 // identified by the same (knot, pipelineRkey, workflow) tuple
89 // Spawn was invoked with.
90 //
91 // Each LogLine corresponds 1:1 to one frame written by the HTTP
92 // /logs WebSocket handler — the handler marshals the LogLine and
93 // emits it as a single TextMessage so appview clients see the
94 // exact same wire shape they would from the stock Tangled spindle
95 // (whose on-disk log file holds the same records, one per line).
96 //
97 // The returned channel is closed by the provider when the stream
98 // is complete; a closed channel is the only "end of stream"
99 // signal — there is no separate done channel. Implementations
100 // MUST also stop sending and close the channel when ctx is
101 // cancelled, so a disconnecting client doesn't leak a producer
102 // goroutine.
103 //
104 // Backend errors that occur *after* a successful return (e.g.
105 // the upstream Buildkite log socket dying mid-stream) are logged
106 // internally and surface to the consumer as an early channel
107 // close. The same observable behaviour the appview already
108 // handles for a websocket that closes mid-stream.
109 //
110 // Returns ErrLogsNotFound if no logs exist for the requested
111 // tuple. Any other returned error indicates a backend failure
112 // and should surface as a 5xx to the HTTP caller. On a non-nil
113 // error, the channel is nil and there is nothing to drain.
114 Logs(
115 ctx context.Context,
116 knot string,
117 pipelineRkey string,
118 workflow string,
119 ) (<-chan LogLine, error)
120}