117 lines
3.6 KiB
Go
117 lines
3.6 KiB
Go
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 `<module>_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 `<node>.<step id>`.
|
|
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: `<node>.<id>`. 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, <node>.<id>", 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
|
|
}
|