builder_build reads the builds and asked states, and the tools bundle follows the catalog half itself (registrations for the watched branches, revision per branch) for `newest`; the manifest names the tool and the two seat verbs. Builds: design 47 G §589, A-87 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
223 lines
7.1 KiB
Go
223 lines
7.1 KiB
Go
package build
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
|
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
|
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
|
)
|
|
|
|
// **builder_build answers a module's build keys whole** (design 47 G §589, A-87): with `fingerprint` that key whole, with
|
|
// `module` alone the module's keys newest first, at most MostBuilds, each with its fingerprint whole, its state, revision and
|
|
// since when, its step and node, its error whole, event-builds and the open ask (§568), and `newest` naming each watched branch
|
|
// whose newest catalog graph entry of the module names that fingerprint, read from the catalog half by the tools bundle.
|
|
|
|
// BuildTool is the builder's read tool of a module's build keys.
|
|
const BuildTool = "builder_build"
|
|
|
|
// MostBuilds is how many keys of a module builder_build answers by module alone.
|
|
const MostBuilds = 20
|
|
|
|
// BuildRow is one build key as builder_build answers it.
|
|
type BuildRow struct {
|
|
Key string `json:"key"`
|
|
Fingerprint string `json:"fingerprint"`
|
|
State string `json:"state"`
|
|
Revision uint64 `json:"revision,omitempty"`
|
|
Since time.Time `json:"since,omitzero"`
|
|
Step string `json:"step,omitempty"`
|
|
Node string `json:"node,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
EventBuilds int `json:"event-builds"`
|
|
// Ask is the open ask of the operator about the key (§568).
|
|
Ask *OpenAsk `json:"ask,omitempty"`
|
|
// Newest names each watched branch whose newest catalog graph entry of the module names this fingerprint.
|
|
Newest []string `json:"newest,omitempty"`
|
|
}
|
|
|
|
// OpenAsk is the ask the operator has not answered.
|
|
type OpenAsk struct {
|
|
ID string `json:"id"`
|
|
Asked string `json:"asked"`
|
|
Expires string `json:"expires"`
|
|
}
|
|
|
|
// BuildAnswer is what builder_build answers.
|
|
type BuildAnswer struct {
|
|
Module string `json:"module"`
|
|
Builds []BuildRow `json:"builds"`
|
|
// Newest is the fingerprint of the module's newest catalog graph entry on each watched branch.
|
|
Newest map[string]string `json:"newest-entries,omitempty"`
|
|
// Note says what no key shows: that none of the keys is the newest entry's, naming its fingerprint.
|
|
Note string `json:"note,omitempty"`
|
|
}
|
|
|
|
// BuildReader reads what builder_build answers.
|
|
type BuildReader struct {
|
|
Builds StateRead
|
|
// Entry is a key's value with its revision and when it was written; nil leaves both out.
|
|
Entry func(key string) (json.RawMessage, uint64, time.Time, error)
|
|
// Asked is the builder's state asked, which holds the asks the operator was asked.
|
|
Asked StateRead
|
|
// Newest is the fingerprint of the module's newest catalog graph entry on each watched branch, read from the catalog half.
|
|
Newest func(module string) (map[string]string, error)
|
|
Steps StuckReader
|
|
index map[string]map[string]string
|
|
}
|
|
|
|
// Build answers for module, and for one fingerprint when fingerprint is not empty.
|
|
func (r *BuildReader) Build(module, fingerprint string) (BuildAnswer, error) {
|
|
out := BuildAnswer{Module: module, Builds: []BuildRow{}}
|
|
if module == "" {
|
|
return out, errors.New("name the module (module)")
|
|
}
|
|
all, err := r.Builds.Keys()
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
var rows []BuildRow
|
|
for _, key := range all {
|
|
mod, fp, err := builds.ParseBuilderBuildKey(key)
|
|
if err != nil || mod != module || (fingerprint != "" && fp != fingerprint) {
|
|
continue
|
|
}
|
|
raw, rev, at, err := r.read(key)
|
|
if err != nil {
|
|
return out, fmt.Errorf("%s cannot be read: %w", key, err)
|
|
}
|
|
if raw == nil {
|
|
continue
|
|
}
|
|
v, err := builds.DecodeBuilderBuild(raw)
|
|
if err != nil {
|
|
return out, fmt.Errorf("%s holds what is not a build: %w", key, err)
|
|
}
|
|
row := BuildRow{Key: key, Fingerprint: fp, State: v.State, Revision: rev, Since: at, Step: v.Step, Error: v.Error, EventBuilds: v.EventBuilds}
|
|
if _, _, node, _, ok := builds.TransportFaultOf(v.Error); ok && node != "no node" {
|
|
row.Node = node
|
|
}
|
|
if row.Node == "" && v.State == builds.BuildBuilding {
|
|
row.Node = r.stepNode(v)
|
|
}
|
|
rows = append(rows, row)
|
|
}
|
|
if fingerprint != "" && len(rows) == 0 {
|
|
return out, fmt.Errorf("the builder holds no build key of %s at %s", module, fingerprint)
|
|
}
|
|
sort.Slice(rows, func(i, j int) bool {
|
|
if !rows[i].Since.Equal(rows[j].Since) {
|
|
return rows[i].Since.After(rows[j].Since)
|
|
}
|
|
return rows[i].Key > rows[j].Key
|
|
})
|
|
if len(rows) > MostBuilds {
|
|
rows = rows[:MostBuilds]
|
|
}
|
|
asks := r.openAsks()
|
|
for i := range rows {
|
|
rows[i].Ask = asks[rows[i].Key]
|
|
}
|
|
if r.Newest != nil {
|
|
newest, err := r.Newest(module)
|
|
if err != nil {
|
|
return out, fmt.Errorf("the catalog half's newest entry of %s cannot be read: %w", module, err)
|
|
}
|
|
out.Newest = newest
|
|
named := false
|
|
for i := range rows {
|
|
for branch, fp := range newest {
|
|
if fp == rows[i].Fingerprint {
|
|
rows[i].Newest, named = append(rows[i].Newest, branch), true
|
|
}
|
|
}
|
|
slices.Sort(rows[i].Newest)
|
|
}
|
|
if !named && len(newest) > 0 {
|
|
var says []string
|
|
for branch, fp := range newest {
|
|
says = append(says, branch+" names "+fp)
|
|
}
|
|
slices.Sort(says)
|
|
out.Note = "no build key of " + module + " is the newest entry's: " + strings.Join(says, "; ")
|
|
}
|
|
}
|
|
out.Builds = append(out.Builds, rows...)
|
|
return out, nil
|
|
}
|
|
|
|
// stepNode is the node a building key's step runs on, read from its asks' steps in the build modules' steps; empty when none runs.
|
|
func (r *BuildReader) stepNode(v builds.BuilderBuild) string {
|
|
if len(v.Asks) == 0 || len(r.Steps.Steps) == 0 {
|
|
return ""
|
|
}
|
|
if r.index == nil {
|
|
idx, err := r.Steps.stepIndex()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
r.index = idx
|
|
}
|
|
languages := make([]string, 0, len(v.Asks))
|
|
for l := range v.Asks {
|
|
languages = append(languages, l)
|
|
}
|
|
slices.Sort(languages)
|
|
for _, l := range languages {
|
|
bm, _ := buildstep.BuildModuleOf(l)
|
|
st, ok := r.Steps.Steps[bm]
|
|
k, found := r.index[bm][v.Asks[l]]
|
|
if !ok || !found {
|
|
continue
|
|
}
|
|
if raw, _, err := st.Get(k); err == nil && raw != nil {
|
|
if step, err := buildstep.DecodeStep(raw); err == nil && step.Node != "" {
|
|
return step.Node
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (r *BuildReader) read(key string) (json.RawMessage, uint64, time.Time, error) {
|
|
if r.Entry != nil {
|
|
return r.Entry(key)
|
|
}
|
|
raw, at, err := r.Builds.Get(key)
|
|
return raw, 0, at, err
|
|
}
|
|
|
|
// openAsks are the asks kept in the state asked that nobody answered yet, by the key each is about.
|
|
func (r *BuildReader) openAsks() map[string]*OpenAsk {
|
|
out := map[string]*OpenAsk{}
|
|
if r.Asked.Keys == nil {
|
|
return out
|
|
}
|
|
keys, err := r.Asked.Keys()
|
|
if err != nil {
|
|
return out
|
|
}
|
|
for _, k := range keys {
|
|
raw, _, err := r.Asked.Get(k)
|
|
if err != nil || raw == nil {
|
|
continue
|
|
}
|
|
var kept occ.Kept
|
|
if json.Unmarshal(raw, &kept) != nil || kept.Ended != "" || kept.Acted || kept.Ask.About == "" {
|
|
continue
|
|
}
|
|
if t, err := occ.Time(kept.Ask.Expires); err == nil && !t.IsZero() && time.Now().After(t) {
|
|
continue
|
|
}
|
|
out[kept.Ask.About] = &OpenAsk{ID: kept.Ask.ID, Asked: kept.Asked.UTC().Format(time.RFC3339), Expires: kept.Ask.Expires}
|
|
}
|
|
return out
|
|
}
|