ci.go
1package lex
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7)
8
9const (
10 WorkflowNSID = "run.rtw.ci.workflow"
11 ResultNSID = "run.rtw.ci.step.result"
12 GateNSID = "run.rtw.ci.gate"
13 TransitionNSID = "run.rtw.ci.transition"
14 EventNSID = "run.rtw.ci.event"
15
16 // ResultVersion is the result contract version this engine speaks; it
17 // also versions the CI_* environment contract.
18 ResultVersion = 1
19
20 EngineDevenv = "devenv"
21)
22
23// Workflow is run.rtw.ci.workflow, the manifest. YAML manifests are decoded
24// by package manifest, which normalises shorthands into this shape.
25type Workflow struct {
26 When []Trigger `json:"when"`
27 Engine string `json:"engine"`
28 Environment map[string]string `json:"environment,omitempty"`
29 Repos []Repo `json:"repos,omitempty"`
30 Steps []string `json:"steps"`
31 Actions map[string]Action `json:"actions"`
32}
33
34type Trigger struct {
35 Event []string `json:"event"`
36 Branch []string `json:"branch,omitempty"`
37 Tag []string `json:"tag,omitempty"`
38 Paths []string `json:"paths,omitempty"`
39}
40
41type Repo struct {
42 Repo string `json:"repo"`
43 Path string `json:"path"`
44 Rev string `json:"rev,omitempty"`
45 Env string `json:"env,omitempty"`
46}
47
48type Action struct {
49 Command string `json:"command"`
50 Dir string `json:"dir,omitempty"`
51 Label string `json:"label,omitempty"`
52 Gate *Gate `json:"gate,omitempty"`
53 Concurrency *Concurrency `json:"concurrency,omitempty"`
54 Manual bool `json:"manual,omitempty"`
55 Style string `json:"style,omitempty"`
56 Audited bool `json:"audited,omitempty"`
57}
58
59// Gate is run.rtw.ci.workflow#gate.
60type Gate struct {
61 Prompt string `json:"prompt,omitempty"`
62 Timeout string `json:"timeout,omitempty"`
63}
64
65type Concurrency struct {
66 Group string `json:"group"`
67 Supersede bool `json:"supersede,omitempty"`
68}
69
70// Result statuses.
71const (
72 Success = "success"
73 Warning = "warning"
74 Failure = "failure"
75)
76
77// Option.Then values.
78const (
79 ThenContinue = "continue"
80 ThenStop = "stop"
81)
82
83// Result is run.rtw.ci.step.result, written by an action to $CI_RESULT.
84type Result struct {
85 Version int `json:"version"`
86 Status string `json:"status"`
87 Summary string `json:"summary,omitempty"`
88 Next *Next `json:"next,omitempty"`
89 Gated bool `json:"gated,omitempty"`
90 Attachments []Attachment `json:"attachments,omitempty"`
91 Outputs json.RawMessage `json:"outputs,omitempty"`
92}
93
94// Next is a result's next field. On the wire it is "stop", an action name, a
95// single option object or an array of options; it always marshals to "stop"
96// or an array.
97type Next struct {
98 Stop bool
99 Options []Option
100}
101
102func (n Next) MarshalJSON() ([]byte, error) {
103 if n.Stop {
104 return []byte(`"stop"`), nil
105 }
106 if n.Options == nil {
107 return []byte(`[]`), nil
108 }
109 return json.Marshal(n.Options)
110}
111
112func (n *Next) UnmarshalJSON(b []byte) error {
113 b = bytes.TrimSpace(b)
114 *n = Next{}
115 switch {
116 case len(b) == 0:
117 return fmt.Errorf("empty next")
118 case b[0] == '"':
119 var s string
120 if err := json.Unmarshal(b, &s); err != nil {
121 return err
122 }
123 if s == "" {
124 return fmt.Errorf("next: empty action name")
125 }
126 if s == ThenStop {
127 n.Stop = true
128 return nil
129 }
130 n.Options = []Option{{Action: s}}
131 return nil
132 case b[0] == '{':
133 var o Option
134 if err := strictUnmarshal(b, &o); err != nil {
135 return fmt.Errorf("next: %w", err)
136 }
137 n.Options = []Option{o}
138 return nil
139 case b[0] == '[':
140 var raw []json.RawMessage
141 if err := json.Unmarshal(b, &raw); err != nil {
142 return fmt.Errorf("next: %w", err)
143 }
144 if len(raw) == 0 {
145 return fmt.Errorf("next: empty list (omit next for default routing, or use \"stop\")")
146 }
147 for i, r := range raw {
148 var o Option
149 if err := strictUnmarshal(r, &o); err != nil {
150 return fmt.Errorf("next[%d]: %w", i, err)
151 }
152 n.Options = append(n.Options, o)
153 }
154 return nil
155 }
156 return fmt.Errorf("next must be \"stop\", an action name, an option or a list of options")
157}
158
159// Option is run.rtw.ci.step.result#option.
160type Option struct {
161 Action string `json:"action"`
162 Then string `json:"then,omitempty"`
163 Label string `json:"label,omitempty"`
164 Prompt string `json:"prompt,omitempty"`
165 Inputs json.RawMessage `json:"inputs,omitempty"`
166}
167
168type Attachment struct {
169 Label string `json:"label,omitempty"`
170 Path string `json:"path"`
171 Type string `json:"type,omitempty"`
172}
173
174// Gate states.
175const (
176 GateWaiting = "waiting"
177 GateApproved = "approved"
178 GateDismissed = "dismissed"
179 GateExpired = "expired"
180 GateSuperseded = "superseded"
181)
182
183// GateRecord is run.rtw.ci.gate.
184type GateRecord struct {
185 Pipeline string `json:"pipeline,omitempty"`
186 Run string `json:"run"`
187 Workflow string `json:"workflow"`
188 Seq int `json:"seq"`
189 From string `json:"from,omitempty"`
190 Prompt string `json:"prompt,omitempty"`
191 Summary string `json:"summary,omitempty"`
192 Options []Option `json:"options"`
193 State string `json:"state"`
194 Choice string `json:"choice,omitempty"`
195 Actor string `json:"actor,omitempty"`
196 ExpiresAt string `json:"expiresAt,omitempty"`
197 CreatedAt string `json:"createdAt"`
198 DecidedAt string `json:"decidedAt,omitempty"`
199}
200
201// Transition reasons.
202const (
203 ReasonStart = "start"
204 ReasonDefault = "default"
205 ReasonNext = "next"
206 ReasonChoice = "choice"
207 ReasonManual = "manual"
208 ReasonStop = "stop"
209 ReasonDismiss = "dismiss"
210 ReasonFailure = "failure"
211 ReasonError = "error"
212 ReasonLimit = "limit"
213 ReasonExpired = "expired"
214 ReasonUntil = "until"
215 ReasonCancel = "cancelled"
216 ReasonSuper = "superseded"
217)
218
219// Transition is run.rtw.ci.transition.
220type Transition struct {
221 Pipeline string `json:"pipeline,omitempty"`
222 Run string `json:"run"`
223 Workflow string `json:"workflow"`
224 Seq int `json:"seq"`
225 From string `json:"from,omitempty"`
226 To string `json:"to,omitempty"`
227 Then string `json:"then,omitempty"`
228 Reason string `json:"reason"`
229 Detail string `json:"detail,omitempty"`
230 CreatedAt string `json:"createdAt"`
231}
232
233// Event kinds.
234const (
235 KindRun = "run"
236 KindCheckout = "checkout"
237 KindStart = "start"
238 KindLog = "log"
239 KindEnd = "end"
240 KindResult = "result"
241 KindTransition = "transition"
242 KindGate = "gate"
243)
244
245// RunEvent is run.rtw.ci.event, one line of events.ndjson.
246type RunEvent struct {
247 Seq int `json:"seq"`
248 Time string `json:"time"`
249 Run string `json:"run"`
250 Kind string `json:"kind"`
251 Action string `json:"action,omitempty"`
252 Attempt int `json:"attempt,omitempty"`
253 Stream string `json:"stream,omitempty"`
254 Content string `json:"content,omitempty"`
255 Command string `json:"command,omitempty"`
256 ExitCode *int `json:"exitCode,omitempty"`
257 Status string `json:"status,omitempty"`
258 Repo string `json:"repo,omitempty"`
259 Rev string `json:"rev,omitempty"`
260 Path string `json:"path,omitempty"`
261 Result *Result `json:"result,omitempty"`
262 Transition *Transition `json:"transition,omitempty"`
263 Gate *GateRecord `json:"gate,omitempty"`
264}
265
266// strictUnmarshal decodes JSON rejecting unknown fields, so a typo in a
267// result ("summray") is a contract error rather than silently ignored.
268func strictUnmarshal(b []byte, v any) error {
269 d := json.NewDecoder(bytes.NewReader(b))
270 d.DisallowUnknownFields()
271 if err := d.Decode(v); err != nil {
272 return err
273 }
274 if d.More() {
275 return fmt.Errorf("trailing data after JSON value")
276 }
277 return nil
278}
279
280// StrictUnmarshal is strictUnmarshal for other packages.
281func StrictUnmarshal(b []byte, v any) error { return strictUnmarshal(b, v) }