A check that failed for a part of the mesh not heard was judged again at every change of the nodes. It now says could not run, naming the part, and is kept in the checker's own state could-not-run; only the running of that part asks it again, once per heads and part at one fingerprint (PartRunning). Not asked again within an hour it is the finding check-could-not-run, which clears when the heads are checked again, replaced, or their pull request closes. checker_status reads the state. The node-engine seat's running event is wired once its definition lands. Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ Builds: design 47 G §253, §315
293 lines
12 KiB
Go
293 lines
12 KiB
Go
package check
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
|
|
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
|
)
|
|
|
|
// Pull is one pull request the checker judges: its head, the trunk head of its repository it is merged onto and the
|
|
// modules the indexer says it touches, with the fingerprints the indexer wrote for them.
|
|
type Pull struct {
|
|
Repository string
|
|
Number int
|
|
Head string
|
|
Trunk string
|
|
Modules []string
|
|
Fps map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue
|
|
}
|
|
|
|
// Case is one check: a candidate's pull requests that are judged (those of a repository whose trunk is not known are left
|
|
// out; hq is judged as any repository since R4g, its checks Go tests: ADR 0527), the trunk head of each repository and the key of the verdict.
|
|
type Case struct {
|
|
// AskedAt is the running that asked again a check of these heads that could not run (PartRunning), "" otherwise.
|
|
AskedAt string
|
|
// Key is the verdict key, `<group>.<heads hash>`.
|
|
Key string
|
|
Group string
|
|
HeadsHash string
|
|
// Heads is the heads hash the gate matches an Approve's members by (GateHeads): a verdict carries it.
|
|
Heads string
|
|
Pulls []Pull
|
|
// Trunk is the trunk head of each repository of Pulls.
|
|
Trunk map[string]string
|
|
// Left says why pull requests of the candidate are not judged (a repository with no known trunk); empty when all are.
|
|
Left []string
|
|
// Queued is when the checker took the case up, for the time of its queue.
|
|
Queued time.Time
|
|
// Seats is the seat list the checking program made, the checker's reader's list with the heads' definitions laid over
|
|
// it, which the manifest validator's and the node-engine composer's requests carry (ADR 0636 §1, §4); none until it is
|
|
// made.
|
|
Seats []json.RawMessage
|
|
// Try narrows a try's check (its level, its tests); nil for a pull request's.
|
|
Try *TryOptions
|
|
// Judged are the check's own judge's verdicts on the manifests the change brings, by module, as the seat mesh-catalog's
|
|
// verdict value (judgeManifests): the composer's request carries them in place of the running indexer's (ADR 0578 §2).
|
|
Judged map[string]json.RawMessage
|
|
|
|
notesMu sync.Mutex
|
|
// warnings are, by pull request, what part of its check did not run (Case.warn).
|
|
warnings map[string]string
|
|
// seriesOf are, by pull request, the changes of a package series it makes (series.go), named in its status line.
|
|
seriesOf map[string][]string
|
|
// seriesChanges are, by pull request, the same changes as structures (series.go), named in the verdict and its event.
|
|
seriesChanges map[string][]meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem
|
|
notes []string
|
|
// asks are the ids of the asks the check put on the build seats' streams, in order: a try names them (Try.Asks).
|
|
asks []string
|
|
// Watch, when set, hears each ask the check puts, as it is put (step nil), and each value its step key takes while
|
|
// the check waits for it: the node that took it, the phase it says, its end. A try writes them into its record, so a
|
|
// running try names its asks and where each stands (issue 1890). Called from the check's goroutines; it must not block
|
|
// for long.
|
|
Watch func(askID string, step *buildstep.Step)
|
|
// Progress says what the check is doing, so a check that ran past its bound names the step it was stuck at.
|
|
Progress *Progress
|
|
// Wait says what a whole try waits for now ("" when nothing); the try keeps it for try-read. Nil for a pull request's check.
|
|
Wait func(why string)
|
|
// Kind is what asked the check (KindNew, KindNodes, KindTrunk): a carry after a trunk or sibling move is asked on the
|
|
// build seats' recheck stream, every other check on check (ADR 0536 §4, ADR 0548 §5).
|
|
Kind string
|
|
// CarriedFrom are, for the carry of a verdict that passed, the trunk and sibling commits it was judged on (ADR 0548 §5);
|
|
// nil for a whole check.
|
|
CarriedFrom map[string]string
|
|
// FPSet is the candidate's fingerprint set (Candidate.Fpset): a composition names the set it judged.
|
|
FPSet string
|
|
// Composition is, for a carry, the composition that passed on the same fingerprint set: it is kept and not asked again,
|
|
// since no fingerprint of a module it composed changed (ADR 0548 §5). Nil composes.
|
|
Composition *meshdeliverychecker.VerdictsByGroupHeadsValueComposition
|
|
}
|
|
|
|
// Asked records the id of an ask the check put.
|
|
func (c *Case) Asked(id string) {
|
|
c.notesMu.Lock()
|
|
c.asks = append(c.asks, id)
|
|
c.notesMu.Unlock()
|
|
}
|
|
|
|
// Asks are the ids of the asks the check put, in order.
|
|
func (c *Case) Asks() []string {
|
|
c.notesMu.Lock()
|
|
defer c.notesMu.Unlock()
|
|
return append([]string(nil), c.asks...)
|
|
}
|
|
|
|
// Progress is the step a check is at, written by the check and read when its bound passes.
|
|
type Progress struct {
|
|
mu sync.Mutex
|
|
stage, node string
|
|
}
|
|
|
|
// Set records the step now running and the node it runs on, empty for none.
|
|
func (p *Progress) Set(stage, node string) {
|
|
p.mu.Lock()
|
|
p.stage, p.node = stage, node
|
|
p.mu.Unlock()
|
|
}
|
|
|
|
// Now is the step recorded last and its node.
|
|
func (p *Progress) Now() (stage, node string) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
return p.stage, p.node
|
|
}
|
|
|
|
// CaseOf is the check of a candidate against the trunk heads (the gate's source.main): the pull requests that are judged.
|
|
// The checker judges pull requests into main only (ADR 0484 clause 11): the pull message carries no base, a merge into
|
|
// another branch is a closed message, so every candidate is into the trunk; a repository whose trunk head is not known has
|
|
// none to merge onto. It answers nil when no pull request is left to judge.
|
|
func CaseOf(c meshcatalog.CandidatesByGroupValue, trunk map[string]string) *Case {
|
|
key, err := meshdeliverycheckerclient.VerdictKey(c.Group, c.Heads)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
cs := &Case{Key: key, Group: c.Group, HeadsHash: c.Heads, Heads: GateHeads(c), Trunk: map[string]string{}, Progress: &Progress{}, FPSet: c.Fpset}
|
|
for _, p := range c.Pulls {
|
|
switch head, known := trunk[p.Repository]; {
|
|
case !known || head == "":
|
|
cs.Left = append(cs.Left, fmt.Sprintf("%s#%d: %s", p.Repository, p.Number, UntrunkedWordsOf([]string{p.Repository})))
|
|
default:
|
|
cs.Pulls = append(cs.Pulls, Pull{Repository: p.Repository, Number: int(p.Number), Head: p.Head, Trunk: head, Modules: p.Modules, Fps: p.Fps})
|
|
cs.Trunk[p.Repository] = head
|
|
}
|
|
}
|
|
if len(cs.Pulls) == 0 {
|
|
return nil
|
|
}
|
|
sort.SliceStable(cs.Pulls, func(i, j int) bool {
|
|
if cs.Pulls[i].Repository != cs.Pulls[j].Repository {
|
|
return cs.Pulls[i].Repository < cs.Pulls[j].Repository
|
|
}
|
|
return cs.Pulls[i].Number < cs.Pulls[j].Number
|
|
})
|
|
return cs
|
|
}
|
|
|
|
// UntrunkedWords open the words of a verdict, and of a status, of a pull request that cannot be judged because the gate's
|
|
// source.main names no trunk head of its repository (issue 1422): never a failure of the change, and judged whole once the
|
|
// trunk head is known. It is said loudly, never left silently "new".
|
|
const UntrunkedWords = "cannot be judged: the trunk head of "
|
|
|
|
// UntrunkedWordsOf are those words for the repositories whose trunk head is not known, in order, each named once.
|
|
func UntrunkedWordsOf(repos []string) string {
|
|
rs := slices.Clone(repos)
|
|
slices.Sort(rs)
|
|
rs = slices.Compact(rs)
|
|
return UntrunkedWords + strings.Join(rs, ", ") + " is not known: the gate's source.main names none, so there is no commit to merge it onto"
|
|
}
|
|
|
|
// IsUntrunked says whether a verdict says its heads cannot be judged for want of a trunk head (UntrunkedWords).
|
|
func IsUntrunked(v meshdeliverychecker.VerdictsByGroupHeadsValue) bool {
|
|
return v.State == meshdeliverycheckerclient.VerdictFail && failedOf(v).Machine == "checker" && strings.HasPrefix(failedOf(v).Words, UntrunkedWords)
|
|
}
|
|
|
|
// **A candidate outside the mesh is not the checker's** (sweep 2026-10-30 item 7): the forge carries repositories the mesh
|
|
// does not deliver (an operator's own), whose pull requests the indexer writes as candidates like any. Their trunk the
|
|
// gate never names, so each was judged "cannot be judged" and owed the status error on mesh/check, which the forge
|
|
// module wrote 119 times per head into a repository the mesh's forge account may not write (403). A candidate every pull
|
|
// request of which is of a repository the gate's source.main names no trunk head of, and touches no module, is outside
|
|
// the mesh: judged nothing, owed nothing on the forge, said once in the log and in checker_status (outside_the_mesh). A
|
|
// new repository of the mesh is not outside: its first pull request adds a module, and is said untrunked (issue 1422).
|
|
|
|
// OutsideWords say why a candidate is outside the mesh.
|
|
const OutsideWords = "outside the mesh: the gate's source.main names no trunk head of its repositories and it touches no module, so the checker judges nothing and owes no status on the forge"
|
|
|
|
// Outside says every pull request of c is of a repository trunk names no head of and touches no module. Never with
|
|
// trunk unknown (nil): before the gate's source is read nothing is outside.
|
|
func Outside(c meshcatalog.CandidatesByGroupValue, trunk map[string]string) bool {
|
|
if trunk == nil || len(c.Pulls) == 0 {
|
|
return false
|
|
}
|
|
for _, p := range c.Pulls {
|
|
if trunk[p.Repository] != "" || len(p.Modules) > 0 || len(p.Fps) > 0 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// leftOf are the pull requests of a candidate whose repository has no trunk head known, with Trunk empty, in repository and
|
|
// number order; nil when every repository's is known.
|
|
func leftOf(c meshcatalog.CandidatesByGroupValue, trunk map[string]string) []Pull {
|
|
var out []Pull
|
|
for _, p := range c.Pulls {
|
|
if trunk[p.Repository] == "" {
|
|
out = append(out, Pull{Repository: p.Repository, Number: int(p.Number), Head: p.Head, Modules: p.Modules, Fps: p.Fps})
|
|
}
|
|
}
|
|
sort.SliceStable(out, func(i, j int) bool {
|
|
if out[i].Repository != out[j].Repository {
|
|
return out[i].Repository < out[j].Repository
|
|
}
|
|
return out[i].Number < out[j].Number
|
|
})
|
|
return out
|
|
}
|
|
|
|
// GateHeads is the hash the gate matches the members an Approve covers by (mesh-catalog mesh-delivery-gate onestatus.go
|
|
// headsOf): meshdeliveryclient.Heads over every pull request of the candidate, hq's included, as
|
|
// `<owner/name lowercase>#<number>=<full head sha lowercase>`. A lone pull request is a group of one. Empty for a candidate
|
|
// with no pull request.
|
|
func GateHeads(c meshcatalog.CandidatesByGroupValue) string {
|
|
heads := make([]meshdeliveryclient.Head, 0, len(c.Pulls))
|
|
for _, p := range c.Pulls {
|
|
heads = append(heads, meshdeliveryclient.Head{Repository: strings.ToLower(p.Repository), Number: int(p.Number), Head: strings.ToLower(p.Head)})
|
|
}
|
|
hash, err := meshdeliveryclient.HeadsHash(heads)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return hash
|
|
}
|
|
|
|
// sameTrunk says whether two verdicts' trunk heads are the same.
|
|
func sameTrunk(a, b map[string]string) bool { return maps.Equal(a, b) }
|
|
|
|
// Trunks holds the trunk head of each repository, as the catalog half's trunk revision says (a follower of the seat mesh-catalog's
|
|
// verb revision and its event catalog-revision sets it; design 47 G §197).
|
|
type Trunks struct {
|
|
mu sync.Mutex
|
|
heads map[string]string
|
|
}
|
|
|
|
// Set takes the trunk heads in and answers whether they changed.
|
|
func (t *Trunks) Set(heads map[string]string) bool {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
if maps.Equal(t.heads, heads) {
|
|
return false
|
|
}
|
|
t.heads = maps.Clone(heads)
|
|
return true
|
|
}
|
|
|
|
// Heads are the trunk heads now (a copy).
|
|
func (t *Trunks) Heads() map[string]string {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return maps.Clone(t.heads)
|
|
}
|
|
|
|
// note puts a word of the check's way on the case, for a try's line (issue 1921).
|
|
func (c *Case) note(s string) {
|
|
c.notesMu.Lock()
|
|
defer c.notesMu.Unlock()
|
|
if !slices.Contains(c.notes, s) {
|
|
c.notes = append(c.notes, s)
|
|
}
|
|
}
|
|
|
|
// warn says of a pull request (pullKey) that a part of its check did not run, in words for its status line (issue 1577).
|
|
func (c *Case) warn(key, words string) {
|
|
c.notesMu.Lock()
|
|
defer c.notesMu.Unlock()
|
|
if c.warnings == nil {
|
|
c.warnings = map[string]string{}
|
|
}
|
|
c.warnings[key] = words
|
|
}
|
|
|
|
// Warning is what warn said of a pull request, or "".
|
|
func (c *Case) Warning(key string) string {
|
|
c.notesMu.Lock()
|
|
defer c.notesMu.Unlock()
|
|
return c.warnings[key]
|
|
}
|
|
|
|
// Notes are the words noted on the case, in the order they were noted.
|
|
func (c *Case) Notes() []string {
|
|
c.notesMu.Lock()
|
|
defer c.notesMu.Unlock()
|
|
return slices.Clone(c.notes)
|
|
}
|