A delivery made earlier can be announced again after a newer one, with main's head moved on: its base_sha is the freshest. The gate now keeps when each base_sha was announced and seeds from the latest.
424 lines
20 KiB
Go
424 lines
20 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// What mesh-delivery owns (novox/hq ADR 0239): the delivery — one commit in one repository — and the
|
|
// delivery group above it. Each is kept whole, one key of the module's own state per delivery and per
|
|
// group, written only by this holder.
|
|
|
|
// Delivery is one commit in one repository, from its pull request's head to every machine.
|
|
type Delivery struct {
|
|
// ID is owner/repository@<twelve characters of its commit>.
|
|
ID string `json:"id"`
|
|
Repository string `json:"repository"`
|
|
Commit string `json:"commit"`
|
|
// The pull request, when there is one: its number, title, base, head branch and page.
|
|
Number int `json:"number,omitempty"`
|
|
Title string `json:"title,omitempty"`
|
|
Base string `json:"base,omitempty"`
|
|
Branch string `json:"branch,omitempty"`
|
|
HTMLURL string `json:"html_url,omitempty"`
|
|
CloneURL string `json:"clone_url,omitempty"`
|
|
// The diffset, as the forge said it: what the planner maps onto modules.
|
|
Paths []string `json:"paths,omitempty"`
|
|
PathsTruncated bool `json:"paths_truncated,omitempty"`
|
|
Removed []string `json:"removed,omitempty"`
|
|
ModuleDirs []string `json:"module_dirs,omitempty"`
|
|
ModuleDirsSaid bool `json:"module_dirs_said,omitempty"`
|
|
// After are the repositories its pull request's description names in `after: <repository>` lines. They are not read
|
|
// (novox/hq ADR 0419 §2): kept only to say once on the pull request that the line is not read. AfterSaid is whether
|
|
// that was said, for this pull request.
|
|
After []string `json:"after,omitempty"`
|
|
AfterSaid bool `json:"after_said,omitempty"`
|
|
// Comments is how many comments this owner owed on the pull request at this head, which numbers the next.
|
|
Comments int `json:"comments,omitempty"`
|
|
// Issues are the issues its title and body name ("issue 948"), shown to the operator who is asked to approve its merge.
|
|
Issues []int `json:"issues,omitempty"`
|
|
// Group is the delivery group it is in: its head branch name. Empty for a lone delivery.
|
|
Group string `json:"group,omitempty"`
|
|
|
|
State State `json:"state"`
|
|
Since time.Time `json:"since"`
|
|
Created time.Time `json:"created"`
|
|
|
|
// Check is its own check's verdict.
|
|
Check *Verdict `json:"check,omitempty"`
|
|
// Required are the statuses its base requires, as the forge last said them (gitea_open_pulls): mesh/check makes it
|
|
// the checker's (issue 1302). Empty until the forge said them.
|
|
Required []string `json:"required,omitempty"`
|
|
// CheckState is its head's mesh/check as the forge last said it (pending, success, failure, error): a pending one
|
|
// after a pass is the checker judging it again, and no Approve is asked meanwhile (issue 1302).
|
|
CheckState string `json:"check_state,omitempty"`
|
|
// CheckAsk is this owner's own ask of its check, when the forge's announcement did not bring one: a head
|
|
// adopted while it already waited, or one whose announcement the controller missed.
|
|
CheckAsk *CheckAsk `json:"check_ask,omitempty"`
|
|
// TrunkNow is its trunk's head as this owner last heard it move while it was open: the commit the last merge
|
|
// into its trunk landed as (novox/hq ADR 0323 §7). A verdict that names another trunk commit is out of date.
|
|
// Empty until a merge into its trunk is heard after it was made.
|
|
TrunkNow string `json:"trunk_now,omitempty"`
|
|
// BaseSHA is the base branch's head the forge last announced its head with (ADR 0323): sent with a check this
|
|
// owner asks, for a verdict no build judges to name its trunk commit.
|
|
BaseSHA string `json:"base_sha,omitempty"`
|
|
// BaseSHAAt is when the forge last announced its head with BaseSHA: the freshest of a repository's announcements gives
|
|
// source.main its trunk head when it names none (seedTrunks, issue 1422). Zero for one kept before it was written.
|
|
BaseSHAAt time.Time `json:"base_sha_at,omitzero"`
|
|
// AskedAgain is when its head was announced again while its verdict was out of date — the merge tool's ask of
|
|
// its check against the trunk's head (ADR 0323 §5) — and no verdict has come since: its one-hour bound runs from
|
|
// then. Nil while nothing is asked, and cleared by a verdict or another move of its trunk.
|
|
AskedAgain *time.Time `json:"asked_again,omitempty"`
|
|
|
|
// The facts the table's guards read. NewerHead is the head that superseded it; ClosedUnmerged the pull
|
|
// request closed; MergedAs the commit it landed on the trunk as; HeldWhy what holds it for a person.
|
|
NewerHead string `json:"newer_head,omitempty"`
|
|
ClosedUnmerged bool `json:"closed_unmerged,omitempty"`
|
|
// Others are the other open pull requests announced at this head while this one was live (issue 1114), as they were
|
|
// announced: the record is theirs to take over when this one's pull request closes unmerged.
|
|
Others []PullEvent `json:"others,omitempty"`
|
|
MergedAs string `json:"merged_as,omitempty"`
|
|
MergedAt time.Time `json:"merged_at"`
|
|
// InSource says the merge was looked at for source.main: written there, or left as older than the trunk's head (issue 888).
|
|
// A merge the gate handed to the bridge on an Approve is moved there by finish instead (merges.go).
|
|
InSource bool `json:"in_source,omitempty"`
|
|
// LearntLate says its merge was learnt after the fact, from its absence among the forge's open pull requests
|
|
// (settleGone, issue 1275): it moved no trunk here, and it never moves source.main, not even at a catch-up.
|
|
LearntLate bool `json:"learnt_late,omitempty"`
|
|
HeldWhy string `json:"held_why,omitempty"`
|
|
// FromForge is a delivery made from the forge's word at its merge: its head was never heard by this owner.
|
|
FromForge bool `json:"from_forge,omitempty"`
|
|
|
|
// Ready is what this owner did with the operator's Approve of its merge into main, and with the merge (novox/hq ADR
|
|
// 0421, ready.go). The json key is the one the ready mark's record had, so rows written before still read.
|
|
Ready *ReadyRun `json:"ready,omitempty"`
|
|
// ReviewedBy and ReviewedAt are the review record its pull request's description holds: the line
|
|
// `Reviewed-by: <who> at <full sha>` (novox/hq ADR 0421 §5, issue 1022), as written. Read at every head; never a
|
|
// hold: no Approve is asked until it names the head (novox/hq ADR 0496, issue 1803).
|
|
ReviewedBy string `json:"reviewed_by,omitempty"`
|
|
ReviewedAt string `json:"reviewed_at,omitempty"`
|
|
// PushedBy is the session that pushed its head, as its pull request's description names it in a line
|
|
// `Pushed-by: <who@node>` (playbook 07): named with its branch when a new head is needed (novox/hq ADR 0505 §5).
|
|
PushedBy string `json:"pushed_by,omitempty"`
|
|
// FreshWanted is whether a passing verdict that arrived out of date is to have its check asked against the
|
|
// trunk's head, before the operator is asked (novox/hq ADR 0421 §3, issue 1022). Cleared when it is asked.
|
|
FreshWanted bool `json:"fresh_wanted,omitempty"`
|
|
|
|
// Settle is the settle ask a person had written for it while it is held for a merge without a passing check (settle.go,
|
|
// novox/hq ADR 0556 clause 5): its key in the gate's asks, why, who asked and when, and the operator's answer once read.
|
|
Settle *SettleAsk `json:"settle,omitempty"`
|
|
|
|
Transitions []Transition `json:"transitions,omitempty"`
|
|
// Owed are the forge's and the bus's effects of its transitions not yet done: said only after the
|
|
// delivery is kept, retried until done.
|
|
Owed []Effect `json:"owed,omitempty"`
|
|
}
|
|
|
|
// Transition is one move of the table, kept.
|
|
type Transition struct {
|
|
At time.Time `json:"at"`
|
|
From State `json:"from"`
|
|
To State `json:"to"`
|
|
Event Event `json:"event"`
|
|
Why string `json:"why"`
|
|
By string `json:"by,omitempty"`
|
|
}
|
|
|
|
// Verdict is a check's verdict: the gate's and the repository's own. ID is the build seat's ask that ran
|
|
// it, On the machine it ran on, At when this owner heard it; Report is the check's own account, as bounded
|
|
// as the controller says it. All of it is what `checks` answers, whole, beside the forge's short statuses.
|
|
type Verdict struct {
|
|
ID string `json:"id,omitempty"`
|
|
On string `json:"on,omitempty"`
|
|
At time.Time `json:"at"`
|
|
Gate string `json:"gate"`
|
|
Summary string `json:"summary"`
|
|
Modules []string `json:"modules,omitempty"`
|
|
Dependents []string `json:"dependents,omitempty"`
|
|
Repo string `json:"repo,omitempty"`
|
|
RepoSaid string `json:"repo_summary,omitempty"`
|
|
Report string `json:"report,omitempty"`
|
|
// Head and Trunk are what it judged (novox/hq ADR 0323 §3): the head merged into the trunk commit, twelve
|
|
// characters each. Trunk is empty for a verdict of a build seat that judged the head alone. Beside are the
|
|
// commits of the other repositories the check read, by directory; Took how long it ran.
|
|
Head string `json:"head,omitempty"`
|
|
Trunk string `json:"trunk,omitempty"`
|
|
Beside map[string]string `json:"beside,omitempty"`
|
|
Took string `json:"took,omitempty"`
|
|
// Status is the forge status it was read from when it is not the controller's verdict: mesh/check, the checker's
|
|
// (novox/hq ADR 0447, issue 1302). Empty for the controller's `checked`.
|
|
Status string `json:"status,omitempty"`
|
|
}
|
|
|
|
// Judged is what a verdict judged, in words: "head 1a2b3c4d5e6f merged into trunk 7a8b9c0d1e2f" (ADR 0323 §3);
|
|
// empty for a verdict that names no trunk commit.
|
|
func (v *Verdict) Judged() string {
|
|
if v == nil || v.Trunk == "" {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("head %s merged into trunk %s", shortOf(v.Head, 12), shortOf(v.Trunk, 12))
|
|
}
|
|
|
|
// outOfDate is why a delivery's verdict no longer counts: it names a trunk commit that is not its trunk's head
|
|
// as this owner heard it (novox/hq ADR 0323 §7). Empty when it counts — or names no trunk commit, as a verdict
|
|
// of a build seat from before the rule does, which counts as it did.
|
|
func (d *Delivery) outOfDate() string {
|
|
if d.Check == nil || d.Check.Trunk == "" || d.TrunkNow == "" || sameCommit(d.Check.Trunk, d.TrunkNow) {
|
|
return ""
|
|
}
|
|
return outOfDateWords(d.Check.Trunk, d.TrunkNow)
|
|
}
|
|
|
|
// outOfDateWords is a verdict out of date, as the forge's holder says it on the pull request (ADR 0323 §4).
|
|
func outOfDateWords(judged, now string) string {
|
|
return fmt.Sprintf("out of date: judged against trunk %s, the trunk is now %s; judged again on a recheck or when "+
|
|
"it is merged", shortOf(judged, 12), shortOf(now, 12))
|
|
}
|
|
|
|
// CheckAsk is a head's own check, asked of the controller by this owner (or re-asked by healer H2).
|
|
type CheckAsk struct {
|
|
// ID is the ask's id as the controller answered it.
|
|
ID string `json:"id"`
|
|
At time.Time `json:"at"`
|
|
By string `json:"by"`
|
|
Why string `json:"why"`
|
|
// Reasked is how many times H2 asked it again: once at most, then the operator's.
|
|
Reasked int `json:"reasked,omitempty"`
|
|
}
|
|
|
|
// GateComposed is the gate layer of a group member's own verdict when the group's composed check judges it instead
|
|
// (novox/hq ADR 0403 §1): no gate was run for the member alone.
|
|
const GateComposed = "composed"
|
|
|
|
// Passes is whether the verdict lets the delivery be ready: the gate passed or warned, and the
|
|
// repository's own check did not fail or error.
|
|
func (v *Verdict) Passes() bool {
|
|
if v == nil {
|
|
return false
|
|
}
|
|
// A group member's own gate is the group's composed check's (novox/hq ADR 0403 §1): the controller says the layer
|
|
// "composed", and the member counts as passed here once its repository's check did not fail, as the composed
|
|
// check, asked after every member is ready, then judges them together.
|
|
gate := v.Gate == "pass" || v.Gate == "warning" || v.Gate == GateComposed
|
|
repo := v.Repo == "" || v.Repo == "pass" || v.Repo == "warning"
|
|
return gate && repo
|
|
}
|
|
|
|
// Effect is one thing owed outside the state: an event said, a line appended to a commit's note, the view
|
|
// on the pull request refreshed, a status set.
|
|
type Effect struct {
|
|
Kind string `json:"kind"`
|
|
Event string `json:"event,omitempty"` // an emit's module event, when not transition or group (issue 1025)
|
|
Commit string `json:"commit,omitempty"`
|
|
Line string `json:"line,omitempty"`
|
|
Context string `json:"context,omitempty"`
|
|
State string `json:"state,omitempty"`
|
|
Tries int `json:"tries,omitempty"`
|
|
Last string `json:"last,omitempty"`
|
|
Since time.Time `json:"since"`
|
|
// Next is when a failed effect is tried again: later after every failure, up to retryCap.
|
|
Next *time.Time `json:"next,omitempty"`
|
|
}
|
|
|
|
// The kinds of effect.
|
|
const (
|
|
EffectEmit = "emit"
|
|
EffectNote = "note"
|
|
EffectView = "view"
|
|
EffectStatus = "status"
|
|
// EffectComment is a comment on the pull request; its Context is the sequence number of the comment on the head, which
|
|
// keeps two comments on one head two acts (mesh-sdk go/delivery OwedCommentAct).
|
|
EffectComment = "comment"
|
|
)
|
|
|
|
// FromForgesWord is whether the delivery was made from the forge's word at its merge: said by FromForge, or —
|
|
// for one made before FromForge was kept — by its first transition.
|
|
func (d *Delivery) FromForgesWord() bool {
|
|
if d.FromForge {
|
|
return true
|
|
}
|
|
return len(d.Transitions) > 0 && d.Transitions[0].Event == EvAnnounced &&
|
|
strings.Contains(d.Transitions[0].Why, "so it is made from the forge's word")
|
|
}
|
|
|
|
func (d *Delivery) shownOnItsPullRequest() bool {
|
|
return d.Number > 0
|
|
}
|
|
|
|
// Owner and Repo split the repository.
|
|
func (d *Delivery) Owner() string { o, _, _ := strings.Cut(d.Repository, "/"); return o }
|
|
func (d *Delivery) Repo() string { _, r, _ := strings.Cut(d.Repository, "/"); return r }
|
|
|
|
// IDOf is a delivery's id: the repository and twelve characters of its commit.
|
|
func IDOf(repository, commit string) string {
|
|
return strings.ToLower(repository) + "@" + shortOf(commit, 12)
|
|
}
|
|
|
|
func shortOf(commit string, n int) string {
|
|
if len(commit) > n {
|
|
return commit[:n]
|
|
}
|
|
return commit
|
|
}
|
|
|
|
// sameCommit is whether two commits are one, however either is abbreviated.
|
|
func sameCommit(a, b string) bool {
|
|
if a == "" || b == "" {
|
|
return false
|
|
}
|
|
if len(a) > len(b) {
|
|
a, b = b, a
|
|
}
|
|
return len(a) >= 7 && strings.HasPrefix(strings.ToLower(b), strings.ToLower(a))
|
|
}
|
|
|
|
// reviewedLine is the review record of a pull request's description (novox/hq ADR 0421 §5, issue 1022):
|
|
// `Reviewed-by: <who> at <full sha>`, on a line of its own, who as who@node.
|
|
var reviewedLine = regexp.MustCompile(`(?im)^\s*Reviewed-by:\s*(\S+)\s+at\s+([0-9a-f]{40}|[0-9a-f]{64})\b`)
|
|
|
|
// ReviewIn reads the review record of a pull request's description: who reviewed and the full sha named. The last such
|
|
// line counts; empty when there is none.
|
|
func ReviewIn(body string) (who, sha string) {
|
|
for _, m := range reviewedLine.FindAllStringSubmatch(body, -1) {
|
|
who, sha = m[1], strings.ToLower(m[2])
|
|
}
|
|
return who, sha
|
|
}
|
|
|
|
// pushedLine is the line naming the session that pushed a pull request's head: `Pushed-by: <who@node>` (ADR 0505 §5).
|
|
var pushedLine = regexp.MustCompile(`(?im)^\s*Pushed-by:\s*([A-Za-z0-9_.\-]+(?:@[A-Za-z0-9_.\-]+)?)\s*$`)
|
|
|
|
// PushedIn reads it: the last such line, or "" when there is none.
|
|
func PushedIn(body string) string {
|
|
who := ""
|
|
for _, m := range pushedLine.FindAllStringSubmatch(body, -1) {
|
|
who = m[1]
|
|
}
|
|
return who
|
|
}
|
|
|
|
// authorOf is a delivery's author as the operator reads it: "the author of <branch>", and the session that pushed it
|
|
// when its description names one ("who on node": the router refuses an @ in its words).
|
|
func authorOf(d *Delivery) string {
|
|
who := "the author of " + d.Branch
|
|
if d.Branch == "" {
|
|
who = "its author"
|
|
}
|
|
if d.PushedBy != "" {
|
|
who += " (" + strings.Replace(d.PushedBy, "@", " on ", 1) + ")"
|
|
}
|
|
return who
|
|
}
|
|
|
|
// reviewWords is what the delivery's review record says of its head, for the ask: reviewed by whom, or, first, that it
|
|
// is not reviewed at this head (the ask is held then, ADR 0496). ok is whether it is reviewed at this head.
|
|
func reviewWords(d *Delivery) (words string, ok bool) {
|
|
switch {
|
|
case d.ReviewedBy == "":
|
|
return "not reviewed at this head: the description holds no Reviewed-by line", false
|
|
case !strings.EqualFold(d.ReviewedAt, d.Commit):
|
|
return fmt.Sprintf("not reviewed at this head: the Reviewed-by line names %s, the head is %s",
|
|
shortOf(d.ReviewedAt, 12), shortOf(d.Commit, 12)), false
|
|
}
|
|
// The router refuses an @ in an ask's words: who@node is said "who on node".
|
|
return "reviewed by " + strings.Replace(d.ReviewedBy, "@", " on ", 1) + " at " + shortOf(d.Commit, 12), true
|
|
}
|
|
|
|
// afterLines are the `after: <repository>` lines of a pull request's description.
|
|
var afterLines = regexp.MustCompile(`(?im)^\s*after:\s*([A-Za-z0-9_.\-/]+)\s*$`)
|
|
|
|
// AfterIn reads them.
|
|
func AfterIn(body string) []string {
|
|
var out []string
|
|
for _, m := range afterLines.FindAllStringSubmatch(body, -1) {
|
|
out = append(out, strings.TrimSuffix(m[1], ".git"))
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// Group is a delivery group: two or more deliveries sharing a head branch name, one level only.
|
|
type Group struct {
|
|
ID string `json:"id"`
|
|
Members []string `json:"members"`
|
|
Created time.Time `json:"created"`
|
|
Updated time.Time `json:"updated"`
|
|
// Check is its composed check: asked of the controller for these member commits, and its verdict.
|
|
Check *GroupCheck `json:"check,omitempty"`
|
|
// SaidGate is, by member id, why the member's mesh/merge-gate was last said pending (a group the controller judges;
|
|
// a group on mesh/check is owed none, issue 1302): written again only when it
|
|
// changes (novox/hq ADR 0403 §1, §6, issue 978). Cleared once a composed verdict stands at the heads, which the
|
|
// forge module then sets as every member's gate.
|
|
SaidGate map[string]string `json:"said_gate,omitempty"`
|
|
// Closed is set once a member reached the trunk: a group delivering takes no new member.
|
|
Closed bool `json:"closed,omitempty"`
|
|
// Said is the derived state last said, so a change of it is said once.
|
|
Said string `json:"said,omitempty"`
|
|
// Hold is why the group's merge or its Approve's ask is held back by a member's required status that is not a
|
|
// success (issue 1275); nil when nothing is.
|
|
Hold *GroupHold `json:"hold,omitempty"`
|
|
// SaidStatus is the mesh/delivery-group status last written for it: a change of the status a state takes is
|
|
// written too, though the state did not change (novox/hq issue 902).
|
|
SaidStatus string `json:"said_status,omitempty"`
|
|
// SaidReview is the wait for a review last said of the group while it was held for one (ADR 0496 §2): a change of
|
|
// the members still waiting is said too, though the state did not change.
|
|
SaidReview string `json:"said_review,omitempty"`
|
|
Owed []Effect `json:"owed,omitempty"`
|
|
}
|
|
|
|
// GroupHold is a group held back from its Approve or its merge because a member's required status is not a success.
|
|
type GroupHold struct {
|
|
// Kind is "unasked" (the Approve is not asked) or "blocked" (the approved merge is not started: nothing is merged).
|
|
Kind string `json:"kind"`
|
|
Member string `json:"member"`
|
|
Says string `json:"says"`
|
|
Since time.Time `json:"since"`
|
|
}
|
|
|
|
// GroupCheck is a group's composed check.
|
|
type GroupCheck struct {
|
|
Asked string `json:"asked"`
|
|
At time.Time `json:"at"`
|
|
Commits map[string]string `json:"commits"`
|
|
Verdict string `json:"verdict,omitempty"`
|
|
Summary string `json:"summary,omitempty"`
|
|
// Trunks are the trunk commit each member's head was merged into, by member id (novox/hq ADR 0323 §6).
|
|
Trunks map[string]string `json:"trunks,omitempty"`
|
|
// OutOfDate is why its verdict no longer counts: a merge that is not one of the group's members moved a
|
|
// member's trunk, at OutOfDateAt. It counts as outstanding (ADR 0239 §18), and runs again once a member is
|
|
// judged again after it.
|
|
OutOfDate string `json:"out_of_date,omitempty"`
|
|
OutOfDateAt time.Time `json:"out_of_date_at"`
|
|
}
|
|
|
|
// Commits is each member's commit, by id.
|
|
func commitsOf(ds []*Delivery) map[string]string {
|
|
out := map[string]string{}
|
|
for _, d := range ds {
|
|
out[d.ID] = d.Commit
|
|
}
|
|
return out
|
|
}
|
|
|
|
// sameMembers is whether two member→commit maps are one.
|
|
func sameMembers(a, b map[string]string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for k, v := range a {
|
|
if b[k] != v {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func mustJSON(v any) json.RawMessage {
|
|
raw, _ := json.Marshal(v)
|
|
return raw
|
|
}
|