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/modules/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.BuildSeat(l) // the steps are by build seat (StuckReader.Steps) 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 }