Files
mesh-delivery/modules/mesh-delivery-checker/internal/check/pins.go
T

178 lines
7.3 KiB
Go

package check
import (
"context"
"errors"
"fmt"
"path"
"regexp"
"sort"
"strings"
)
// The pin of mesh-sdk in a Go member's go.mod (novox/hq ADR 0484 clause 9, ADR 0482 clause 2, issue 1624): a delivery group
// has one verdict, and the mesh-sdk its Go members are built and tested with is the group's: a pin to the head of the group's
// mesh-sdk pull request, or to a commit already on mesh-sdk's trunk. Any other commit is a stale or a foreign pin, and the
// group fails naming the member and the commit.
// SDKRepository is the repository of mesh-sdk, and SDKModulePath its Go module.
const (
SDKRepository = "novox/mesh-sdk"
SDKModulePath = "git.novox.be/novox/mesh-sdk/go"
)
// Issue1148 is why a pin that only a head carries fails the check.
const Issue1148 = "issue 1148"
// ErrNotFound is what Forge.File wraps when the ref holds no such file (the forge answered 404): a file the ref does not hold,
// told from a forge that could not be read (issue 1845).
var ErrNotFound = errors.New("the ref holds no such file")
// Forge reads a file of a repository at a ref, and says whether a commit is on a repository's trunk.
type Forge interface {
// File is the text of a file at a ref (a branch, a tag or a commit).
File(ctx context.Context, repository, path, ref string) (string, error)
// OnTrunk says whether commit (a full or a 12-digit sha) is already merged into the trunk of the repository: it holds no
// commit the trunk does not.
OnTrunk(ctx context.Context, repository, commit string) (bool, error)
// FullSHA is the 40-digit sha of a commit named by its first 12 digits.
FullSHA(ctx context.Context, repository, commit string) (string, error)
// BranchHeads is the commit every branch of a repository points at, by branch name (issue 1921).
BranchHeads(ctx context.Context, repository string) (map[string]string, error)
}
var pseudoVersion = regexp.MustCompile(`-([0-9a-f]{12})$`)
// SDKPin is the commit (12 hex digits) a go.mod names for mesh-sdk by a pseudo-version, or none for a go.mod that does not
// require mesh-sdk or requires a version that is no pseudo-version (a tag is a release the SDK itself names).
func SDKPin(goMod string) (commit string, ok bool) {
for _, line := range strings.Split(goMod, "\n") {
f := strings.Fields(strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), "require")))
if len(f) >= 2 && f[0] == SDKModulePath {
if m := pseudoVersion.FindStringSubmatch(f[1]); m != nil {
return m[1], true
}
return "", false
}
}
return "", false
}
// checkPinsOf refuses a Go member whose go.mod names a mesh-sdk commit that is neither on mesh-sdk's trunk nor the head of the
// group's mesh-sdk pull request. It reads each go.mod the fetch named from the forge: the one the check builds, which is the
// head's when the head holds the file and the trunk's when it does not, since a module added on the trunk after the branch was
// cut is in the merge and not at the head (issue 1845). It answers nil when every pin is the group's.
func (st *state) checkPinsOf(ctx context.Context, t *target) verdictPtr {
if st.j.Forge == nil {
return nil
}
sdk := st.c.member(SDKRepository)
if t.module != goModule || t.pull.Repository == SDKRepository {
return nil
}
{
for _, dir := range t.fetched.Modules {
file := path.Join(dir, "go.mod")
st.c.Progress.Set(fmt.Sprintf("read %s of %s#%d", file, t.pull.Repository, t.pull.Number), t.node)
text, err := st.j.Forge.File(ctx, t.pull.Repository, file, t.pull.Head)
if errors.Is(err, ErrNotFound) && t.pull.Trunk != "" {
// Only a file the head does not hold is read at the trunk; any other failure fails the check as it did.
text, err = st.j.Forge.File(ctx, t.pull.Repository, file, t.pull.Trunk)
}
if err != nil {
return failed(forgeMachine(err, t.node), fmt.Sprintf("%s#%d: %s cannot be read to check its mesh-sdk pin: %v%s", t.pull.Repository, t.pull.Number, file, err, unheardWords(err)))
}
pin, ok := SDKPin(text)
if !ok {
continue
}
if sdk != nil && strings.HasPrefix(sdk.Head, pin) {
continue // the group's own head
}
onTrunk, err := st.j.Forge.OnTrunk(ctx, SDKRepository, pin)
if err != nil {
return failed(forgeMachine(err, t.node), fmt.Sprintf("%s#%d: whether mesh-sdk %s is on its trunk cannot be told: %v%s", t.pull.Repository, t.pull.Number, pin, err, unheardWords(err)))
}
if onTrunk {
continue
}
if st.c.Try != nil {
// A try only (issue 1921): a repository tried on a branch that mesh-sdk shares by name (a milestone's branch across
// repositories) is built with that branch's mesh-sdk head, which no group pull request holds before the pull requests exist.
branch, err := st.sameBranchPin(ctx, t, pin)
if err != nil {
return failed(forgeMachine(err, t.node), fmt.Sprintf("%s#%d: whether mesh-sdk %s is the head of a branch cannot be told: %v%s", t.pull.Repository, t.pull.Number, pin, err, unheardWords(err)))
}
if branch != "" {
st.c.note(fmt.Sprintf("pin accepted: head of mesh-sdk %s", branch))
continue
}
}
nor := "the group has no mesh-sdk pull request"
if sdk != nil {
nor = "the head of the group's mesh-sdk pull request is " + short12(sdk.Head)
}
return failed(t.node, fmt.Sprintf("%s#%d: %s names mesh-sdk %s, which is neither on mesh-sdk's trunk nor the group's head (%s)", t.pull.Repository, t.pull.Number, file, pin, nor))
}
}
return nil
}
// sameBranchPin names the branch of the tried repository that points at the tried head and that mesh-sdk also has, at the
// commit the pin names; empty when there is none.
func (st *state) sameBranchPin(ctx context.Context, t *target, pin string) (string, error) {
mine, err := st.j.Forge.BranchHeads(ctx, t.pull.Repository)
if err != nil {
return "", err
}
var same []string
for name, head := range mine {
if head == t.pull.Head && name != "main" {
same = append(same, name)
}
}
if len(same) == 0 {
return "", nil
}
sdk, err := st.j.Forge.BranchHeads(ctx, SDKRepository)
if err != nil {
return "", err
}
sort.Strings(same)
for _, name := range same {
if h, ok := sdk[name]; ok && strings.HasPrefix(h, pin) {
return name, nil
}
}
return "", nil
}
// failureVerdict is a verdict of failure the check ends with.
// pinHint is added to the failure of a Go member's fetch that names mesh-sdk while the group holds a mesh-sdk pull request:
// a pin to that pull request's head names a commit only the head carries, which go mod download cannot resolve before the
// pull request is merged (issue 1148), so such a check fails and never passes.
func pinHint(c *Case, fetchError string) string {
if c.member(SDKRepository) == nil || !strings.Contains(fetchError, "mesh-sdk") {
return ""
}
return " (a pin to the head of the group's mesh-sdk pull request names a commit only that head carries, which go mod download cannot resolve; " + Issue1148 + ")"
}
// unheardWords ends the words of a failed read of the forge that reached nobody: the forge was not heard (UnheardWords).
func unheardWords(err error) string {
if isUnheard(err) {
return " (the forge was " + UnheardWords + ")"
}
return ""
}
// forgeMachine is the machine a failed read of the forge is put at: the forge when nobody answered it (no build node is to
// blame, issue 1820), the build node whose workspace was read otherwise.
func forgeMachine(err error, node string) string {
if isUnheard(err) {
return "forge"
}
return node
}