package delivery import ( "encoding/json" "fmt" "regexp" "strings" "time" ) // StepsState is the name of the state a build module keeps its long steps in; its bucket is `_steps`, built // by StepsBucket (novox/hq ADR 0469 clause 2; issue 1471). A tool call is cut at 30 s, so a fetch, a test level or a // build answers at once with a step id and writes its progress and its end to the key `.`. const StepsState = "steps" // StepsBucket is the bucket of a build module's steps. func StepsBucket(module string) string { return module + "_" + StepsState } // The states of a step. A step ends in passed, failed or refused; its key is written for each of them, and that // write is the event the step's reader waits on. const ( StepRunning = "running" StepPassed = "passed" StepFailed = "failed" StepRefused = "refused" ) var stepIDOK = regexp.MustCompile(`^[a-z0-9]+$`) func checkStepID(id string) error { if !stepIDOK.MatchString(id) { return fmt.Errorf("delivery: the step id %q is not lower-case letters and digits", id) } return nil } // StepKey is the key of a node's step: `.`. It refuses a node that is not a name and an id that is not // lower-case letters and digits (one key token, never a dot). func StepKey(node, id string) (string, error) { if err := checkName("node", node); err != nil { return "", err } if err := checkStepID(id); err != nil { return "", err } return node + "." + id, nil } // ParseStepKey is the inverse of StepKey. func ParseStepKey(key string) (node, id string, err error) { node, id, ok := strings.Cut(key, ".") if !ok { return "", "", fmt.Errorf("delivery: %q is not a step key, .", key) } if _, err := StepKey(node, id); err != nil { return "", "", fmt.Errorf("delivery: %q is not a step key: %w", key, err) } return node, id, nil } // Step is the value of a steps key: which tool runs on which node, how far it is, when it started and ended (UTC, as // the toolchains' At), why it was refused or failed, and what it returned. type Step struct { V int `json:"v"` ID string `json:"id"` Tool string `json:"tool"` Node string `json:"node"` State string `json:"state"` Started time.Time `json:"started"` Ended time.Time `json:"ended,omitzero"` Reason string `json:"reason,omitempty"` Result json.RawMessage `json:"result,omitempty"` } func (s Step) check() error { if err := checkStepID(s.ID); err != nil { return err } if err := checkName("node", s.Node); err != nil { return err } if s.Tool == "" { return fmt.Errorf("delivery: a step needs its tool") } switch s.State { case StepRunning, StepPassed, StepFailed, StepRefused: default: return fmt.Errorf("delivery: the step state %q is not running, passed, failed or refused", s.State) } if len(s.Result) > 0 && !json.Valid(s.Result) { return fmt.Errorf("delivery: the step's result is not JSON") } return nil } // EncodeStep is s as JSON at Version; it refuses an id that is not one key token, a node that is not a name, an empty // tool, a state outside the four and a result that is not JSON. func EncodeStep(s Step) ([]byte, error) { if err := s.check(); err != nil { return nil, err } s.V = Version return json.Marshal(s) } // DecodeStep reads a steps value; it refuses an unknown version (ErrUnknownVersion) and what EncodeStep refuses. func DecodeStep(data []byte) (Step, error) { var s Step if err := decode(data, &s); err != nil { return Step{}, err } if err := s.check(); err != nil { return Step{}, err } return s, nil }