164 lines
6.9 KiB
Go
164 lines
6.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"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, ", "))
|
|
askBuilds(ctx, 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
|
|
}
|