Files
mesh-catalog/modules/mesh-build-node/cmd/mesh-build-node/steps.go
T
jschoubben 4ad5f40817 BLD7, BLD8: the Node and image build seats serve the verb steps
Node answers the type-check as lint always, unit where a package has a test
script and integration where it has test:integration, kept in detected-steps by
fingerprint as the Go module keeps it; the image answers none. The manifests
serve it. The step ask's end (step-ended) of BLD7 and BLD8 waits for BLD3 and
the build-step package of mesh-delivery-sdk.

Builds: design 47 G §22, §307, §326, §328

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
2026-11-11 11:06:40 +01:00

160 lines
5.6 KiB
Go

package main
// **The verb `steps`** (design 47 G §22, §307, §326, §328): which pipeline steps a TypeScript artifact can run, by the
// convention check.go tests by. The type-check is the lint step (§328) and is always one; the unit step is the packages
// holding a package.json script `test`, the integration step those holding `test:integration`. A level with none is not
// answered. The answer is kept in the state detected-steps by the artifact's fingerprint, replaced by a newer one and by
// again, as the Go build module keeps it.
import (
"context"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
"git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildnode"
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/forgekey"
)
// The pipeline steps `steps` answers (the contract's DetectedStep enum).
const (
StepLint = "lint"
StepUnit = "unit"
StepIntegration = "integration"
)
// DetectedStep is the contract's.
type DetectedStep = seat.DetectedStep
// StepsAnswer is what `steps` answers.
type StepsAnswer struct {
Artifact string `json:"artifact"`
Fingerprint string `json:"fingerprint,omitempty"`
Steps []DetectedStep `json:"steps"`
Kept bool `json:"kept,omitempty"`
}
// detectSteps finds the steps of the package directories under dir.
func detectSteps(dir string) ([]DetectedStep, error) {
targets, err := testTargets(dir, ".")
if err != nil {
return nil, err
}
unit, integration := []string{}, []string{}
for _, tg := range targets {
scripts, err := packageScripts(tg.dir)
if err != nil {
return nil, err
}
if scripts["test"] != "" {
unit = append(unit, tg.where)
}
if scripts["test:integration"] != "" {
integration = append(integration, tg.where)
}
}
steps := []DetectedStep{{Step: StepLint}}
for _, l := range []struct {
step string
pkgs []string
}{{StepUnit, unit}, {StepIntegration, integration}} {
if len(l.pkgs) > 0 {
sort.Strings(l.pkgs)
steps = append(steps, DetectedStep{Step: l.step, Packages: l.pkgs})
}
}
return steps, nil
}
// stepsTool is the seat's verb `steps`.
func (m *Module) stepsTool() stdio.Tool {
return stdio.Tool{Name: Seat + "." + seat.VerbSteps,
Description: "Which pipeline steps a TypeScript artifact can run: the type-check as lint, always; unit tests where a package has a test script; " +
"integration tests where one has a test:integration script. A level with no test is not answered.",
Input: map[string]any{"type": "object", "required": []string{"module", "artifact", "fetch"}, "properties": map[string]any{
"module": map[string]any{"type": "string"},
"artifact": map[string]any{"type": "string", "description": "the artifact reference, <module>:<artifact>"},
"fingerprint": map[string]any{"type": "string"},
"fetch": map[string]any{"type": "object", "description": "what the fetch tool is asked: the repository, the full commit and the module's directory"},
"again": map[string]any{"type": "boolean", "description": "find them anew, not from the kept answer"},
}},
Run: m.Short(m.steps)}
}
func (m *Module) steps(ctx context.Context, args map[string]any) (any, error) {
str := func(k string) string { s, _ := args[k].(string); return s }
fetch, _ := args["fetch"].(map[string]any)
req, err := sdkstep.ParseFetchRequest(fetch)
if err != nil {
return nil, err
}
if str("artifact") == "" || str("module") == "" {
return nil, fmt.Errorf("steps: the module and the artifact are asked")
}
artifact, fp := str("artifact"), str("fingerprint")
name := artifact
if _, n, ok := strings.Cut(artifact, ":"); ok {
name = n
}
key := seat.DetectedStepsByModuleArtifactKey(str("module"), name)
if again, _ := args["again"].(bool); !again && fp != "" && m.ReadDetected != nil {
if raw, err := m.ReadDetected(key); err == nil && len(raw) > 0 {
if kept, err := seat.DecodeDetectedStepsByModuleArtifactValue(raw); err == nil && kept.Fingerprint == fp {
return StepsAnswer{Artifact: artifact, Fingerprint: fp, Steps: kept.Steps, Kept: true}, nil
}
}
}
steps, err := m.stepsOfTree(ctx, req)
if err != nil {
return nil, err
}
if fp != "" && m.Detected != nil {
at := time.Now()
if m.Clock != nil {
at = m.Clock()
}
if _, err := m.Detected(key, seat.DetectedSteps{V: 1, Fingerprint: fp, Steps: steps, At: &at}); err != nil {
return nil, fmt.Errorf("steps: keeping the answer: %w", err)
}
}
return StepsAnswer{Artifact: artifact, Fingerprint: fp, Steps: steps}, nil
}
// stepsOfTree fetches the commit alone (nothing installed) into a workspace removed at the end.
func (m *Module) stepsOfTree(ctx context.Context, req sdkstep.FetchRequest) ([]DetectedStep, error) {
if err := os.MkdirAll(m.Work, 0o700); err != nil {
return nil, err
}
root, err := os.MkdirTemp(m.Work, "steps-")
if err != nil {
return nil, err
}
defer os.RemoveAll(root)
tree := filepath.Join(root, "tree")
if err := os.MkdirAll(tree, 0o700); err != nil {
return nil, err
}
gitRun := func(ctx context.Context, env []string, name string, a ...string) forgekey.Ran {
return m.Run(ctx, append(m.baseEnv(), env...), name, a...)
}
if err := buildstep.FetchCommit(ctx, gitRun, tree, m.remote(req.Repository), req.Commit, req.Ref, m.FetchOptions...); err != nil {
return nil, err
}
if len(req.Paths) > 0 {
if err := buildstep.Narrow(tree, req); err != nil {
return nil, err
}
}
dir := tree
if req.Dir != "" && req.Dir != "." {
dir = filepath.Join(tree, filepath.FromSlash(req.Dir))
}
return detectSteps(dir)
}