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 …
298 lines
10 KiB
Go
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 ""
|
|
}
|