190 lines
6.2 KiB
Go
190 lines
6.2 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"
|
|
StepPassed = "passed"
|
|
StepFailed = "failed"
|
|
StepRefused = "refused"
|
|
)
|
|
|
|
// 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"`
|
|
What string `json:"what,omitempty"`
|
|
Phase string `json:"phase,omitempty"`
|
|
PhaseAt time.Time `json:"phase-at,omitzero"`
|
|
Owner string `json:"owner,omitempty"`
|
|
}
|
|
|
|
var (
|
|
nameOK = regexp.MustCompile(`^[-/_=a-zA-Z0-9]+$`)
|
|
stepIDOK = regexp.MustCompile(`^[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 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("buildstep: %q is not a step key, <node>.<id>", key)
|
|
}
|
|
if _, err := StepKey(node, id); err != nil {
|
|
return "", "", fmt.Errorf("buildstep: %q is not a step key: %w", key, err)
|
|
}
|
|
return 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, StepPassed, StepFailed, StepRefused:
|
|
default:
|
|
return fmt.Errorf("buildstep: 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("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"`
|
|
}
|