178 lines
7.3 KiB
Go
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
|
|
}
|