Files
mesh-catalog/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/checks.go
T
jschoubben c4fd31aa7e
mesh/merge-gate head c4fd31aa7e97 merged into trunk eb4f16431ce3 — pass: builds mesh-delivery-gate → novox; no bus step; every machine composes with the ch…
mesh/repo-check head c4fd31aa7e97 merged into trunk eb4f16431ce3 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/check failed: unit: clipmenu/cmd/clipmenu-tools.TestTheLauncherWaitsQuietlyWhileAnotherClipmenudHoldsTheLock: manifest_test.go:199: the launcher …
Say a held group waits for a review and keep each member's status current (issue 1803, round 1)
2026-10-22 01:37:49 +02:00

298 lines
10 KiB
Go

package main
import (
"errors"
"fmt"
"sort"
"strings"
"time"
)
// The `checks` verb (novox/hq ADR 0239): what the mesh's checks said of a pull request's head, or of one
// commit. Two records answer it, and neither alone is enough. The forge keeps each status — the newest of
// each context, a description clipped to what it shows, and the protection that decides whether the merge
// button works. This owner keeps the verdict whole as the controller said it: each layer's summary
// unclipped ("0 of 4 compose" is not lost to a 140-character description), the build seat's ask that ran
// it, the machine it ran on, and its report. So the forge is asked for its statuses and this owner joins
// its own verdict to the two statuses that verdict set. Nothing is written: a read, by anyone.
// Context names of the statuses this owner can say more of than the forge keeps.
const (
ctxGate = "mesh/merge-gate"
ctxRepo = "mesh/repo-check"
ctxDelivery = "mesh/delivery"
ctxGroup = "mesh/delivery-group"
)
// CheckStatus is one status of the commit, with what this owner knows of it beyond the forge.
type CheckStatus struct {
Name string `json:"name"`
State string `json:"state"`
Description string `json:"description,omitempty"`
SetAt string `json:"set_at,omitempty"`
SetBy string `json:"set_by,omitempty"`
Link string `json:"link,omitempty"`
// From the verdict that set it, when this owner holds it: the build seat's ask, the machine that ran it,
// when it was heard, and the layer's verdict in full.
Build string `json:"build,omitempty"`
CheckedOn string `json:"checked_on,omitempty"`
CheckedAt string `json:"checked_at,omitempty"`
Verdict string `json:"verdict,omitempty"`
// Source says where the status was read: the forge, or — the forge unreachable — this owner's verdict.
Source string `json:"source"`
}
// ChecksAnswer is the `checks` verb's answer.
type ChecksAnswer struct {
Repository string `json:"repository"`
Commit string `json:"commit"`
Pull *ForgePull `json:"pull,omitempty"`
Delivery *ChecksOf `json:"delivery,omitempty"`
Statuses []CheckStatus `json:"statuses"`
// Verdict is the merge check's verdict whole, as this owner heard it for this commit.
Verdict *Verdict `json:"verdict,omitempty"`
// Judged is what the verdict judged: the head merged into the trunk commit (novox/hq ADR 0323 §3); OutOfDate
// why it no longer counts, when its trunk has moved since.
Judged string `json:"judged,omitempty"`
OutOfDate string `json:"out_of_date,omitempty"`
// Merge is whether the branch's protection lets the commit merge; Mergeable nil when it is not known.
Merge ForgeMerge `json:"merge"`
// Forge is why the forge could not be read, when it could not.
Forge string `json:"forge,omitempty"`
Says string `json:"says"`
}
// ChecksOf is the delivery of the commit, in brief: `show` has it whole.
type ChecksOf struct {
ID string `json:"id"`
State State `json:"state"`
Since string `json:"since"`
Waits string `json:"waits,omitempty"`
Group string `json:"group,omitempty"`
// Class, Opened and MergedAt are what the issue tracker measures a fix's time by (novox/hq ADR 0313): the
// class of what it moves, when its pull request was first heard, and when it merged.
Class string `json:"class,omitempty"`
Opened string `json:"opened,omitempty"`
MergedAt string `json:"merged_at,omitempty"`
}
// Checks is the `checks` verb: a repository and a pull request's number, or a repository and a commit.
func (h *Holder) Checks(repository string, number int, commit string) (*ChecksAnswer, error) {
repository = strings.ToLower(strings.TrimSuffix(strings.TrimSpace(repository), ".git"))
owner, repo, ok := strings.Cut(repository, "/")
if !ok || owner == "" || repo == "" || strings.Contains(repo, "/") {
return nil, fmt.Errorf("%q is not owner/repository", repository)
}
commit = strings.ToLower(strings.TrimSpace(commit))
if number < 0 {
return nil, fmt.Errorf("%d is not a pull request's number", number)
}
if number == 0 && commit == "" {
return nil, errors.New("name a pull request's number or a commit")
}
if commit != "" && (len(commit) < 7 || strings.Trim(commit, "0123456789abcdef") != "") {
return nil, fmt.Errorf("%q is not a commit: seven or more hexadecimal characters", commit)
}
// What this owner knows first, for the base whose protection is read; the forge then says the head.
base := ""
if d := h.deliveryOf(repository, number, commit); d != nil {
base = d.Base
}
a := &ChecksAnswer{Repository: repository, Commit: commit}
fc, err := h.Forge.Statuses(owner, repo, number, commit, base)
if err != nil {
a.Forge = err.Error()
} else {
a.Commit, a.Pull = fc.Commit, fc.Pull
}
d := h.deliveryOf(repository, number, a.Commit)
if a.Commit == "" && d != nil {
a.Commit = d.Commit // the forge away: the newest head this owner holds for the pull request
}
if d != nil {
a.Delivery = &ChecksOf{ID: d.ID, State: d.State, Since: d.Since.UTC().Format(time.RFC3339), Waits: d.waits,
Group: d.Group, Class: fixClass(&d.Delivery)}
if !d.opened.IsZero() {
a.Delivery.Opened = d.opened.UTC().Format(time.RFC3339)
}
if !d.MergedAt.IsZero() {
a.Delivery.MergedAt = d.MergedAt.UTC().Format(time.RFC3339)
}
if d.Check != nil && sameCommit(d.Commit, a.Commit) {
a.Verdict = d.Check
a.Judged, a.OutOfDate = d.Check.Judged(), d.outOfDate()
}
}
if fc != nil {
for _, s := range fc.Statuses {
a.Statuses = append(a.Statuses, CheckStatus{Name: s.Context, State: s.State, Description: s.Description,
SetAt: firstOf(s.UpdatedAt, s.CreatedAt), SetBy: s.Creator, Link: s.TargetURL, Source: "the forge"})
}
a.Merge = fc.Merge
} else {
// The forge away: the statuses the verdict set, as the forge holder sets them from it, said as such.
if v := a.Verdict; v != nil {
a.Statuses = append(a.Statuses, CheckStatus{Name: ctxGate, State: forgeStateOf(v.Gate), Source: "mesh-delivery's verdict; the forge could not be read"})
if v.Repo != "" {
a.Statuses = append(a.Statuses, CheckStatus{Name: ctxRepo, State: forgeStateOf(v.Repo), Source: "mesh-delivery's verdict; the forge could not be read"})
}
}
a.Merge = ForgeMerge{Branch: firstOf(base, "main"), Says: "not known: the forge could not be read, and only it holds the protection"}
}
for i := range a.Statuses {
h.addWhatIsKnown(&a.Statuses[i], a.Verdict, d)
}
sort.SliceStable(a.Statuses, func(i, j int) bool { return a.Statuses[i].Name < a.Statuses[j].Name })
a.Says = a.says()
return a, nil
}
// checksDelivery is the part of a delivery `checks` reads, copied under the lock.
type checksDelivery struct {
Delivery
waits string
groupSaid string
opened time.Time
}
// deliveryOf is the delivery of a commit — its head, or the commit it merged as — else, for a pull request,
// its newest head that was not superseded, else its newest.
func (h *Holder) deliveryOf(repository string, number int, commit string) *checksDelivery {
h.mu.Lock()
defer h.mu.Unlock()
var found *Delivery
if commit != "" {
for _, d := range h.deliveries {
if strings.EqualFold(d.Repository, repository) && (sameCommit(d.Commit, commit) || sameCommit(d.MergedAs, commit)) {
found = d
break
}
}
} else if number > 0 {
for _, d := range h.deliveries {
if !strings.EqualFold(d.Repository, repository) || d.Number != number {
continue
}
if found == nil || newerHead(d, found) {
found = d
}
}
}
if found == nil {
return nil
}
c := &checksDelivery{Delivery: *found, waits: h.waitsFor(found), opened: h.openedOf(found)}
if found.Check != nil {
v := *found.Check
c.Check = &v
}
if g := h.groups[found.Group]; g != nil {
state, why, _ := h.groupState(g, h.membersOf(g))
c.groupSaid = state + ": " + why
if g.Check != nil && g.Check.Verdict != "" {
c.groupSaid += "; composed check " + g.Check.Verdict + " — " + g.Check.Summary
}
}
return c
}
// newerHead is whether d is a pull request's head to read before e: one not superseded first, then the newest.
func newerHead(d, e *Delivery) bool {
if (d.State == Superseded) != (e.State == Superseded) {
return e.State == Superseded
}
return d.Created.After(e.Created)
}
// addWhatIsKnown joins this owner's verdict to the status it set, and the delivery to its own statuses.
func (h *Holder) addWhatIsKnown(s *CheckStatus, v *Verdict, d *checksDelivery) {
switch s.Name {
case ctxGate, ctxRepo:
if v == nil {
return
}
s.Build, s.CheckedOn = v.ID, v.On
if !v.At.IsZero() {
s.CheckedAt = v.At.UTC().Format(time.RFC3339)
}
if s.Name == ctxGate {
s.Verdict = strings.ToUpper(firstOf(v.Gate, "error")) + " — " + v.Summary
if len(v.Modules) > 0 {
s.Verdict += " (modules: " + strings.Join(v.Modules, ", ")
if len(v.Dependents) > 0 {
s.Verdict += "; built after them: " + strings.Join(v.Dependents, ", ")
}
s.Verdict += ")"
}
} else if v.Repo != "" {
s.Verdict = strings.ToUpper(v.Repo) + " — " + v.RepoSaid
}
case ctxDelivery:
if d != nil {
s.Verdict = string(d.State)
if d.waits != "" {
s.Verdict += ": waits for " + d.waits
}
}
case ctxGroup:
if d != nil && d.groupSaid != "" {
s.Verdict = d.groupSaid
}
}
}
// forgeStateOf is the forge state the forge's holder sets for a verdict (the gitea module's pulls.ts): a
// warning is a pass with a note, an error never a success.
func forgeStateOf(verdict string) string {
switch verdict {
case "pass", "warning":
return "success"
case "fail":
return "failure"
}
return "error"
}
func (a *ChecksAnswer) says() string {
what := a.Repository + "@" + shortOf(a.Commit, 8)
if a.Pull != nil {
what = fmt.Sprintf("%s#%d at %s", a.Repository, a.Pull.Number, shortOf(a.Commit, 8))
}
var parts []string
for _, s := range a.Statuses {
parts = append(parts, s.Name+" "+s.State)
}
statuses := "no status"
if len(parts) > 0 {
statuses = strings.Join(parts, ", ")
}
merge := a.Merge.Says
switch {
case a.Merge.Mergeable == nil:
merge = "mergeable: not known — " + merge
case *a.Merge.Mergeable:
merge = "mergeable — " + merge
default:
merge = "NOT mergeable — " + merge
}
if a.Judged != "" {
what += " (" + a.Judged + ")"
}
if a.OutOfDate != "" {
merge += "; its verdict is " + a.OutOfDate
}
return what + ": " + statuses + "; " + merge
}
func firstOf(s ...string) string {
for _, x := range s {
if x != "" {
return x
}
}
return ""
}