Once main requires mesh/check, the controller publishes no verdict for it and the gate, which moved deliveries on the controller's checked, would hold every pull request proposed. A head's mesh/check is now its verdict, from the forge's status event (gitea now sends the status's words) or its open pull requests. The Approve is asked only on the checker's verdict at exactly the heads the ask covers, so a member judged alone never passes its group; a group on mesh/check asks no composed check and owes no mesh/merge-gate; a member's mesh/check that changes withdraws the ask. Nothing is asked of the controller for such a base, and recheck refuses, naming checker_pull. hq's mesh/repo-check keeps today's path.
303 lines
10 KiB
Go
303 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 is the controller's merge gate, kept where a base still requires it; mesh/check is ctxCheck (onestatus.go).
|
|
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 ctxCheck:
|
|
if v != nil && v.Status == ctxCheck {
|
|
s.Verdict = strings.ToUpper(firstOf(v.Gate, "error")) + " — " + v.Summary
|
|
}
|
|
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 ""
|
|
}
|