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) }