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.
135 lines
5.7 KiB
Go
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
|
|
}
|