Files
mesh-controller/internal/builder/check_base.go
T
jochen cb0327de7f
mesh/delivery superseded: a newer delivery to the same trunk took over its walk
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
Judge a pull request by the base branch's merge-check.sh, not its own copy
A change could delete or gut its merge-check.sh and skip its own tests, and a
branch cut before the script escaped them. Where the base branch holds one, its
script, declared parts and toolchain run against the change's tree, and the
verdict says when the change lacks or alters the check (novox/hq issue 310).
2026-10-08 10:58:19 +02:00

135 lines
5.0 KiB
Go

package builder
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
// **The base branch's check judges a pull request** (novox/hq issue 310).
//
// A repository's own check is its merge-check.sh, the parts it declares (`# mesh-check-also:`) and the
// toolchain it names. Were the check the one in the pull request's head, a change could delete or weaken
// the script and so skip its own tests — and a branch cut before the repository had a script escaped
// its tests without deleting anything. So when the base branch holds a merge-check.sh, **the base
// branch's** script, its declared parts and its toolchain run against the change's tree, and the verdict
// says so whenever the change lacks the script or alters any of it.
//
// A change that legitimately alters the check is judged by the check it replaces: its own version judges
// the pull requests after it merges. A base branch with no script keeps what was so before — the head's,
// when it brings one, and a warning when it brings none.
// Judging is the check that judges a change, and what of it the base branch puts in the change's tree.
type Judging struct {
// Script is the merge-check.sh that judges, nil when neither the base branch nor the change holds one.
Script []byte
// Said is what the verdict says of it, empty when the change's own script runs as the base's would.
Said string
// files are the base branch's scripts, by path in the tree, put there before the check runs.
files map[string][]byte
}
// Put writes the base branch's scripts into the change's tree, in place of the change's own.
func (j Judging) Put(tree string) error {
for path, body := range j.files {
at := filepath.Join(tree, path)
if err := os.MkdirAll(filepath.Dir(at), 0o755); err != nil {
return err
}
if err := os.WriteFile(at, body, 0o755); err != nil {
return fmt.Errorf("the base branch's %s cannot be put in the change's tree: %w", path, err)
}
}
return nil
}
// TheCheckThatJudges is the check that judges the change cloned at tree, merging into base: the base
// branch's when it holds one, the change's own otherwise. An error is that the base branch could not be
// read — never a reason to fall back to the change's script.
func TheCheckThatJudges(ctx context.Context, tree, base string) (Judging, error) {
head, err := os.ReadFile(filepath.Join(tree, CheckScript))
if err != nil && !errors.Is(err, os.ErrNotExist) {
return Judging{}, err
}
if err != nil {
head = nil
}
if base == "" {
return Judging{Script: head}, nil
}
ref := "origin/" + base
if _, err := gitIn(ctx, tree, "rev-parse", "--verify", "--quiet", ref+"^{commit}"); err != nil {
return Judging{}, fmt.Errorf("the base branch %s cannot be read, and the check it holds judges the change: %w",
base, err)
}
script, held, err := onBranch(ctx, tree, ref, CheckScript)
if err != nil {
return Judging{}, err
}
if !held {
return Judging{Script: head}, nil
}
j := Judging{Script: script, files: map[string][]byte{CheckScript: script}}
var altered []string
if head != nil && !bytes.Equal(head, script) {
altered = append(altered, CheckScript)
}
for _, part := range ScriptParts(script)[1:] {
if !filepath.IsLocal(part.Script) {
continue // refused by name when the check runs
}
body, held, err := onBranch(ctx, tree, ref, part.Script)
if err != nil {
return Judging{}, err
}
if !held {
continue // the base branch's own check lacks it: the change's, if it brings it
}
j.files[filepath.Clean(part.Script)] = body
if own, err := os.ReadFile(filepath.Join(tree, part.Script)); err != nil || !bytes.Equal(own, body) {
altered = append(altered, part.Script)
}
}
switch {
case head == nil:
j.Said = fmt.Sprintf("the change holds no %s: %s's judged it", CheckScript, base)
if len(altered) > 0 {
j.Said += fmt.Sprintf(", and the change alters its part(s) %s", strings.Join(altered, ", "))
}
case len(altered) > 0:
j.Said = fmt.Sprintf("THE CHANGE ALTERS ITS OWN CHECK (%s): %s's version judged it; the change's judges "+
"the pull requests after it merges", strings.Join(altered, ", "), base)
}
return j, nil
}
// onBranch is a file as a ref has it, and whether the ref holds it at all.
func onBranch(ctx context.Context, tree, ref, path string) ([]byte, bool, error) {
at := ref + ":" + filepath.ToSlash(filepath.Clean(path))
// ls-tree answers nothing for a path the ref does not hold, and fails only when it cannot read.
listed, err := gitIn(ctx, tree, "ls-tree", ref, "--", filepath.ToSlash(filepath.Clean(path)))
if err != nil {
return nil, false, fmt.Errorf("%s cannot be read: %w", ref, err)
}
if fields := strings.Fields(string(listed)); len(fields) < 2 || fields[1] != "blob" {
return nil, false, nil // the ref holds no such file
}
body, err := gitIn(ctx, tree, "cat-file", "blob", at)
if err != nil {
return nil, false, fmt.Errorf("%s cannot be read: %w", at, err)
}
return body, true, nil
}
func gitIn(ctx context.Context, dir string, args ...string) ([]byte, error) {
cmd := exec.CommandContext(ctx, "git", args...)
cmd.Dir = dir
return cmd.Output()
}