Files
mesh-delivery/modules/mesh-delivery-builder/builds/log.go
T

177 lines
8.6 KiB
Go

package builds
import (
"encoding/json"
"fmt"
"sort"
"time"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// The builder's log (clause 1; the lead and the operator, 2026-10-29): the builder's own module log (ADR 0297) `build-log`,
// kept 30 days (max-age-days), one entry under the key `<module>` when a build of a fingerprint starts and one when it
// ends, so the newest entry of a key is the module's newest start or end. The builder's tools read it through the
// runtime's log verbs; another module reads it by naming BuildLogRead in its manifest's `reads`: a bundle through the
// runtime (stdio.Log(BuildLogRead)), a process on its own credential (process.Conn.ReadLog), each by newest per key,
// from a time, or following new entries. The catalogue module asks the builder's tool BuildsSinceTool instead.
const (
BuilderLog = "build-log"
BuildLogRead = BuilderModule + "." + BuilderLog
BuildStartedEvent = "build.started"
BuildEndedEvent = "build.ended"
// BuildSaidEvent is a build's narration between its start and its end (design 51: a build narrates its publish,
// asked, waiting on a seat, stored, refused, at least every five minutes while it waits): its module, fingerprint,
// line and why.
BuildSaidEvent = "build.said"
// BuilderLogDays is how long the log keeps its entries.
BuilderLogDays = 30
// BuildsSinceTool is the builder's tool answering every ended build since a time, in order (clause 9): its
// argument `since` (RFC 3339), its answer BuildsSince.
BuildsSinceTool = "builder_builds_since"
)
// BuildLogKey is the key of a module's entries in the builder's log: the module's name, refused when a log key could not
// hold it.
func BuildLogKey(module string) (string, error) {
if err := keys.CheckName("module", module); err != nil {
return "", err
}
return module, nil
}
// BuildLogEntry is one entry of the builder's log: the module, the fingerprint, the source revision, where the module is
// built from (its repository, its directory there, the commit and the ref), the build node and the time; at the end,
// the outcome, the manifest built (its digest, read from the indexer's state `manifests` under ManifestsKey), what it was
// built against (the bases' references, as the controller's built-before said them) and each artifact made
// (BuiltArtifact, the done key's: its name, kind, digest and reference). The repository, directory and commit are what
// the indexer's PathOf, the outside-built registrations and the assigner's running commits read; the manifest, against
// and made what the catalogue module registers a build from (the lead, 2026-10-29).
type BuildLogEntry struct {
V int `json:"v"`
Event string `json:"event"`
Module string `json:"module"`
FP string `json:"fp"`
Source string `json:"source"`
Repository string `json:"repository"`
Directory string `json:"directory"`
Commit string `json:"commit"`
Ref string `json:"ref,omitempty"`
Node string `json:"node"`
At time.Time `json:"at"`
Outcome string `json:"outcome,omitempty"`
Manifest string `json:"manifest,omitempty"`
Against []string `json:"against,omitempty"`
Made []BuiltArtifact `json:"made,omitempty"`
// Imports are, at a build's end, the mesh packages (import paths) the built programs import, their tests left out,
// sorted and each once, as the build module answered them (BuildAnswer.Imports; novox/hq ADR 0593 §2): the indexer
// keeps them on the module (KeptInputs2.Imports) when the build's fingerprint is the module's.
Imports []string `json:"imports,omitempty"`
// OutOfStep are, at a build's end, the generated files its test step found out of step with what their generator
// writes, as the build module answered them (its test step's `out-of-step`): the indexer raises a finding naming them
// for a trunk build, and only a clean build of the same module clears it. A start or a narration names none.
OutOfStep []string `json:"out-of-step,omitempty"`
// Line is a narration's words, Why what it narrates (asked, waiting, stored, refused): a build.said entry's alone.
Line string `json:"line,omitempty"`
Why string `json:"why,omitempty"`
}
// BuildsSince is the answer of BuildsSinceTool: every ended build since a time, oldest first, and whether the log held
// more than one answer carries (then ask again from the last one's time).
type BuildsSince struct {
Since time.Time `json:"since"`
Builds []BuildLogEntry `json:"builds"`
More bool `json:"more,omitempty"`
}
// NewestPerModule is each module's newest entry of entries (one kind, read from the last per subject or whole), by
// module: the reader's helper for the five newest and the running commits.
func NewestPerModule(entries []BuildLogEntry) map[string]BuildLogEntry {
out := map[string]BuildLogEntry{}
for _, e := range entries {
if have, ok := out[e.Module]; !ok || e.At.After(have.At) {
out[e.Module] = e
}
}
return out
}
// EndedSince is, of entries, the ended builds at or after since, oldest first: the reader's helper for history and for
// BuildsSinceTool's answer.
func EndedSince(entries []BuildLogEntry, since time.Time) []BuildLogEntry {
var out []BuildLogEntry
for _, e := range entries {
if e.Event == BuildEndedEvent && !e.At.Before(since) {
out = append(out, e)
}
}
sort.SliceStable(out, func(i, j int) bool { return out[i].At.Before(out[j].At) })
return out
}
// EncodeBuildLogEntry is e as JSON at Version; it refuses an entry a reader could not place.
func EncodeBuildLogEntry(e BuildLogEntry) ([]byte, error) {
e.V = keys.Version
switch {
case e.Event == BuildSaidEvent:
switch {
case e.Module == "" || e.FP == "" || e.At.IsZero() || e.Line == "":
return nil, fmt.Errorf("builds: a build's narration names its module, fingerprint, line and time")
case e.Outcome != "" || len(e.Made) > 0 || e.Manifest != "" || len(e.Against) > 0 || len(e.Imports) > 0 || len(e.OutOfStep) > 0:
return nil, fmt.Errorf("builds: a build's narration names no outcome, manifest, base, artifact, import or file out of step")
}
return json.Marshal(e)
case e.Event != BuildStartedEvent && e.Event != BuildEndedEvent:
return nil, fmt.Errorf("builds: a build log entry is %s, %s or %s, not %q", BuildStartedEvent, BuildSaidEvent, BuildEndedEvent, e.Event)
case e.Line != "" || e.Why != "":
return nil, fmt.Errorf("builds: a build's start or end carries no narration's line")
case e.Module == "" || e.FP == "" || e.Node == "" || e.At.IsZero() || e.Repository == "" || e.Commit == "":
return nil, fmt.Errorf("builds: a build log entry names its module, fingerprint, repository, commit, node and time")
case e.Event == BuildEndedEvent && e.Outcome == "":
return nil, fmt.Errorf("builds: a build's end names its outcome")
case e.Event == BuildStartedEvent && (e.Outcome != "" || len(e.Made) > 0 || e.Manifest != "" || len(e.Against) > 0 || len(e.Imports) > 0 ||
len(e.OutOfStep) > 0):
return nil, fmt.Errorf("builds: a build's start names no outcome, manifest, base, artifact, import or file out of step")
}
if err := trunk.CheckImports(e.Imports); err != nil {
return nil, err
}
return json.Marshal(e)
}
// DecodeBuildLogEntry reads one entry.
func DecodeBuildLogEntry(data []byte) (BuildLogEntry, error) {
var e BuildLogEntry
if err := json.Unmarshal(data, &e); err != nil {
return BuildLogEntry{}, fmt.Errorf("builds: a build log entry is not readable: %w", err)
}
if e.V != keys.Version {
return BuildLogEntry{}, fmt.Errorf("builds: a build log entry of version %d, and this reads %d", e.V, keys.Version)
}
return e, nil
}
// BuildDuration is a build's duration, its end less its start (clause 1): false unless both are the same module's
// build of the same fingerprint, the start first.
func BuildDuration(start, end BuildLogEntry) (time.Duration, bool) {
if start.Event != BuildStartedEvent || end.Event != BuildEndedEvent || start.Module != end.Module || start.FP != end.FP ||
end.At.Before(start.At) {
return 0, false
}
return end.At.Sub(start.At), true
}
// IsBuildEntry says whether an entry of the builder's log is a build's start or end, read from its event alone, so an entry of
// another kind (a build's narration, build.said) is passed over before its form is read. One whose event cannot be read is
// taken as a build's, so its decoding says what is wrong with it.
func IsBuildEntry(value []byte) bool {
var e struct {
Event *string `json:"event"`
}
if json.Unmarshal(value, &e) != nil || e.Event == nil {
return true
}
return *e.Event == BuildStartedEvent || *e.Event == BuildEndedEvent
}