···1+package models
2+3+import (
4+ "context"
5+6+ "github.com/bluesky-social/indigo/atproto/syntax"
7+)
8+9+// Adapter is the core of the spindle. It can use its own way to configure and
10+// run the workflows. The workflow definition can be either yaml files in git
11+// repositories or even from dedicated web UI.
12+//
13+// An adapter is expected to be hold all created workflow runs.
14+type Adapter interface {
15+ // Init intializes the adapter
16+ Init() error
17+18+ // Shutdown gracefully shuts down background jobs
19+ Shutdown(ctx context.Context) error
20+21+ // SetupRepo ensures adapter connected to the repository.
22+ // This usually includes adding repository watcher that does sparse-clone.
23+ SetupRepo(ctx context.Context, repo syntax.ATURI) error
24+25+ // ListWorkflowDefs parses and returns all workflow definitions in the given
26+ // repository at the specified revision
27+ ListWorkflowDefs(ctx context.Context, repo syntax.ATURI, rev string) ([]WorkflowDef, error)
28+29+ // EvaluateEvent consumes a trigger event and returns a list of triggered
30+ // workflow runs. It is expected to return immediately after scheduling the
31+ // workflows.
32+ EvaluateEvent(ctx context.Context, event Event) ([]WorkflowRun, error)
33+34+ // GetActiveWorkflowRun returns current state of specific workflow run.
35+ // This method will be called regularly for active workflow runs.
36+ GetActiveWorkflowRun(ctx context.Context, runId syntax.ATURI) (WorkflowRun, error)
37+38+39+40+41+ // NOTE: baisically I'm not sure about this method.
42+ // How to properly sync workflow.run states?
43+ //
44+ // for adapters with external engine, they will hold every past
45+ // workflow.run objects.
46+ // for adapters with internal engine, they... should also hold every
47+ // past workflow.run objects..?
48+ //
49+ // problem:
50+ // when spindle suffer downtime (spindle server shutdown),
51+ // external `workflow.run`s might be unsynced in "running" or "pending" state
52+ // same for internal `workflow.run`s.
53+ //
54+ // BUT, spindle itself is holding the runs,
55+ // so it already knows unsynced workflows (=workflows not finished)
56+ // therefore, it can just fetch them again.
57+ // for adapters with internal engines, they will fail to fetch previous
58+ // run.
59+ // Leaving spindle to mark the run as "Lost" or "Failed".
60+ // Because of _lacking_ adaters, spindle should be able to manually
61+ // mark unknown runs with "lost" state.
62+ //
63+ // GetWorkflowRun : used to get background crawling
64+ // XCodeCloud: ok
65+ // Nixery: (will fail if unknown) -> spindle will mark workflow as failed anyways
66+ // StreamWorkflowRun : used to notify real-time updates
67+ // XCodeCloud: ok (but old events will be lost)
68+ // Nixery: same. old events on spindle downtime will be lost
69+ //
70+ //
71+ // To avoid this, each adapters should hold outbox buffer
72+ //
73+ // |
74+ // v
75+76+ // StreamWorkflowRun(ctx context.Context) <-chan WorkflowRun
77+78+79+ // ListActiveWorkflowRuns returns current list of active workflow runs.
80+ // Runs where status is either Pending or Running
81+ ListActiveWorkflowRuns(ctx context.Context) ([]WorkflowRun, error)
82+ SubscribeWorkflowRun(ctx context.Context) <-chan WorkflowRun
83+84+85+86+87+ // StreamWorkflowRunLogs streams logs for a running workflow execution
88+ StreamWorkflowRunLogs(ctx context.Context, runId syntax.ATURI, handle func(line LogLine) error) error
89+90+ // CancelWorkflowRun attempts to stop a running workflow execution.
91+ // It won't do anything when the workflow has already completed.
92+ CancelWorkflowRun(ctx context.Context, runId syntax.ATURI) error
93+}
···1+package models
2+3+import (
4+ "fmt"
5+ "slices"
6+7+ "github.com/bluesky-social/indigo/atproto/syntax"
8+ "tangled.org/core/api/tangled"
9+)
10+11+// `sh.tangled.ci.event`
12+type Event struct {
13+ SourceRepo syntax.ATURI // repository to find the workflow definition
14+ SourceSha string // sha to find the workflow definition
15+ TargetSha string // sha to run the workflow
16+ // union type of:
17+ // 1. PullRequestEvent
18+ // 2. PushEvent
19+ // 3. ManualEvent
20+}
21+22+func (e *Event) AsRecord() tangled.CiEvent {
23+ // var meta tangled.CiEvent_Meta
24+ // return tangled.CiEvent{
25+ // Meta: &meta,
26+ // }
27+ panic("unimplemented")
28+}
29+30+// `sh.tangled.ci.pipeline`
31+//
32+// Pipeline is basically a group of workflows triggered by single event.
33+type Pipeline2 struct {
34+ Did syntax.DID
35+ Rkey syntax.RecordKey
36+37+ Event Event // event that triggered the pipeline
38+ WorkflowRuns []WorkflowRun // workflow runs inside this pipeline
39+}
40+41+func (p *Pipeline2) AtUri() syntax.ATURI {
42+ return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", p.Did, tangled.CiPipelineNSID, p.Rkey))
43+}
44+45+func (p *Pipeline2) AsRecord() tangled.CiPipeline {
46+ event := p.Event.AsRecord()
47+ runs := make([]string, len(p.WorkflowRuns))
48+ for i, run := range p.WorkflowRuns {
49+ runs[i] = run.AtUri().String()
50+ }
51+ return tangled.CiPipeline{
52+ Event: &event,
53+ WorkflowRuns: runs,
54+ }
55+}
56+57+// `sh.tangled.ci.workflow.run`
58+type WorkflowRun struct {
59+ Did syntax.DID
60+ Rkey syntax.RecordKey
61+62+ AdapterId string // adapter id
63+ Name string // name of workflow run (not workflow definition name!)
64+ Status WorkflowStatus // workflow status
65+ // TODO: can add some custom fields like adapter-specific log-id
66+}
67+68+func (r WorkflowRun) WithStatus(status WorkflowStatus) WorkflowRun {
69+ return WorkflowRun{
70+ Did: r.Did,
71+ Rkey: r.Rkey,
72+ AdapterId: r.AdapterId,
73+ Name: r.Name,
74+ Status: status,
75+ }
76+}
77+78+func (r *WorkflowRun) AtUri() syntax.ATURI {
79+ return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", r.Did, tangled.CiWorkflowRunNSID, r.Rkey))
80+}
81+82+func (r *WorkflowRun) AsRecord() tangled.CiWorkflowRun {
83+ statusStr := string(r.Status)
84+ return tangled.CiWorkflowRun{
85+ Adapter: r.AdapterId,
86+ Name: r.Name,
87+ Status: &statusStr,
88+ }
89+}
90+91+// `sh.tangled.ci.workflow.status`
92+type WorkflowStatus string
93+94+var (
95+ WorkflowStatusPending WorkflowStatus = "pending"
96+ WorkflowStatusRunning WorkflowStatus = "running"
97+ WorkflowStatusFailed WorkflowStatus = "failed"
98+ WorkflowStatusCancelled WorkflowStatus = "cancelled"
99+ WorkflowStatusSuccess WorkflowStatus = "success"
100+ WorkflowStatusTimeout WorkflowStatus = "timeout"
101+102+ activeStatuses [2]WorkflowStatus = [2]WorkflowStatus{
103+ WorkflowStatusPending,
104+ WorkflowStatusRunning,
105+ }
106+)
107+108+func (s WorkflowStatus) IsActive() bool {
109+ return slices.Contains(activeStatuses[:], s)
110+}
111+112+func (s WorkflowStatus) IsFinish() bool {
113+ return !s.IsActive()
114+}
115+116+// `sh.tangled.ci.workflow.def`
117+//
118+// Brief information of the workflow definition. A workflow can be defined in
119+// any form. This is a common info struct for any workflow definitions
120+type WorkflowDef struct {
121+ AdapterId string // adapter id
122+ Name string // name or the workflow (usually the yml file name)
123+ When any // events the workflow is listening to
124+}
+40
spindle/pipeline.go
···0000000000000000000000000000000000000000
···1+package spindle
2+3+import (
4+ "context"
5+6+ "tangled.org/core/spindle/models"
7+)
8+9+// createPipeline creates a pipeline from given event.
10+// It will call `EvaluateEvent` for all adapters, gather the triggered workflow
11+// runs, and constuct a pipeline record from them. pipeline record. It will
12+// return nil if no workflow run has triggered.
13+//
14+// NOTE: This method won't fail. If `adapter.EvaluateEvent` returns an error,
15+// the error will be logged but won't bubble-up.
16+//
17+// NOTE: Adapters might create sub-event on its own for workflows triggered by
18+// other workflow runs.
19+func (s *Spindle) createPipeline(ctx context.Context, event models.Event) (*models.Pipeline2) {
20+ l := s.l
21+22+ pipeline := models.Pipeline2{
23+ Event: event,
24+ }
25+26+ // TODO: run in parallel
27+ for id, adapter := range s.adapters {
28+ runs, err := adapter.EvaluateEvent(ctx, event)
29+ if err != nil {
30+ l.Error("failed to process trigger from adapter '%s': %w", id, err)
31+ }
32+ pipeline.WorkflowRuns = append(pipeline.WorkflowRuns, runs...)
33+ }
34+35+ if len(pipeline.WorkflowRuns) == 0 {
36+ return nil
37+ }
38+39+ return &pipeline
40+}