A step's key keeps its answer only within what the steps state holds per value (kvlimit.ModuleStateValueBytes less the headers' room). Over it, finish keeps the answer with the holder's KeepOutput as the object output.<ask> in its outputs store and the key names it in output-object: a check's whole answer, its result emptied; a run's output, the key keeping the run answer without it. An answer that cannot be kept ends the key failed, saying why. The Puller says step-ended from the end finish wrote, so the key and the event say one outcome: a run whose program exited other than 0 ends its key failed as well, its answer kept; step-ended names the object finish kept instead of putting it twice. Step.Whole reads a key's object back through the seat's verb output on the step's node. The part build-step's Step gains output-object, the verb output says it answers a step's answer; fixtures and the generated contracts follow. Builds: design 47 G A-290, A-254, §467 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
231 lines
8.7 KiB
Go
231 lines
8.7 KiB
Go
package buildstep
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// The layouts this package reads and writes are the part `build-step`'s, shared by the three build seats'
|
|
// contracts in mesh-delivery-contracts (novox/hq ADR 0634 §6): this package imports no seat's contract, so the Go,
|
|
// image and Node build modules each use it with their own seat's, and the layout is held equal by the part's
|
|
// fixtures (a step's value decodes in both).
|
|
|
|
// Version is the layout version of a step value; a reader refuses another.
|
|
const Version = 1
|
|
|
|
// ErrUnknownVersion is what DecodeStep returns for a value whose version is missing or not Version.
|
|
var ErrUnknownVersion = errors.New("buildstep: unknown value version")
|
|
|
|
// The states of a step.
|
|
const (
|
|
StepRunning = "running"
|
|
// StepAgain is a step that ended on a transport fault with runs left: the bus gives its ask again after AgainAt
|
|
// (ADR 0664 §1). It is not an end (Ended), and nothing acknowledges the ask.
|
|
StepAgain = "again"
|
|
StepPassed = "passed"
|
|
StepFailed = "failed"
|
|
StepRefused = "refused"
|
|
// StepDropped is a step ended by the seat's verb drop: its asker no longer waited for it (ADR 0664 §4). The cause its
|
|
// caller named is its reason. It is an end, and no failure of the source.
|
|
StepDropped = "dropped"
|
|
)
|
|
|
|
// StepBound is how long an asker waits for one ask's step once a build node took it (30 minutes): past it the asker
|
|
// has stopped waiting, so a key still running is nobody's.
|
|
const StepBound = 30 * time.Minute
|
|
|
|
// Step is one value of a build module's `steps` state.
|
|
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"`
|
|
// OutputObject names the object in the build module's outputs store (OutputObjectName) that holds the step's answer
|
|
// when the whole value would be over what the steps state holds (design 47 G A-290): a check's whole answer, Result
|
|
// then empty; a run's output, Result the run's answer without it. A reader reads the answer through the seat's verb
|
|
// output (Step.Answer).
|
|
OutputObject string `json:"output-object,omitempty"`
|
|
What string `json:"what,omitempty"`
|
|
Phase string `json:"phase,omitempty"`
|
|
PhaseAt time.Time `json:"phase-at,omitzero"`
|
|
Owner string `json:"owner,omitempty"`
|
|
// Run is which run of the ask this is, counted by the bus's delivery count (1 for the first); AgainAt when the bus
|
|
// gives it again (the state again); Passing says the step ended on a transport fault, so a failed end after the
|
|
// last run says so (ADR 0664 §1, §2).
|
|
Run int `json:"run,omitempty"`
|
|
AgainAt time.Time `json:"again-at,omitzero"`
|
|
Passing bool `json:"passing,omitempty"`
|
|
}
|
|
|
|
// Ended says a step's state is an end: passed, failed, refused or dropped. A step running, or ended on a transport fault with runs
|
|
// left (StepAgain), is not.
|
|
func Ended(s Step) bool { return s.State != StepRunning && s.State != StepAgain && s.State != "" }
|
|
|
|
var (
|
|
nameOK = regexp.MustCompile(`^[-/_=a-zA-Z0-9]+$`)
|
|
stepIDOK = regexp.MustCompile(`^[a-z0-9]+$`)
|
|
askerOK = regexp.MustCompile(`^[a-z][a-z0-9-]*$`)
|
|
askIDOK = regexp.MustCompile(`^[0-9a-z]{8,9}[0-9a-f]{6}$`)
|
|
)
|
|
|
|
func checkNode(node string) error {
|
|
if node == "" || !nameOK.MatchString(node) {
|
|
return fmt.Errorf("buildstep: the node name %q is empty or holds a dot, a wildcard, whitespace or a character a key refuses", node)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// StepKey is the key of a step in the steps state: `<node>.<id>`.
|
|
func StepKey(node, id string) (string, error) {
|
|
if err := checkNode(node); err != nil {
|
|
return "", err
|
|
}
|
|
if !stepIDOK.MatchString(id) {
|
|
return "", fmt.Errorf("buildstep: the step id %q is not lower-case letters and digits", id)
|
|
}
|
|
return node + "." + id, nil
|
|
}
|
|
|
|
// ParseStepKey reads both forms of a step's key (design 47 G §484, §649): `<asker>.<node>.<id>`, and main's `<node>.<id>`,
|
|
// which answers no asker (an empty asker). A third form is refused.
|
|
func ParseStepKey(key string) (asker, node, id string, err error) {
|
|
parts := strings.Split(key, ".")
|
|
switch len(parts) {
|
|
case 2:
|
|
node, id = parts[0], parts[1]
|
|
case 3:
|
|
asker, node, id = parts[0], parts[1], parts[2]
|
|
if !askerOK.MatchString(asker) {
|
|
return "", "", "", fmt.Errorf("buildstep: %q is not a step key: %q is not an asker", key, asker)
|
|
}
|
|
default:
|
|
return "", "", "", fmt.Errorf("buildstep: %q is not a step key, <asker>.<node>.<id> or <node>.<id>", key)
|
|
}
|
|
if _, err := StepKey(node, id); err != nil {
|
|
return "", "", "", fmt.Errorf("buildstep: %q is not a step key: %w", key, err)
|
|
}
|
|
return asker, node, id, nil
|
|
}
|
|
|
|
// ParseAskStepKey is ParseStepKey for the key of a pulled ask's step, whose id is the ask's (base-36 milliseconds
|
|
// and six hex digits).
|
|
func ParseAskStepKey(key string) (node, askID string, err error) {
|
|
_, node, askID, err = ParseStepKey(key)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
if !askIDOK.MatchString(askID) {
|
|
return "", "", fmt.Errorf("buildstep: %q is not an ask's step key: %q is not an ask id", key, askID)
|
|
}
|
|
return node, askID, nil
|
|
}
|
|
|
|
func (s Step) check() error {
|
|
if _, err := StepKey(s.Node, s.ID); err != nil {
|
|
return err
|
|
}
|
|
if s.Tool == "" {
|
|
return fmt.Errorf("buildstep: a step needs its tool")
|
|
}
|
|
switch s.State {
|
|
case StepRunning, StepAgain, StepPassed, StepFailed, StepRefused, StepDropped:
|
|
default:
|
|
return fmt.Errorf("buildstep: the step state %q is not running, again, passed, failed, refused or dropped", s.State)
|
|
}
|
|
if len(s.Result) > 0 && !json.Valid(s.Result) {
|
|
return fmt.Errorf("buildstep: the step's result is not JSON")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EncodeStep is a step as its value, refusing one that is not whole.
|
|
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 probe struct {
|
|
V *int `json:"v"`
|
|
}
|
|
if err := json.Unmarshal(data, &probe); err != nil {
|
|
return Step{}, fmt.Errorf("buildstep: value is not JSON: %w", err)
|
|
}
|
|
if probe.V == nil || *probe.V != Version {
|
|
return Step{}, fmt.Errorf("%w: this reader reads %d", ErrUnknownVersion, Version)
|
|
}
|
|
var s Step
|
|
if err := json.Unmarshal(data, &s); err != nil {
|
|
return Step{}, fmt.Errorf("buildstep: value does not fit its shape: %w", err)
|
|
}
|
|
if err := s.check(); err != nil {
|
|
return Step{}, err
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
var (
|
|
askCommitOK = regexp.MustCompile(`^[0-9a-f]{40}$`)
|
|
// PackageSemver is a Go semantic version as a delivery group's check names a tagged version (`v0.1.15-feat-x.2`).
|
|
PackageSemver = regexp.MustCompile(`^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$`)
|
|
)
|
|
|
|
// PackageVersions are the versions of the mesh packages a build or a check builds against, by package (a Go module's
|
|
// path): each a full commit or a Go semantic version.
|
|
type PackageVersions map[string]string
|
|
|
|
// Check refuses a package that is no module path or a version that is neither a full commit nor a Go semantic
|
|
// version, naming it.
|
|
func (p PackageVersions) Check() error {
|
|
for pkg, version := range p {
|
|
if pkg == "" || strings.ContainsAny(pkg, " \t\n@") || strings.HasPrefix(pkg, "/") {
|
|
return fmt.Errorf("buildstep: the package %q is no module path", pkg)
|
|
}
|
|
if !askCommitOK.MatchString(version) && !PackageSemver.MatchString(version) {
|
|
return fmt.Errorf("buildstep: the package %s names the version %q, which is neither a full commit nor a Go semantic version (v0.1.15-feat-x.2)", pkg, version)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Ask is the part of a build seat's ask that AskWhat reads: the layout is the part's `StepAsk`.
|
|
type Ask struct {
|
|
Kind string `json:"kind"`
|
|
Build *struct {
|
|
Module string `json:"module"`
|
|
FP string `json:"fp"`
|
|
Fetch map[string]any `json:"fetch"`
|
|
} `json:"build"`
|
|
Check *struct {
|
|
Fetch map[string]any `json:"fetch"`
|
|
Levels []string `json:"levels"`
|
|
Run string `json:"run"`
|
|
} `json:"check"`
|
|
Compose *struct {
|
|
Fetch map[string]any `json:"fetch"`
|
|
} `json:"compose"`
|
|
Run *RunJob `json:"run"`
|
|
}
|
|
|
|
// CheckAskID refuses an id that is no ask's: base-36 milliseconds and six hex digits, as NewID gives. Its form keeps it apart from
|
|
// a module's or a node's name in a key.
|
|
func CheckAskID(id string) error {
|
|
if !askIDOK.MatchString(id) {
|
|
return fmt.Errorf("buildstep: %q is not an ask id (base-36 milliseconds and six hex digits)", id)
|
|
}
|
|
return nil
|
|
}
|