202 lines
8.1 KiB
Go
202 lines
8.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/builder"
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"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 283): 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)
|
|
}
|
|
changedText, err := git("diff", "--name-only", "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 283, 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)
|
|
}
|