diff --git a/go/buildstep/capacity.go b/go/buildstep/capacity.go deleted file mode 100644 index 098f657..0000000 --- a/go/buildstep/capacity.go +++ /dev/null @@ -1,7 +0,0 @@ -package buildstep - -// CapacitySetting is the build modules' setting of the node's build capacity (novox/hq ADR 0525 §5, ADR 0567 §12): how -// many asks the node takes at once, an integer config value set on each node's layer of mesh-config. No repository names -// a node: each build module declares it with the default 0, which pulls nothing, and the controller (or mesh-config) -// lays the node's value into the module's settings file. -const CapacitySetting = "capacity" diff --git a/go/buildstep/doc.go b/go/buildstep/doc.go deleted file mode 100644 index 93c1feb..0000000 --- a/go/buildstep/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package buildstep is the contract of a build step every delivery part reads (novox/hq ADR 0593 §1): a step's id, the -// fetch request, a step a previous process left running and its end, a failure's words, a module tree's shape, the -// capacity setting and the launchers' names. The runner the build modules share is the Go build module's own package -// (mesh-catalog modules/mesh-build-go/buildstep), which imports this one. -package buildstep diff --git a/go/buildstep/failure.go b/go/buildstep/failure.go deleted file mode 100644 index bdea558..0000000 --- a/go/buildstep/failure.go +++ /dev/null @@ -1,141 +0,0 @@ -package buildstep - -import ( - "fmt" - "strings" - - "git.novox.be/novox/mesh-sdk/go/sdkversion" -) - -// Failure is one test that failed, or a package or file that failed with no test to name (a build that failed, a test -// program that panicked, a test file that did not load). The JSON names are the ones mesh-build-go and the checker have -// always used; File is new and an older reader ignores it. -type Failure struct { - // Package is the Go package's import path, or the TypeScript package's directory in the workspace. - Package string `json:"package"` - // Test names the failing test, or the tests that were running when the program stopped; "" when none can be named. - Test string `json:"test,omitempty"` - // File is where the failing test is (a Go test's file and line is in its Cause; node names the test's file:line:col). - File string `json:"file,omitempty"` - // Cause is the one line that says why: the assertion's or the error's first line, a "panic:" line, a build's first - // error, or words saying how the program stopped. **Never empty, and never a passing line** (PassingLine). - Cause string `json:"cause,omitempty"` - // Output is the failing test's own output, its last lines up to OutputBytes, Cause first: an older checker reads - // only its first line. - Output string `json:"output,omitempty"` -} - -// OutputBytes is the most a failure keeps of its output, and LineBytes the most of one line: four kilobytes holds a long -// assertion message and stays far under a bus message's or a verdict's bound. -const ( - OutputBytes = 4096 - LineBytes = 1024 -) - -// By names a build module and the SDK it was built with, for a result's "by": the checker names it when a failure hides -// its cause ("the build module hid the failure ()"). -func By(module string) string { - return module + " with mesh-sdk " + sdkversion.Version() -} - -// PassingLine says a line is a test runner's word for a test or a package that passed: node's '✔', TAP's "ok N" (at any -// indent), go test's "ok \t", "PASS" and "--- PASS". A line of a test's own that starts "ok " is none of these. Such a line is never a failure's cause, nor part of a failure's output. -func PassingLine(l string) bool { - if strings.HasPrefix(l, "ok \t") { // go test's package line, at column 0 - return true - } - t := strings.TrimSpace(l) - switch { - case t == "PASS", strings.HasPrefix(t, "✔"), strings.HasPrefix(t, "--- PASS"): - return true - } - return tapOk(t) -} - -// Hidden says how a failure hides why it failed, or "" when it does not: it has no cause, or its cause is a passing line. -// The checker says it loudly (issue 1901): a build module that sends such a failure does not keep the contract. -func Hidden(f Failure) string { - c := strings.TrimSpace(f.Cause) - switch { - case c == "": - return "it sent no cause" - case PassingLine(c): - return fmt.Sprintf("its cause is a passing line (%q)", CutLine(c)) - } - return "" -} - -// Clip joins the last lines of a failure's output that fit OutputBytes, each cut to LineBytes, and says how many earlier -// lines it left out. **Its first line is the cause**, put ahead when the kept lines do not start with it, or, with no -// cause and lines left out, the first non-empty line, then the marker: an older checker reads only the first line. -// Passing lines are dropped. -func Clip(lines []string, cause string) string { - own := make([]string, 0, len(lines)) - for _, l := range lines { - if !PassingLine(l) { - own = append(own, l) - } - } - lines = own - size, from := 0, len(lines) - for from > 0 { - n := len(CutLine(lines[from-1])) + 1 - if size+n > OutputBytes && from < len(lines) { - break - } - size += n - from-- - } - kept := make([]string, 0, len(lines)-from+2) - head := strings.TrimSpace(cause) - for i := 0; head == "" && i < from; i++ { - head = strings.TrimSpace(lines[i]) - } - if head != "" && (from > 0 || len(lines) == 0 || strings.TrimSpace(lines[from]) != head) { - kept = append(kept, CutLine(head)) - } - if from > 0 { - kept = append(kept, fmt.Sprintf("... (%d earlier lines)", from)) - } - for _, l := range lines[from:] { - kept = append(kept, CutLine(l)) - } - return strings.Join(kept, "\n") -} - -// CutLine cuts a line to LineBytes, on a character boundary. -func CutLine(l string) string { - if len(l) <= LineBytes { - return l - } - return strings.ToValidUTF8(l[:LineBytes], "") + "..." -} - -// ConsoleFailure is the failure of a command that is no test runner (go vet, gofmt, tsc, a build script): its cause is the -// first line that names an error (tsc's "error TS", go vet's "file.go:12:3: ..."), else the first line that is not a -// heading ("# ", npm's "> script"), and its output the command's lines without passing ones. -func ConsoleFailure(pkg, test, out string) Failure { - var lines []string - for _, l := range strings.Split(strings.ReplaceAll(stripANSI(out), "\r\n", "\n"), "\n") { - if t := strings.TrimSpace(l); t != "" && !PassingLine(t) { - lines = append(lines, strings.TrimRight(l, " \t")) - } - } - cause := "" - for _, l := range lines { - if t := strings.TrimSpace(l); strings.Contains(t, "error") || strings.Contains(t, "Error") || goPosition.MatchString(t) { - cause = t - break - } - } - for _, l := range lines { - if t := strings.TrimSpace(l); cause == "" && !strings.HasPrefix(t, "# ") && !strings.HasPrefix(t, "> ") && !strings.HasPrefix(t, "npm ") { - cause = t - } - } - if cause == "" { - cause = test + " failed and wrote nothing that says why" - } - cause = CutLine(cause) - return Failure{Package: pkg, Test: test, Cause: cause, Output: Clip(lines, cause)} -} diff --git a/go/buildstep/helpers.go b/go/buildstep/helpers.go deleted file mode 100644 index d9f0b98..0000000 --- a/go/buildstep/helpers.go +++ /dev/null @@ -1,19 +0,0 @@ -package buildstep - -import ( - "regexp" - "strings" -) - -// goPosition matches a line that starts with a Go file's position, as go vet and the compiler write an error. -var goPosition = regexp.MustCompile(`^\S+\.go:\d+(:\d+)?: `) - -var ansiEscape = regexp.MustCompile("\x1b\\[[0-9;]*m") - -func stripANSI(s string) string { return ansiEscape.ReplaceAllString(s, "") } - -// tapOk says a line is a passing test's "ok N - name", indented or not. -func tapOk(l string) bool { - rest, ok := strings.CutPrefix(strings.TrimSpace(l), "ok ") - return ok && rest != "" && rest[0] >= '0' && rest[0] <= '9' -} diff --git a/go/buildstep/launchers.go b/go/buildstep/launchers.go deleted file mode 100644 index 975f560..0000000 --- a/go/buildstep/launchers.go +++ /dev/null @@ -1,14 +0,0 @@ -package buildstep - -import "git.novox.be/novox/mesh-sdk/go/catalogue" - -// LauncherSuffix is what a TypeScript entrypoint's launcher is called beside it: index.js is started as -// index.serve.mjs (novox/hq ADR 0193), an ES module by its own extension whatever the bundle's package.json says. -const LauncherSuffix = catalogue.LauncherSuffix - -// Launchers are the launchers a bundle's build writes, by entrypoint, relative to the bundle's root -// (catalogue.Launchers): the one rule mesh-build-node, build-agent, the controller and a node taking in a build share -// (R4e, issue 1126), so the name a build writes is the name a reader expects. -func Launchers(language string, entrypoints []string) map[string]string { - return catalogue.Launchers(language, entrypoints) -} diff --git a/go/buildstep/launchers_test.go b/go/buildstep/launchers_test.go deleted file mode 100644 index ddea9a0..0000000 --- a/go/buildstep/launchers_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package buildstep - -import ( - "reflect" - "testing" -) - -// A TypeScript bundle's entrypoint x.js is started as x.serve.mjs beside it (novox/hq ADR 0193), the rule mesh-build-node -// and build-agent both apply; an entrypoint that is not .js gets none, and another language gets none. -func TestATypeScriptEntrypointIsStartedByTheLauncherBesideIt(t *testing.T) { - got := Launchers("typescript", []string{"index.js", "tools/x.js", "data.json"}) - want := map[string]string{"index.js": "index.serve.mjs", "tools/x.js": "tools/x.serve.mjs"} - if !reflect.DeepEqual(got, want) { - t.Fatalf("Launchers(typescript) = %v, want %v", got, want) - } - if got := Launchers("go", []string{"index.js"}); len(got) != 0 { - t.Fatalf("a Go bundle got launchers: %v", got) - } - if got := Launchers("typescript", nil); len(got) != 0 { - t.Fatalf("a bundle with no entrypoint got launchers: %v", got) - } -} diff --git a/go/buildstep/request.go b/go/buildstep/request.go deleted file mode 100644 index 345ef00..0000000 --- a/go/buildstep/request.go +++ /dev/null @@ -1,163 +0,0 @@ -package buildstep - -import ( - "fmt" - "maps" - "path" - "regexp" - "strings" - - "git.novox.be/novox/mesh-sdk/go/delivery" -) - -var ( - requestCommitOK = regexp.MustCompile(`^[0-9a-f]{40}$`) - requestRepositoryOK = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*$`) -) - -// FetchRequest is what a build module's fetch tool is asked: the repository and the full commit, the pull request's -// ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, and, for a build of a -// module whose every artifact names its paths, that module's directory and its named paths (novox/hq ADR 0499 clause 4). -// -// **The paths come from the builder, not from a manifest** (ADR 0495 clause 1; the lead's decision L8 of R4e): the -// builder fills Dir and Paths from the indexer's Module value (Path, and KeptInputs2.Paths' keys), and the build module -// gives the build and test steps a workspace holding those paths, the manifest and its lock files. A request without -// them builds from the module's directory, as before. -type FetchRequest struct { - Repository string `json:"repository"` - Commit string `json:"commit"` - Ref string `json:"ref,omitempty"` - Base string `json:"base,omitempty"` - Dir string `json:"dir,omitempty"` - Paths []string `json:"paths,omitempty"` - // Packages are the version of each mesh package the build or the check builds against (delivery.StepAsk.Packages, - // novox/hq ADR 0573 §2, §4): the build module writes each into the workspace's go.mod and go.sum, whatever version - // the repository names, checksum-verified, and names them in its log. - Packages delivery.PackageVersions `json:"packages,omitempty"` -} - -// FetchRequestProperties are the JSON schema properties of FetchRequest's dir and paths, for a fetch tool's Input. -func FetchRequestProperties() map[string]any { - return map[string]any{ - "dir": map[string]any{"type": "string", "description": "the module's directory in the repository (\".\" for its root), with paths"}, - "paths": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, - "description": "the paths the module names, relative to dir, from the indexer's kept inputs: the build sees only these, the manifest and its lock files"}, - "packages": map[string]any{"type": "object", "additionalProperties": map[string]any{"type": "string"}, - "description": "optional: the version (a full commit) of each mesh package to build against, by its module path, whatever version go.mod names (novox/hq ADR 0573 §2); the builder and the checker fill it"}, - } -} - -// Args is r as a tool call's arguments. -func (r FetchRequest) Args() map[string]any { - args := map[string]any{"repository": r.Repository, "commit": r.Commit} - for k, v := range map[string]string{"ref": r.Ref, "base": r.Base, "dir": r.Dir} { - if v != "" { - args[k] = v - } - } - if len(r.Paths) > 0 { - args["paths"] = append([]string(nil), r.Paths...) - } - if len(r.Packages) > 0 { - args["packages"] = map[string]any{} - for p, v := range r.Packages { - args["packages"].(map[string]any)[p] = v - } - } - return args -} - -// ParseFetchRequest reads a fetch tool's arguments; arguments it does not know (a build module's own) are left to the -// caller. It refuses a repository that is not /, a commit or a base that is not 40 hex digits, paths with no -// module directory or a directory with no path, a directory leaving the repository and a path leaving the directory, -// naming each. -func ParseFetchRequest(args map[string]any) (FetchRequest, error) { - var r FetchRequest - r.Repository, _ = args["repository"].(string) - r.Commit, _ = args["commit"].(string) - r.Ref, _ = args["ref"].(string) - r.Base, _ = args["base"].(string) - r.Dir, _ = args["dir"].(string) - switch v := args["paths"].(type) { - case nil: - case []string: - r.Paths = append([]string(nil), v...) - case []any: - for _, p := range v { - s, ok := p.(string) - if !ok { - return FetchRequest{}, fmt.Errorf("paths is a list of strings") - } - r.Paths = append(r.Paths, s) - } - default: - return FetchRequest{}, fmt.Errorf("paths is a list of strings") - } - packages, err := PackagesArg(args["packages"]) - if err != nil { - return FetchRequest{}, err - } - r.Packages = packages - return r, r.Check() -} - -// PackagesArg reads a tool call's packages argument, an object of strings (as JSON gives it) or the map an ask gives; -// nil for none. -func PackagesArg(v any) (delivery.PackageVersions, error) { - out := delivery.PackageVersions{} - switch v := v.(type) { - case nil: - return nil, nil - case delivery.PackageVersions: - maps.Copy(out, v) - case map[string]string: - maps.Copy(out, v) - case map[string]any: - for p, version := range v { - s, ok := version.(string) - if !ok { - return nil, fmt.Errorf("packages is an object of versions (strings), by package") - } - out[p] = s - } - default: - return nil, fmt.Errorf("packages is an object of versions (strings), by package") - } - if len(out) == 0 { - return nil, nil - } - return out, out.Check() -} - -// Check is what ParseFetchRequest refuses. -func (r FetchRequest) Check() error { - if !requestRepositoryOK.MatchString(r.Repository) || strings.Contains(r.Repository, "..") { - return fmt.Errorf("%q is not a repository, /", r.Repository) - } - if !requestCommitOK.MatchString(r.Commit) { - return fmt.Errorf("%q is not a full commit (40 hex digits)", r.Commit) - } - if r.Base != "" && !requestCommitOK.MatchString(r.Base) { - return fmt.Errorf("%q is not a trunk commit (40 hex digits)", r.Base) - } - switch { - case len(r.Paths) > 0 && r.Dir == "": - return fmt.Errorf("the request names paths and names no module directory (dir) they are relative to") - case len(r.Paths) == 0 && r.Dir != "": - return fmt.Errorf("the request names a module directory and no path") - } - if r.Dir != "" && (outside(r.Dir) || strings.HasPrefix(r.Dir, "/")) { - return fmt.Errorf("the module directory %q leaves the repository", r.Dir) - } - for _, p := range r.Paths { - if p == "" || strings.HasPrefix(p, "/") || outside(p) || path.Clean(p) == "." { - return fmt.Errorf("the path %q leaves the module's directory or is not a path in it", p) - } - } - return nil -} - -func outside(p string) bool { - c := path.Clean(p) - return c == ".." || strings.HasPrefix(c, "../") -} diff --git a/go/buildstep/request_test.go b/go/buildstep/request_test.go deleted file mode 100644 index 7732329..0000000 --- a/go/buildstep/request_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package buildstep - -import ( - "reflect" - "strings" - "testing" -) - -// A build's fetch is handed the paths of a module that names them (novox/hq ADR 0499 clause 4): the builder fills them -// from the indexer's kept inputs and the build module reads them from the call, never from the manifest (ADR 0495 -// clause 1). The request survives the trip through a tool call's arguments. -func TestAFetchRequestCarriesTheNamedPathsThroughTheToolCall(t *testing.T) { - // And the versions of the mesh packages to build against (novox/hq ADR 0573 §2; issue 1585 S4). - req := FetchRequest{Repository: "novox/mesh-sdk", Commit: strings.Repeat("a", 40), Ref: "refs/pull/7/head", - Dir: ".", Paths: []string{"package.json", "src"}, Packages: map[string]string{"git.novox.be/novox/mesh-sdk/go": strings.Repeat("d", 40)}} - args := req.Args() - // A tool call's arguments arrive as JSON: lists of any, objects of any. - args["paths"] = []any{"package.json", "src"} - if p, ok := args["packages"].(map[string]any); !ok || p["git.novox.be/novox/mesh-sdk/go"] != strings.Repeat("d", 40) { - t.Fatalf("the packages are not in the call's arguments: %v", args["packages"]) - } - got, err := ParseFetchRequest(args) - if err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(got, req) { - t.Fatalf("got %+v, want %+v", got, req) - } - plain, err := ParseFetchRequest(map[string]any{"repository": "novox/x", "commit": strings.Repeat("b", 40), - "base": strings.Repeat("c", 40), "beside": []any{}}) - if err != nil || plain.Paths != nil || plain.Base != strings.Repeat("c", 40) { - t.Fatalf("a request without paths: %+v, %v", plain, err) - } - - ok := map[string]any{"repository": "novox/x", "commit": strings.Repeat("b", 40)} - for _, c := range []struct { - set map[string]any - says string - }{ - {map[string]any{"repository": "novox"}, "is not a repository"}, - {map[string]any{"repository": "novox/../x"}, "is not a repository"}, - {map[string]any{"commit": "abc"}, "is not a full commit"}, - {map[string]any{"base": "abc"}, "is not a trunk commit"}, - {map[string]any{"paths": []any{"src"}}, "names no module directory"}, - {map[string]any{"dir": "modules/a", "paths": []any{"../b"}}, "leaves the module's directory"}, - {map[string]any{"dir": "../a", "paths": []any{"src"}}, "leaves the repository"}, - {map[string]any{"dir": "modules/a", "paths": []any{3}}, "paths is a list of strings"}, - {map[string]any{"dir": "modules/a"}, "names a module directory and no path"}, - {map[string]any{"packages": map[string]any{"git.novox.be/novox/mesh-sdk/go": "main"}}, "neither a full commit nor a Go semantic version"}, - {map[string]any{"packages": map[string]any{"a b": strings.Repeat("d", 40)}}, "no module path"}, - {map[string]any{"packages": []any{"x"}}, "packages is an object"}, - } { - args := map[string]any{} - for k, v := range ok { - args[k] = v - } - for k, v := range c.set { - args[k] = v - } - if _, err := ParseFetchRequest(args); err == nil || !strings.Contains(err.Error(), c.says) { - t.Errorf("%v: %v, wanted a refusal saying %q", c.set, err, c.says) - } - } -} diff --git a/go/buildstep/stale.go b/go/buildstep/stale.go deleted file mode 100644 index bd7deb7..0000000 --- a/go/buildstep/stale.go +++ /dev/null @@ -1,330 +0,0 @@ -package buildstep - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "os" - "strconv" - "strings" - "time" - - stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/delivery" -) - -// RestartReason is why a step a previous process left running is ended. -const RestartReason = "the build module restarted while it ran" - -// Boot is when this process began. A step with no owner (written by an older build) started at or after it is taken -// for this process's, which may still run it, so EndOne refuses it. -var Boot = time.Now() - -// Owner is this process as a step's owner (delivery.Step.Owner): `..`, the node's boot id -// (/proc/sys/kernel/random/boot_id), the pid and Boot in milliseconds, base 36. The start keeps two processes apart -// that a reboot or a pid namespace gave the same pid. A running step whose owner is another is a previous process's. -var Owner = ownerOf(bootID, os.Getpid(), Boot) - -// bootID is the node's boot id as Owner holds it; noBootID when it could not be read, which EndStaleAtStart says once. -var bootID = readBootID() - -const noBootID = "no-boot-id" - -func ownerOf(bootID string, pid int, start time.Time) string { - return bootID + "." + strconv.Itoa(pid) + "." + strconv.FormatInt(start.UnixMilli(), 36) -} - -// readBootID is the node's boot id, or noBootID where it cannot be read (the pid and the start then tell the processes -// apart). -func readBootID() string { - b, err := os.ReadFile("/proc/sys/kernel/random/boot_id") - if id := strings.TrimSpace(string(b)); err == nil && id != "" { - return id - } - return noBootID -} - -// Scan reads and writes a steps state with its revisions: its keys, a key's stored value and revision (nil and 0 for -// none), a write that lands only while the key is still at the revision read (KeptState.Update), and a delete of a key -// still at the revision read; both fail with stdio.ErrChanged when another writer was first. -type Scan struct { - Keys func() ([]string, error) - Get func(key string) (json.RawMessage, uint64, error) - Update func(key string, value any, revision uint64) (uint64, error) - Delete func(key string, revision uint64) error -} - -// ScanOf is the Scan of a state the process keeps. -// -// Its Delete reads the key again and deletes it only while it is still at the revision read: the runtime's -// mesh/state.delete takes no revision yet, so a write landing between that read and the delete (a few milliseconds) is -// deleted with it. For a step key that is a redelivered ask begun on this node in that moment: its running key leaves -// the steps state until its run writes its end, which its asker still reads (a watch takes puts only). -func ScanOf(s stdio.KeptState) Scan { - get := func(key string) (json.RawMessage, uint64, error) { - e, err := s.Get(key) - if err != nil || e == nil { - return nil, 0, err - } - return e.Value, e.Revision, nil - } - return Scan{Keys: s.Keys, Update: s.Update, Get: get, Delete: func(key string, revision uint64) error { - _, rev, err := get(key) - if err != nil { - return err - } - if rev != revision { - return stdio.ErrChanged - } - return s.Delete(key) - }} -} - -// isAsk says a step is a pulled ask's: its key is . and its tool is " ask" (a hand-started step -// has the tool's name). An ask's key is the stream's to settle, never a restart's: the puller leaves an ask it was -// running unacknowledged so the stream redelivers it, and the asker's wait takes the first end it reads on the key. -// A key a gone process owns is deleted, never ended (EndStale); one with no owner is ended only once the asker's bound -// has passed (delivery.StepBound), when it is nobody's. -func isAsk(key string, s delivery.Step) bool { - _, _, err := delivery.ParseAskStepKey(key) - return err == nil && strings.HasSuffix(s.Tool, " ask") -} - -// AskWhat says what an ask is, in one line: its kind, and its repository, head and levels, or its module and -// fingerprint. -func AskWhat(a delivery.StepAsk) string { - short := func(s string) string { - if len(s) > 12 { - return s[:12] - } - return s - } - str := func(m map[string]any, k string) string { v, _ := m[k].(string); return v } - parts := []string{a.Kind + " ask"} - switch { - case a.Check != nil: - parts = append(parts, str(a.Check.Fetch, "repository")+"@"+short(str(a.Check.Fetch, "commit")), "levels "+strings.Join(a.Check.Levels, ",")) - if a.Check.Run != "" { - parts = append(parts, "run "+a.Check.Run) - } - case a.Build != nil: - parts = append(parts, "module "+a.Build.Module) - if r := str(a.Build.Fetch, "repository"); r != "" { - parts = append(parts, r+"@"+short(str(a.Build.Fetch, "commit"))) - } - if a.Build.FP != "" { - parts = append(parts, "fp "+short(a.Build.FP)) - } - case a.Compose != nil: - parts = append(parts, "compose", str(a.Compose.Fetch, "repository")+"@"+short(str(a.Compose.Fetch, "commit"))) - } - return strings.Join(parts, ", ") -} - -// endRunning writes step ended failed with why, only while its key is still at revision. -func endRunning(scan Scan, key string, step delivery.Step, revision uint64, why string) error { - step.State, step.Reason, step.Ended, step.Result = delivery.StepFailed, why, now().UTC(), nil - raw, err := delivery.EncodeStep(step) - if err != nil { - return err - } - _, err = scan.Update(key, json.RawMessage(raw), revision) - return err -} - -// EndStale ends this node's running steps that no process of this module runs any more, and answers their ids. Only keys -// of node are read; a value that is no step is left alone. -// - A hand-started step with an owner (delivery.Step.Owner) that is not this process (Owner) is ended failed with -// RestartReason at once, whatever its age: its process is gone and nothing redelivers it, so nothing else would -// write its end (issue 1890, note 21). -// - A pulled ask's step with an owner that is not this process is deleted, not ended (novox/hq ADR 0525 §6): the -// stream redelivers an ask whose node stopped (the puller neither ends nor acknowledges one at its stop), and its -// asker takes the first end it reads, so an end written here would fail a check the redelivery would still run. Its -// asker's watch takes puts only and keeps waiting for the redelivery's key, its bound the backstop; the stale running -// key leaves the steps state, and with it the delivery view's and the checker's count of what runs (note 21: it -// held one of a build node's two slots). -// - A step owned by this process is left: it may run now. A redelivered ask begun on this node is owned by it. -// - A step with no owner (written by an older build) keeps the earlier rule: a hand-started step started before -// `before` (the start of this process) is ended; a pulled ask is left to the stream's redelivery (isAsk) until its -// start is older than delivery.StepBound, and then ended "past the step bound: its asker stopped waiting at