Files
mesh-delivery/modules/mesh-delivery-checker/internal/check/case.go
T
jschoubben 7abe230701 Judge a pushed head as a try on the checker, through checker_try and checker_try_read (ADR 0501 §5)
mesh-controller's try asked build-agent, which leaves the checking at R4e. A try is now a key in the checker's state
tries: the checker judges the head merged onto its trunk as it judges a pull request's head, through the same
judge, places and holds, writes its end in the key and owes the forge nothing (no status, no preview, no verdict).
One try per head, at most four at once; hq and a repository whose trunk the gate does not name are refused with
"no level of <repo> is checked by the checker".
2026-10-22 18:03:28 +02:00

157 lines
5.4 KiB
Go

package check
import (
"fmt"
"maps"
"sort"
"strings"
"sync"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// HQRepository is the repository the dark checker writes no status for (ADR 0484 clause 8): hq builds no module and its
// check is a script no build module can run, so it is judged by its merge-check.sh until issue 1296 settles how.
const HQRepository = "novox/hq"
// HQNote is what the read tools say of an hq pull request (ADR 0484 clause 8).
const HQNote = "hq: judged by merge-check.sh until issue 1296"
// 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]delivery.CandidateModule
}
// Case is one check: a candidate's pull requests that are judged (those of hq and of a repository whose trunk is not known
// are left out), the trunk head of each repository and the key of the verdict.
type Case struct {
// 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 (hq's, 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
// Try narrows a try's check (its level, its tests); nil for a pull request's.
Try *TryOptions
// Progress says what the check is doing, so a check that ran past its bound names the step it was stuck at.
Progress *Progress
}
// 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 delivery.Candidate, trunk map[string]string) *Case {
key, err := delivery.CheckerVerdictKey(c.Group, c.HeadsHash)
if err != nil {
return nil
}
cs := &Case{Key: key, Group: c.Group, HeadsHash: c.HeadsHash, Heads: GateHeads(c), Trunk: map[string]string{}, Progress: &Progress{}}
for _, p := range c.Pulls {
switch head, known := trunk[p.Repository]; {
case p.Repository == HQRepository:
cs.Left = append(cs.Left, fmt.Sprintf("%s#%d: %s", p.Repository, p.Number, HQNote))
case !known || head == "":
cs.Left = append(cs.Left, fmt.Sprintf("%s#%d: the trunk head of %s is not known", p.Repository, p.Number, p.Repository))
default:
cs.Pulls = append(cs.Pulls, Pull{Repository: p.Repository, Number: p.Number, Head: p.Head.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
}
// GateHeads is the hash the gate matches the members an Approve covers by (mesh-catalog mesh-delivery-gate onestatus.go
// headsOf): delivery.HeadsHash 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 delivery.Candidate) string {
heads := make([]delivery.Head, 0, len(c.Pulls))
for _, p := range c.Pulls {
heads = append(heads, delivery.Head{Repository: strings.ToLower(p.Repository), Number: p.Number, Head: strings.ToLower(p.Head.Head)})
}
hash, err := delivery.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 gate's source.main says (a watch puts it in).
type Trunks struct {
mu sync.Mutex
heads map[string]string
}
// Put takes a source.main value in and answers whether the heads changed.
func (t *Trunks) Put(value []byte, deleted bool) bool {
var heads map[string]string
if !deleted {
s, err := delivery.DecodeSource(value)
if err != nil {
return false
}
heads = s.Heads
}
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)
}