222 lines
10 KiB
Go
222 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// firstBuilds asks the first build of every module a merge adds to a repository no registered module is built from, when
|
|
// the merge was announced as made on the operator's Approve of that head (novox/hq ADR 0432, issue 1146): the modules'
|
|
// directories are the ones the announcement says hold a module.json at the merge commit, as touchedBy reads them. The
|
|
// merge is kept, so the build's outcome is judged by the rule that registers it (mayRegisterFrom, approvedMove).
|
|
//
|
|
// **Complete in either order, with no timer** (ADR 0426): when mesh-delivery's word is kept first, it is found here; when
|
|
// the forge's merge is heard first, the merge is kept as a first merge waiting for the word, and keepingDeliveryMerges
|
|
// runs this again when the word is kept (resumeFirstBuilds). A word that says no (a Decline, another head) asks nothing and
|
|
// says why. Answers whether the merge was dealt with here.
|
|
func (f following) firstBuilds(ctx context.Context, m link.SourceMoved, entries []inventory.Entry,
|
|
read map[string][]inventory.ReadRepository, now time.Time) (bool, error) {
|
|
inv := f.open.inventory
|
|
repository := m.Owner + "/" + m.Repo
|
|
added, why, err := firstBuildsOf(ctx, inv, m, entries, read)
|
|
switch {
|
|
case errors.Is(err, errFirstNotApproved):
|
|
fmt.Printf("%s merged into %s (%.8s) adds %s, and no build is asked: %v\n", repository, m.Base, m.Commit,
|
|
strings.Join(addedDirs(m, entries, read), ", "), err)
|
|
return true, nil
|
|
case errors.Is(err, errNotAnnounced):
|
|
// Kept, waiting for mesh-delivery's word: a restart keeps it, and the word's arrival asks the builds.
|
|
if kerr := keepFirstMerge(ctx, inv, m, now, true); kerr != nil {
|
|
return true, notNow(kerr)
|
|
}
|
|
fmt.Printf("%s merged into %s (%.8s) adds %s; it is kept, and the first build is asked when mesh-delivery "+
|
|
"announces the merge on the operator's Approve\n", repository, m.Base, m.Commit,
|
|
strings.Join(addedDirs(m, entries, read), ", "))
|
|
return true, nil
|
|
case err != nil:
|
|
return true, notNow(err)
|
|
case len(added) == 0:
|
|
return false, nil
|
|
}
|
|
if err := askFirstBuilds(ctx, inv, m, now, added, why); err != nil {
|
|
return true, notNow(err)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// keepFirstMerge keeps a first merge as the controller heard it, waiting (no plan answers it).
|
|
func keepFirstMerge(ctx context.Context, inv *inventory.Inventory, m link.SourceMoved, now time.Time, waitingForWord bool) error {
|
|
merged, err := time.Parse(time.RFC3339Nano, m.MergedAt)
|
|
if err != nil {
|
|
merged = now
|
|
}
|
|
event, err := json.Marshal(keptMerge{SourceMoved: m, FirstBuild: waitingForWord})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = inv.AddMerge(ctx, inventory.BatchedMerge{Repository: m.Owner + "/" + m.Repo, Branch: m.Base, Commit: m.Commit,
|
|
Merged: merged.UTC(), Heard: now, Event: event})
|
|
return err
|
|
}
|
|
|
|
// askFirstBuilds keeps the merge, asks the builds, and marks the merge as no longer waiting for them.
|
|
func askFirstBuilds(ctx context.Context, inv *inventory.Inventory, m link.SourceMoved, now time.Time, added []string,
|
|
why string) error {
|
|
repository := m.Owner + "/" + m.Repo
|
|
if err := keepFirstMerge(ctx, inv, m, now, false); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("%s merged into %s (%.8s) on the operator's Approve (%s): the mesh builds nothing from it yet, so the "+
|
|
"first build of %s is asked\n", repository, m.Base, m.Commit, why, strings.Join(added, ", "))
|
|
// The ask carries the warrant just checked, for this repository and these module paths alone: the guard of a build
|
|
// through a verb (verbMayAsk) lets exactly that through.
|
|
paths := make([]string, 0, len(added))
|
|
for _, dir := range added {
|
|
if dir == "." {
|
|
dir = ""
|
|
}
|
|
paths = append(paths, dir)
|
|
}
|
|
askBuilds(withFirstBuildWarrant(ctx, repository, paths, why), inv, m, buildSource{Repository: repository, Seat: gitSeat}, added)
|
|
// Answered: it no longer waits, so the word's arrival again asks nothing twice.
|
|
return inv.AnswerMerge(ctx, repository, m.Commit, inventory.WaitingRecord, false)
|
|
}
|
|
|
|
// resumeFirstBuilds runs firstBuilds for the first merges waiting for the word mesh-delivery just announced for a pull
|
|
// request's head: the other order of the same two events.
|
|
func (f following) resumeFirstBuilds(ctx context.Context, d deliveryMerge) error {
|
|
actingOnMerges.Lock()
|
|
defer actingOnMerges.Unlock()
|
|
inv := f.open.inventory
|
|
waiting, err := inv.WaitingMerges(ctx)
|
|
if err != nil {
|
|
return notNow(err)
|
|
}
|
|
for _, w := range waiting {
|
|
k := announcedOf(w)
|
|
if !k.FirstBuild || !strings.EqualFold(k.Owner+"/"+k.Repo, d.Owner+"/"+d.Repo) || k.Number != d.Number ||
|
|
k.HeadSHA != d.HeadSHA {
|
|
continue
|
|
}
|
|
entries, err := inv.Catalogued(ctx)
|
|
if err != nil {
|
|
return notNow(err)
|
|
}
|
|
read, err := readForPlanning(ctx, inv)
|
|
if err != nil {
|
|
return notNow(err)
|
|
}
|
|
added, why, err := firstBuildsOf(ctx, inv, k.SourceMoved, entries, read)
|
|
switch {
|
|
case errors.Is(err, errFirstNotApproved):
|
|
fmt.Printf("%s/%s merged into %s (%.8s) was waiting for mesh-delivery's word, and no build is asked: %v\n",
|
|
k.Owner, k.Repo, k.Base, k.Commit, err)
|
|
if aerr := inv.AnswerMerge(ctx, w.Repository, w.Commit, inventory.WaitingRecord, false); aerr != nil {
|
|
return notNow(aerr)
|
|
}
|
|
case err != nil:
|
|
return notNow(err)
|
|
case len(added) > 0:
|
|
if err := askFirstBuilds(ctx, inv, k.SourceMoved, time.Now().UTC(), added, why); err != nil {
|
|
return notNow(err)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// errFirstNotApproved says a merge into a repository the mesh builds nothing from is not one the operator approved.
|
|
var errFirstNotApproved = errors.New("it is no merge made on the operator's Approve of its head, so a hand `build` " +
|
|
"registers a module from a repository the mesh does not build yet")
|
|
|
|
// addedDirs are the module directories the merge adds to a repository no registered module is built from, none where
|
|
// the merge is not into the trunk or some module is built from the repository already.
|
|
func addedDirs(m link.SourceMoved, entries []inventory.Entry, read map[string][]inventory.ReadRepository) []string {
|
|
if m.Base != "main" {
|
|
return nil
|
|
}
|
|
for _, e := range entries {
|
|
if !e.Provided && sameRepository(e.Source.Repository, m) {
|
|
return nil
|
|
}
|
|
}
|
|
_, added, _ := touchedBy(nil, entries, m, read)
|
|
return added
|
|
}
|
|
|
|
// firstBuildsOf is the directories whose first build a merge asks, and the warrant it was made on; none when the merge
|
|
// is no first merge into a repository the mesh builds nothing from. errFirstNotApproved when it is and the word on record
|
|
// is no Approve of its head; errNotAnnounced when mesh-delivery's word is not kept yet.
|
|
func firstBuildsOf(ctx context.Context, inv catalogueReader, m link.SourceMoved, entries []inventory.Entry,
|
|
read map[string][]inventory.ReadRepository) ([]string, string, error) {
|
|
added := addedDirs(m, entries, read)
|
|
if len(added) == 0 {
|
|
return nil, "", nil
|
|
}
|
|
why, err := approvedAnnounced(ctx, inv, m.Owner, m.Repo, keptMerge{SourceMoved: m})
|
|
switch {
|
|
case err == nil:
|
|
return added, why, nil
|
|
case errors.Is(err, errWordNotAnApprove):
|
|
return nil, "", fmt.Errorf("%w (%v)", errFirstNotApproved, err)
|
|
}
|
|
// Not announced yet, or the bus or the store could not be asked: the caller waits or tries again, and asks nothing.
|
|
return nil, "", err
|
|
}
|
|
|
|
// probeFirstBuildsID is the probe that reads the first merges waiting for mesh-delivery's word.
|
|
const probeFirstBuildsID = "D18"
|
|
|
|
// kindFirstBuildUnasked is what a first merge waiting too long for mesh-delivery's word raises.
|
|
const kindFirstBuildUnasked = "first-build-unasked"
|
|
|
|
// firstBuildWaitBound is how long a first merge waits for mesh-delivery's announcement of the operator's Approve before
|
|
// it is said: a bound in the sense of ADR 0426 §3, not a delay. Nothing waits on it: the word's arrival asks the builds
|
|
// at once, and the merge is said, never dropped, once it is past this (issue 1146).
|
|
const firstBuildWaitBound = 10 * time.Minute
|
|
|
|
// firstBuildObservations are the conditions of the first merges waiting past the bound, one for each; a merge answered is
|
|
// no longer listed as waiting, so its condition clears with the next look.
|
|
func firstBuildObservations(waiting []inventory.BatchedMerge, now time.Time) []conditions.Observation {
|
|
var out []conditions.Observation
|
|
for _, w := range waiting {
|
|
k := announcedOf(w)
|
|
if !k.FirstBuild || now.Sub(w.Heard) <= firstBuildWaitBound {
|
|
continue
|
|
}
|
|
repository := k.Owner + "/" + k.Repo
|
|
for_ := now.Sub(w.Heard).Round(time.Minute)
|
|
out = append(out, conditions.Observation{Scope: conditions.ScopeMerge,
|
|
ID: fmt.Sprintf("%s_%.8s", repository, k.Commit), Kind: kindFirstBuildUnasked, Severity: conditions.Warning,
|
|
Summary: fmt.Sprintf("the merge %.8s of %s adds modules the mesh builds nothing from yet, and has waited %s for "+
|
|
"mesh-delivery's announcement of the operator's Approve of its head: no build is asked", k.Commit, repository, for_),
|
|
Said: fmt.Sprintf("%s at %s waits for mesh-delivery's word on pull request %d (head %.8s)", repository, k.Commit,
|
|
k.Number, k.HeadSHA),
|
|
Headline: fmt.Sprintf("The first build of %s is not asked: no Approve was announced", repository),
|
|
Explanation: fmt.Sprintf("The merge %.8s into %s adds modules the mesh builds nothing from yet. It registers them "+
|
|
"only when mesh-delivery announces that it merged the pull request on your Approve of that head, and no such "+
|
|
"announcement has come in %s. The merge is kept, and the builds are asked the moment the announcement is kept.",
|
|
k.Commit, repository, for_),
|
|
Resolved: "mesh-delivery announces the merge, or a hand `build` registers the modules",
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// probeFirstBuilds is D18: no first merge waits past its bound for mesh-delivery's word unsaid.
|
|
func probeFirstBuilds(ctx context.Context, d *doctor) ([]conditions.Observation, error) {
|
|
waiting, err := d.open.inventory.WaitingMerges(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return firstBuildObservations(waiting, time.Now().UTC()), nil
|
|
}
|