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 ] [--base main] [--registry ] [--forge ] // [--number ] [--user uid:gid] [--facts store|] [--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 /.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 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) }