Swaps three import paths for mesh-sdk's go/catalogue, go/overlay and go/validate (the same packages, copied from this repository at 6c2a8d62) and lets gofmt order the groups. The six swap commits compile together once the pin commit (go.mod, modules.txt) is on the branch; each alone does not, because a file on the SDK's types cannot call one still on the old package.
204 lines
8.3 KiB
Go
204 lines
8.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"github.com/novox/mesh-controller/internal/builder"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// `check-here` is a pull request's merge check run on the machine at hand **exactly as the build seat
|
|
// runs it** (novox/hq issue 286): the same code (builder.Check), the same ask the controller would make of
|
|
// the seat — the gate's modules and judge by the planner's own answer over the facts snapshot, the
|
|
// repositories beside it at the refs the snapshot says the seat clones them at — in the toolchain image
|
|
// the mesh holds, as the user the build seat runs as, against a throwaway store and bus of the versions
|
|
// the mesh runs.
|
|
//
|
|
// **The build seat is the reference.** A merge-check.sh that passed on an agent's machine failed on the
|
|
// seat for three reasons that were each the agent's environment, never the change: a newer Go whose gofmt
|
|
// lays a file out differently, a sibling checkout at the agent's feature branch where the seat had the
|
|
// commit the mesh runs, and a different user. Run here, a check sees what the seat will.
|
|
//
|
|
// check-here [--tree <checkout>] [--base main] [--registry <artifact store>] [--forge <url of the owner>]
|
|
// [--number <n>] [--user uid:gid] [--facts store|<file>] [--keep]
|
|
//
|
|
// The checkout's HEAD is what is checked, and it must be committed: the seat checks a commit, never a
|
|
// working tree.
|
|
func checkHereCommand(ctx context.Context, args []string) error {
|
|
set := flag.NewFlagSet("check-here", flag.ContinueOnError)
|
|
tree := set.String("tree", ".", "the change's checkout; its HEAD is checked")
|
|
base := set.String("base", "main", "the branch the change would merge into")
|
|
registry := set.String("registry", os.Getenv("MESH_REGISTRY"), "the artifact store holding the facts and the toolchains")
|
|
forge := set.String("forge", "", "where the repositories beside it are cloned from, as <forge>/<repository>.git; "+
|
|
"the checkout's origin without its own name when not given")
|
|
number := set.Int("number", 0, "the pull request's number, when there is one")
|
|
// The build seat's service runs as root, and its check containers run as the builder does.
|
|
user := set.String("user", "0:0", "the user the check's containers run as: the build seat's")
|
|
keep := set.Bool("keep", false, "keep the workspace afterwards")
|
|
factsFrom := set.String("facts", "store", "the facts snapshot: `store`, the one the artifact store holds — "+
|
|
"what the seat reads — or a file")
|
|
if _, err := parseAround(set, args); err != nil {
|
|
return err
|
|
}
|
|
if *registry == "" {
|
|
return errors.New("check-here reads the facts and the toolchains from the artifact store: --registry <host:port> or MESH_REGISTRY")
|
|
}
|
|
dir, err := filepath.Abs(*tree)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
git := func(args ...string) (string, error) {
|
|
cmd := exec.CommandContext(ctx, "git", args...)
|
|
cmd.Dir = dir
|
|
out, err := cmd.Output()
|
|
return strings.TrimSpace(string(out)), err
|
|
}
|
|
if dirty, err := git("status", "--porcelain", "--untracked-files=no"); err != nil {
|
|
return fmt.Errorf("%s is not a checkout: %w", dir, err)
|
|
} else if dirty != "" {
|
|
return errors.New("the checkout has changes not committed: the build seat checks a commit, so commit first")
|
|
}
|
|
head, err := git("rev-parse", "HEAD")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
origin, err := git("remote", "get-url", "origin")
|
|
if err != nil {
|
|
return fmt.Errorf("the checkout has no origin to say which repository it is: %w", err)
|
|
}
|
|
owner, repo, prefix := ownerRepoOf(origin)
|
|
if *forge == "" {
|
|
*forge = prefix
|
|
}
|
|
if _, err := git("fetch", "--quiet", "origin", *base); err != nil {
|
|
return fmt.Errorf("cannot fetch %s to say what the change touches: %w", *base, err)
|
|
}
|
|
// Without rename detection, so a file moved out of a build source is said under its old name as well:
|
|
// its going is a change to the build that held it (novox/hq ADR 0267).
|
|
changedText, err := git("diff", "--name-only", "--no-renames", "origin/"+*base+"...HEAD")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var paths, removed []string
|
|
for _, p := range strings.Split(changedText, "\n") {
|
|
if p = strings.TrimSpace(p); p == "" {
|
|
continue
|
|
}
|
|
paths = append(paths, p)
|
|
if _, err := os.Stat(filepath.Join(dir, p)); os.IsNotExist(err) {
|
|
removed = append(removed, p)
|
|
}
|
|
}
|
|
|
|
_ = os.Setenv("MESH_REGISTRY", *registry)
|
|
f, err := readFacts(ctx, *factsFrom)
|
|
if err != nil {
|
|
return fmt.Errorf("the facts snapshot cannot be read: %w", err)
|
|
}
|
|
entries, read, edges, err := graphOfFacts(f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p := link.PullUpdated{Owner: owner, Repo: repo, Number: *number, Base: *base, Commit: head, Paths: paths,
|
|
Removed: removed, ModuleDirs: moduleDirsIn(dir, paths), ModuleDirsSaid: true}
|
|
scope := pullScope(p, entries, read, edges)
|
|
_, scriptErr := os.Stat(filepath.Join(dir, builder.CheckScript))
|
|
if !scope.gated() && !scope.Mesh {
|
|
fmt.Printf("%s: %s, and the repository is not the mesh's: the build seat runs nothing for it\n",
|
|
owner+"/"+repo, noModuleTouched)
|
|
return nil
|
|
}
|
|
if !scope.gated() && scriptErr != nil {
|
|
fmt.Printf("%s: %s; %s\n", owner+"/"+repo, noModuleTouched, noMergeCheck)
|
|
return nil
|
|
}
|
|
|
|
toolchains := map[string]string{}
|
|
for language, reference := range f.Versions.Toolchains {
|
|
toolchains[language] = catalogue.Rerouted(reference, *registry)
|
|
}
|
|
if len(toolchains) == 0 {
|
|
return errors.New("the facts snapshot names no toolchain: it was taken by a controller from before " +
|
|
"issue 286, and the seat's toolchain cannot be known here")
|
|
}
|
|
beside := map[string]builder.Beside{}
|
|
for d, ref := range f.Beside {
|
|
from := d
|
|
if d == "mesh-controller-main" {
|
|
from = "mesh-controller"
|
|
}
|
|
beside[d] = builder.Beside{Repository: strings.TrimSuffix(*forge, "/") + "/" + from + ".git", Ref: ref}
|
|
}
|
|
id := fmt.Sprintf("check-here-%d", time.Now().UnixNano())
|
|
spec := builder.CheckSpec{ID: id, Repository: dir, Ref: head, Owner: owner, Repo: repo, Number: *number,
|
|
Paths: paths, Beside: beside, Modules: scope.Modules, New: scope.New, Manifests: scope.Manifests,
|
|
Base: *base, Judge: scope.Judge, Toolchain: toolchains["go"], Toolchains: toolchains, User: *user}
|
|
|
|
workspace, err := os.MkdirTemp("", "mesh-check-here-")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !*keep {
|
|
defer removeWorkspace(spec.Toolchain, workspace, *user)
|
|
}
|
|
fmt.Fprintf(os.Stderr, "checking %s/%s at %.8s as the build seat would, against the facts of %s, in %s\n",
|
|
owner, repo, head, f.Taken.Format(time.RFC3339), workspace)
|
|
v, err := builder.Check(ctx, builder.Command, spec, workspace, *registry, builder.GitCredential{},
|
|
func(step, message string) {
|
|
if step != "output" {
|
|
fmt.Fprintf(os.Stderr, " [%s] %s\n", step, message)
|
|
}
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("the check could not run — on the seat an error, never a pass: %w", err)
|
|
}
|
|
fmt.Println(v.Report)
|
|
fmt.Println()
|
|
fmt.Printf("mesh/merge-gate: %s — %s\n", strings.ToUpper(v.Gate.Verdict), v.Gate.Summary)
|
|
if v.Repo != nil {
|
|
fmt.Printf("mesh/repo-check: %s — %s\n", strings.ToUpper(v.Repo.Verdict), v.Repo.Summary)
|
|
}
|
|
for _, l := range []*builder.Layer{v.Gate, v.Repo} {
|
|
if l != nil && l.Verdict != "pass" && l.Verdict != "warning" {
|
|
return errors.New("the build seat would not pass this change")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ownerRepoOf reads owner, repository and the owner's URL from a remote: ssh://git@host:222/novox/mesh-host.git
|
|
// → novox, mesh-host, ssh://git@host:222/novox.
|
|
func ownerRepoOf(remote string) (string, string, string) {
|
|
trimmed := strings.TrimSuffix(strings.TrimSuffix(remote, "/"), ".git")
|
|
cut := strings.LastIndexAny(trimmed, "/:")
|
|
if cut < 0 {
|
|
return "", trimmed, ""
|
|
}
|
|
repo, prefix := trimmed[cut+1:], trimmed[:cut]
|
|
owner := prefix
|
|
if at := strings.LastIndexAny(prefix, "/:"); at >= 0 {
|
|
owner = prefix[at+1:]
|
|
}
|
|
return owner, repo, prefix
|
|
}
|
|
|
|
// removeWorkspace removes what the check left, written as the seat's user: by a container of that user
|
|
// when it is not this one.
|
|
func removeWorkspace(image, workspace, user string) {
|
|
if image != "" && user != fmt.Sprintf("%d:%d", os.Getuid(), os.Getgid()) {
|
|
// Everything in it — the check's HOME is the workspace, so the toolchain's own files are there too.
|
|
_ = exec.Command("docker", "run", "--rm", "--user", user, "--volume", workspace+":/workspace", image,
|
|
"sh", "-c", "rm -rf /workspace/* /workspace/.[!.]*").Run()
|
|
}
|
|
_ = os.RemoveAll(workspace)
|
|
}
|