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
This commit is contained in:
@@ -77,7 +77,7 @@ func (m *Module) Puller(steps []StepTool, source buildstep.AskSource) *buildstep
|
||||
|
||||
// SeatTools are the seat's verbs on this node (ADR 0525 §10).
|
||||
func (m *Module) SeatTools(p *buildstep.Puller) []stdio.Tool {
|
||||
return buildstep.SeatTools(Seat, "image", p)
|
||||
return append(buildstep.SeatTools(Seat, "image", p), stepsTool())
|
||||
}
|
||||
|
||||
// StepEndTool ends one step left running (issue 1890); the module's steps state is the one it reads and writes.
|
||||
|
||||
@@ -125,7 +125,7 @@ func TestManifestKeepsItsStatesClaimsItsBuildSeatAndDeclaresItsHealthTool(t *tes
|
||||
if want := []string{"keys", MirrorWaitsState, "pauses", "steps"}; !slices.Equal(states, want) {
|
||||
t.Errorf("state = %v, want %v", states, want)
|
||||
}
|
||||
if len(m.Claims) != 1 || m.Claims[0].Name != Seat || !slices.Equal(m.Claims[0].Serves, []string{"current", "drop", "pause", "resume"}) {
|
||||
if len(m.Claims) != 1 || m.Claims[0].Name != Seat || !slices.Equal(m.Claims[0].Serves, []string{"current", "drop", "pause", "resume", "steps"}) {
|
||||
t.Errorf("claims = %v, want the seat %s alone", m.Claims, Seat)
|
||||
}
|
||||
raw, _ := os.ReadFile(filepath.Join(moduleDir, "module.json"))
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
// **The verb `steps`** (design 47 G §22, §307, §328): an image has no lint, unit or integration test step, so `steps`
|
||||
// answers none for it and the seed has those steps `bypass` (*A module's registration*). The answer is the same for
|
||||
// every fingerprint, so nothing is kept for it.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildimage"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
)
|
||||
|
||||
// StepsAnswer is what `steps` answers.
|
||||
type StepsAnswer struct {
|
||||
Artifact string `json:"artifact"`
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
Steps []seat.DetectedStep `json:"steps"`
|
||||
}
|
||||
|
||||
// stepsTool is the seat's verb `steps`.
|
||||
func stepsTool() stdio.Tool {
|
||||
return stdio.Tool{Name: Seat + "." + seat.VerbSteps,
|
||||
Description: "Which pipeline steps an image artifact can run: none (an image has no lint, unit or integration test step).",
|
||||
Input: map[string]any{"type": "object", "required": []string{"module", "artifact"}, "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": "not read: an image's steps need no tree"},
|
||||
"again": map[string]any{"type": "boolean", "description": "not read: the answer is the same anew"},
|
||||
}},
|
||||
Run: steps}
|
||||
}
|
||||
|
||||
func steps(args map[string]any) (any, error) {
|
||||
str := func(k string) string { s, _ := args[k].(string); return s }
|
||||
if str("artifact") == "" || str("module") == "" {
|
||||
return nil, fmt.Errorf("steps: the module and the artifact are asked")
|
||||
}
|
||||
return StepsAnswer{Artifact: str("artifact"), Fingerprint: str("fingerprint"), Steps: []seat.DetectedStep{}}, nil
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
// Design 47 G §22, §328 (BLD8): the verb steps is served and answers none for an image.
|
||||
func TestTheStepsVerbAnswersNoStepForAnImage(t *testing.T) {
|
||||
served := false
|
||||
for _, tool := range (&Module{}).SeatTools(nil) {
|
||||
if tool.Name == Seat+".steps" {
|
||||
served = true
|
||||
got, err := tool.Run(map[string]any{"module": "m", "artifact": "m:server", "fingerprint": "fp"})
|
||||
a, ok := got.(StepsAnswer)
|
||||
if err != nil || !ok || a.Artifact != "m:server" || a.Steps == nil || len(a.Steps) != 0 {
|
||||
t.Errorf("steps = %+v (%v)", got, err)
|
||||
}
|
||||
if _, err := tool.Run(map[string]any{"module": "m"}); err == nil {
|
||||
t.Error("an ask with no artifact was answered")
|
||||
}
|
||||
}
|
||||
}
|
||||
if !served {
|
||||
t.Fatal("the module serves no verb steps")
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
{
|
||||
"name": "node-build-image",
|
||||
"scope": "node",
|
||||
"serves": ["current", "drop", "pause", "resume"]
|
||||
"serves": ["current", "drop", "pause", "resume", "steps"]
|
||||
}
|
||||
],
|
||||
"config": {
|
||||
|
||||
@@ -15,6 +15,7 @@ var Kept = []announce.State{
|
||||
{Name: "keys", Readable: true},
|
||||
{Name: "steps", Readable: true},
|
||||
{Name: "pauses", Readable: true},
|
||||
{Name: "detected-steps", Readable: true},
|
||||
}
|
||||
|
||||
// keepStates announces Kept as this module's (stdio.Keeps): main calls it before it serves.
|
||||
|
||||
@@ -87,7 +87,7 @@ func (m *Module) Puller(steps []StepTool, source buildstep.AskSource) *buildstep
|
||||
|
||||
// SeatTools are the seat's verbs on this node (ADR 0525 §10).
|
||||
func (m *Module) SeatTools(p *buildstep.Puller) []stdio.Tool {
|
||||
return buildstep.SeatTools(Seat, "Node", p)
|
||||
return append(buildstep.SeatTools(Seat, "Node", p), m.stepsTool())
|
||||
}
|
||||
|
||||
// StepEndTool ends one step left running (issue 1890); the module's steps state is the one it reads and writes.
|
||||
|
||||
@@ -48,6 +48,8 @@ func main() {
|
||||
Emit: func(t string, b any) error { return stdio.Emit(t, b) },
|
||||
Steps: stdio.State(seat.StateSteps).Put,
|
||||
ReadSteps: readOf(stdio.State(seat.StateSteps)),
|
||||
Detected: stdio.State(seat.StateDetectedSteps).Put,
|
||||
ReadDetected: readOf(stdio.State(seat.StateDetectedSteps)).Get,
|
||||
Pauses: stdio.State(sdkstep.PausesState).Put,
|
||||
ReadPauses: readOf(stdio.State(sdkstep.PausesState)),
|
||||
ForgetPause: stdio.State(sdkstep.PausesState).Delete,
|
||||
|
||||
@@ -117,7 +117,7 @@ func TestManifestKeepsItsStatesClaimsItsBuildSeatAndDeclaresItsHealthTool(t *tes
|
||||
if want := []string{"keys", "pauses", "steps", "toolchains"}; !slices.Equal(states, want) {
|
||||
t.Errorf("state = %v, want %v", states, want)
|
||||
}
|
||||
if len(m.Claims) != 1 || m.Claims[0].Name != Seat || !slices.Equal(m.Claims[0].Serves, []string{"current", "drop", "pause", "resume"}) {
|
||||
if len(m.Claims) != 1 || m.Claims[0].Name != Seat || !slices.Equal(m.Claims[0].Serves, []string{"current", "drop", "pause", "resume", "steps"}) {
|
||||
t.Errorf("claims = %v, want the seat %s alone", m.Claims, Seat)
|
||||
}
|
||||
if !slices.Contains(m.Tools, HealthTool) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"net/http"
|
||||
@@ -47,6 +48,9 @@ type Module struct {
|
||||
// Held is the puller whose running asks a step-end tool will not end (issue 1890); nil in a test.
|
||||
Held *buildstep.Puller
|
||||
ReadSteps, ReadToolchainsState StateRead
|
||||
// Detected and ReadDetected are the Put and the read of the state detected-steps (design 47 G §326, §414).
|
||||
Detected buildstep.Put
|
||||
ReadDetected func(key string) (json.RawMessage, error)
|
||||
// Pauses is the Put of the pauses state, which keeps the node's pause key without an age; ReadPauses reads it back.
|
||||
Pauses buildstep.Put
|
||||
ReadPauses StateRead
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Design 47 G §22, §307, §328 (BLD7): steps answers the type-check as lint always, the packages with a test script as
|
||||
// unit, those with test:integration as integration, and leaves out a level with none.
|
||||
func TestTheStepsOfATypeScriptArtifactAreFoundByItsScripts(t *testing.T) {
|
||||
put := func(dir, name, body string) {
|
||||
if err := os.MkdirAll(filepath.Join(dir), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for name, c := range map[string]struct {
|
||||
pkg string
|
||||
want []DetectedStep
|
||||
}{
|
||||
"both levels": {`{"scripts":{"test":"node t.js","test:integration":"node i.js"}}`,
|
||||
[]DetectedStep{{Step: "lint"}, {Step: "unit", Packages: []string{"."}}, {Step: "integration", Packages: []string{"."}}}},
|
||||
"unit only": {`{"scripts":{"test":"node t.js"}}`, []DetectedStep{{Step: "lint"}, {Step: "unit", Packages: []string{"."}}}},
|
||||
"no script": {`{"scripts":{}}`, []DetectedStep{{Step: "lint"}}},
|
||||
} {
|
||||
dir := t.TempDir()
|
||||
put(dir, "package.json", c.pkg)
|
||||
got, err := detectSteps(dir)
|
||||
if err != nil || !reflect.DeepEqual(got, c.want) {
|
||||
t.Errorf("%s: steps = %+v (%v), want %+v", name, got, err, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The verb is served beside the seat's others and refuses an ask that names no artifact.
|
||||
func TestTheStepsVerbIsServedAndRefusesAnAskWithoutAnArtifact(t *testing.T) {
|
||||
m, _, _ := newMachine(t)
|
||||
served := false
|
||||
for _, tool := range m.SeatTools(m.Puller(nil, nil)) {
|
||||
served = served || tool.Name == Seat+".steps"
|
||||
}
|
||||
if !served {
|
||||
t.Fatal("the module serves no verb steps")
|
||||
}
|
||||
if _, err := m.steps(context.Background(), map[string]any{"module": "m", "fetch": map[string]any{"repository": "novox/m", "commit": "0123456789abcdef0123456789abcdef01234567"}}); err == nil {
|
||||
t.Error("an ask with no artifact was answered")
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
{
|
||||
"name": "node-build-node",
|
||||
"scope": "node",
|
||||
"serves": ["current", "drop", "pause", "resume"]
|
||||
"serves": ["current", "drop", "pause", "resume", "steps"]
|
||||
}
|
||||
],
|
||||
"config": {
|
||||
@@ -65,6 +65,7 @@
|
||||
}
|
||||
],
|
||||
"requires": [
|
||||
{"name": "detected-steps", "storage": "key-value", "template": "state", "options": {"history": 1}},
|
||||
"forge-account",
|
||||
"npm-package-registry",
|
||||
"node-toolchain",
|
||||
|
||||
Reference in New Issue
Block a user