Files
mesh-catalog/modules/mesh-build-go/cmd/mesh-build-go/candidate.go
T
jschoubben 071c6885d9
mesh/delivery merged: source.main moved to novox/mesh-catalog@61563abf2f93
mesh/delivery-group group fix/fetch-reads-all merged: every member is merged
mesh/check checking 071c6885d923 on 8393e2962193
The mesh's forge accounts all read every repository, and a build's fetch says the forge's own words
mesh-catalog #495 gave read on every repository to the tools' account alone. The build modules
fetch over ssh as nox-builder (the forge-account provider's builders account, ADR 0466 §3), so
ace's and g14's fetch of jschoubben/car-hunter and jschoubben/snake still failed; the machines'
agents (nox-agent, ADR 0410 §1) could not read them either.

- gitea: readall.ts grants every account of one list, meshAccounts in tools-account.ts (mesh-tools,
  nox-builder, nox-agent; a Go test holds the two Go names to internal/forgeaccount's constants),
  on the same start pass, forge events and tools. gitea_unreadable_repos answers per account and
  names the accounts the forge does not hold yet; gitea_grant_read takes accounts as well as
  repositories. An absent account is asked nothing; the start pass runs again ten minutes apart
  until every account was there once.
- mesh-build-go, mesh-build-node, mesh-build-image: pin mesh-sdk ff81bdf8 (fix/fetch-reads-all),
  whose forgekey.Said keeps git's words past the bare "error:" line: the mirror's line said only
  "git -C: error:". Their own git reasons (ls-files, ls-remote, rev-parse, repack) go through it too.
2026-10-26 15:27:38 +01:00

135 lines
5.7 KiB
Go

package main
// A change to this module is judged by its own candidate (novox/hq ADR 0535; issue 1898): asked with the runner
// workspace, the digest and test steps compile the program of mesh-build-go from the workspace's merged tree in a run
// step, shut off, and run the step as that program in one shut-off process (`<candidate> level <spec>`, level.go), where
// this program would run the step's commands. This program keeps the fetch, the forge key and the bus login; the
// candidate's program holds none of them. Each answer says which program ran it (judged_by).
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"git.novox.be/novox/mesh-sdk/go/forgekey"
)
// The runners of a digest or test step: this installed program, or the candidate's compiled from the workspace.
const (
RunnerInstalled = "installed"
RunnerWorkspace = "workspace"
)
// runnerProperty is the runner argument as the digest and test tools announce it: a checker reads it there, and an
// installed module that does not announce it judges as it always did (ADR 0535 §2).
var runnerProperty = map[string]any{"type": "string", "enum": []string{RunnerInstalled, RunnerWorkspace},
"description": "optional: installed (the default) runs the step as this installed program; workspace compiles the " +
"workspace's own mesh-build-go (modules/mesh-build-go of the merged tree) shut off and runs the step as it, for a " +
"change to this module (ADR 0535); the answer's judged_by names the program that ran"}
// candidateModule is the directory of this module's go.mod in the catalogue's tree, and candidateMain its program's
// package there.
const (
candidateModule = "modules/mesh-build-go"
candidateMain = "./cmd/mesh-build-go"
)
// runnerArg reads the runner argument: "" (the installed program), installed or workspace.
func runnerArg(v any) (string, error) {
if v == nil {
return "", nil
}
s, ok := v.(string)
if !ok || (s != "" && s != RunnerInstalled && s != RunnerWorkspace) {
return "", fmt.Errorf("the runner %v is neither %s nor %s", v, RunnerInstalled, RunnerWorkspace)
}
return s, nil
}
// workspaceThere refuses a workspace id that is not one, or a workspace this node never fetched.
func (m *Module) workspaceThere(workspace string) error {
if !workspaceOK.MatchString(workspace) {
return fmt.Errorf("%q is not a workspace id", workspace)
}
if _, err := os.Stat(filepath.Join(m.Work, workspace, "tree")); err != nil {
return fmt.Errorf("no workspace %s: it was never fetched here, or it is older than a day and was removed", workspace)
}
return nil
}
// candidateLocks hold one mutex per workspace, so its candidate is compiled once.
var candidateLocks sync.Map
// candidate is the program of mesh-build-go compiled from the workspace's tree, in the workspace beside its tree
// (candidate/mesh-build-go), compiled once per workspace, in a run step shut off with the caches read-only and nothing
// fetched (GOPROXY=off: the fetch step downloaded every module the tree's go.mod files name). A candidate that does not
// compile is the step's failure, naming the compiler's first error (ADR 0535 §3).
func (m *Module) candidate(ctx context.Context, workspace string) (string, error) {
exe := filepath.Join(m.Work, workspace, "candidate", "mesh-build-go")
mu, _ := candidateLocks.LoadOrStore(exe, &sync.Mutex{})
mu.(*sync.Mutex).Lock()
defer mu.(*sync.Mutex).Unlock()
if st, err := os.Stat(exe); err == nil && st.Mode().IsRegular() {
return exe, nil
}
dir := filepath.Join(m.Work, workspace, "tree", candidateModule)
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err != nil {
return "", fmt.Errorf("the workspace holds no %s/go.mod, so it has no candidate mesh-build-go to run", candidateModule)
}
if err := os.MkdirAll(filepath.Dir(exe), 0o700); err != nil {
return "", err
}
run, err := m.stepRunner(workspace, false)
if err != nil {
return "", err
}
env := append(m.goEnvFor(dir), "GOPROXY=off", "GOTOOLCHAIN=local")
r := run(ctx, env, "go", "-C", dir, "build", "-o", exe, candidateMain)
if r.Status != 0 || r.Err != "" {
return "", fmt.Errorf("the candidate's mesh-build-go does not compile: %s", compileError(r.Err+"\n"+r.Stderr+"\n"+r.Stdout))
}
return exe, nil
}
// compileError is the first line of the compiler's output that says an error: not the `# package` heading go build
// prints above a package's errors, nor Go's progress.
func compileError(out string) string {
for _, l := range strings.Split(out, "\n") {
if l = strings.TrimSpace(l); l != "" && !strings.HasPrefix(l, "#") && !strings.HasPrefix(l, "go: downloading ") {
return l
}
}
return firstLine(out)
}
// mergeCommit is the commit the workspace's tree holds: the merge the fetch made, or the head it fetched.
func (m *Module) mergeCommit(ctx context.Context, workspace string) (string, error) {
r := m.Run(ctx, nil, "git", "-C", filepath.Join(m.Work, workspace, "tree"), "rev-parse", "HEAD")
commit := strings.TrimSpace(r.Stdout)
if r.Status != 0 || r.Err != "" || commit == "" {
return "", fmt.Errorf("the workspace's commit: git rev-parse: %s", forgekey.Said(r.Err+"\n"+r.Stderr))
}
return commit, nil
}
// asCandidate runs the spec's step as the candidate's program, shut off, and answers what it said and the judged_by of
// the answer: "candidate <merge commit>".
func (m *Module) asCandidate(ctx context.Context, spec levelSpec) (levelAnswer, string, error) {
commit, err := m.mergeCommit(ctx, spec.Workspace)
if err != nil {
return levelAnswer{}, "", err
}
exe, err := m.candidate(ctx, spec.Workspace)
if err != nil {
return levelAnswer{}, "", err
}
a, err := m.shutOff(ctx, exe, spec)
if err != nil {
return levelAnswer{}, "", fmt.Errorf("the candidate's mesh-build-go: %w", err)
}
return a, "candidate " + commit, nil
}