A-297: the runner's step ask id takes the usual ask id form, so the holders accept it

The runner's step ask id was `step-<24 hex>`; every build seat holder's CheckAskID refuses it (base 36
milliseconds and six hex digits), so no placed step could be accepted. AskID now opens with base 36 of
the pipeline's start time in milliseconds and ends with six hex digits of the sha256 of the pipeline,
the artifact and the step: the same placement makes the same id at every start. A rerun uses its own
time.

Builds: design 47 G A-297, A-232, A-111

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 18:50:29 +01:00
parent c529ca259f
commit 03b7b6ed24
4 changed files with 32 additions and 11 deletions
+1 -1
View File
@@ -42,7 +42,7 @@ require (
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/containerimageregistryclient v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/containerimageregistry v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/ociartifactregistry v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk v0.0.0-00010101000000-000000000000
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op // indirect
github.com/minio/highwayhash v1.0.4 // indirect
github.com/nats-io/jwt/v2 v2.8.2 // indirect
@@ -5,6 +5,7 @@ import (
"encoding/hex"
"fmt"
"sort"
"strconv"
"strings"
"time"
@@ -53,10 +54,21 @@ var moduleSteps = []string{seat.StepResultStepComposition, seat.StepResultStepRe
// kindImage is the form of an acting rule's `on` naming an artifact kind (§324).
const kindImage = "kind: image"
// AskID is a step's ask id, the same for the same pipeline, artifact and step, so a start asked again asks nothing new.
func AskID(pipeline, artifact, step string) string {
// AskID is a step's ask id in the usual ask id form the holders accept (buildstep.CheckAskID): base 36 of started in
// milliseconds, then six hex digits of the sha256 of the pipeline, the artifact and the step, so the same placement makes
// the same id at every start and the bus dedupes on it; started is the pipeline's start time, or a rerun's own (design 47
// G A-297).
func AskID(started time.Time, pipeline, artifact, step string) string {
sum := sha256.Sum256([]byte(pipeline + "\n" + artifact + "\n" + step))
return "step-" + hex.EncodeToString(sum[:12])
return strconv.FormatInt(started.UnixMilli(), 36) + hex.EncodeToString(sum[:3])
}
// startOf is a pipeline's start time, the time its step ask ids open with (A-297); now for a pipeline that records none.
func startOf(p seat.Pipeline, now time.Time) time.Time {
if p.Started != nil {
return *p.Started
}
return now
}
// Place places a pipeline's steps (design 47 G §21, §23, §323; A-91): only the steps the module's registration policy
@@ -74,7 +86,7 @@ func Place(p seat.Pipeline, reg catalog.Registration, entries map[string]catalog
// The build step is every pipeline's first, required whatever the policy names, asked of the seat mesh-builds for the
// module's own entry only (A-99, A-97): a group's other members are built by their own pipelines.
build := seat.StepResult{Step: seat.StepResultStepBuild, Status: seat.StepResultStatusRequired, Fingerprint: p.Fingerprint,
Seat: builds.Seat, Outcome: seat.StepResultOutcomeRunning, Started: &now, Ask: AskID(p.Pipeline, "", seat.StepResultStepBuild)}
Seat: builds.Seat, Outcome: seat.StepResultOutcomeRunning, Started: &now, Ask: AskID(startOf(p, now), p.Pipeline, "", seat.StepResultStepBuild)}
steps = append(steps, build)
asks = append(asks, Asked{Seat: builds.Seat, ID: build.Ask, Ask: builds.StepRun{Pipeline: p.Pipeline, Step: builds.StepRunStepBuild, Branch: p.Branch,
Revision: p.Revision, Source: p.Source, Entries: []builds.BuildEntry{{Module: p.Module, Commit: p.Commit, Fingerprint: p.Fingerprint}}}})
@@ -102,7 +114,7 @@ func Place(p seat.Pipeline, reg catalog.Registration, entries map[string]catalog
continue
}
s.Seat, s.Outcome, s.Started = bs, seat.StepResultOutcomeRunning, &now
s.Ask = AskID(p.Pipeline, a.Ref, step)
s.Ask = AskID(startOf(p, now), p.Pipeline, a.Ref, step)
steps = append(steps, s)
ask := Asked{Seat: bs, ID: s.Ask, Ask: stepAsk(p, entry, a, step, s.Ask, asker, now)}
if a.Package != nil {
@@ -124,7 +136,7 @@ func Place(p seat.Pipeline, reg catalog.Registration, entries map[string]catalog
// running at once, and its end is heard from that seat as the build seats' are. Review and approval wait.
if step == seat.StepResultStepComposition && status != seat.StepResultStatusBypass {
s.Seat, s.Outcome, s.Started, s.Fingerprint = wants.Seat, seat.StepResultOutcomeRunning, &now, p.Fingerprint
s.Ask = AskID(p.Pipeline, "", step)
s.Ask = AskID(startOf(p, now), p.Pipeline, "", step)
asks = append(asks, Asked{Seat: wants.Seat, ID: s.Ask, Ask: wants.StepRun{Branch: p.Branch, Commit: p.Commit, Fingerprint: p.Fingerprint,
Module: p.Module, Pipeline: p.Pipeline, Revision: p.Revision, Source: p.Source, Step: wants.StepRunStepComposition}})
}
@@ -2,6 +2,7 @@ package pipeline
import (
"encoding/json"
"strconv"
"strings"
"testing"
"time"
@@ -11,6 +12,7 @@ import (
seat "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshpipelinerunner"
wants "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants"
nodebuildgo "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/nodebuildgo"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep"
)
func stepSeatOf(t *testing.T, name string) StepSeat {
@@ -81,6 +83,12 @@ func TestStartPlacesTheStepsThePolicyNamesAndAsksEachBuildSeat(t *testing.T) {
if len(asks.asks) != 7 {
t.Fatalf("asked %d steps, want the build, the five running build steps and the composition", len(asks.asks))
}
// A-297: every step's ask id is the usual ask id form the holders accept, opened by the pipeline's start time.
for i, id := range asks.ids {
if err := buildstep.CheckAskID(id); err != nil || !strings.HasPrefix(id, strconv.FormatInt(p.Started.UnixMilli(), 36)) {
t.Errorf("ask %d's id %q is not the usual form opened by the pipeline's start: %v", i, id, err)
}
}
// A-99: the build is every pipeline's first step, required whatever the policy names, asked of mesh-builds for the
// module's own entry only.
b, ok := asks.asks[0].(builds.StepRun)
@@ -181,10 +189,11 @@ func TestCompositionIsAskedOfTheAssignerAndItsEndIsTaken(t *testing.T) {
// placed under; an ended step, an ended pipeline and another branch's pipeline are asked of no one (design 47 G §88).
func TestResumeAsksTheStepsPlacedAndNotEnded(t *testing.T) {
reg, entries := placeFixture()
started := time.Unix(1, 0) // A-297: a step's ask id opens with its pipeline's start, so resuming later places it under the same id
placed := func(branch string, source int64) seat.Pipeline {
p := seat.Pipeline{Pipeline: Key(branch, source, "app"), Branch: branch, Source: source, Module: "app", Commit: "c1", Fingerprint: "src2:aa",
Repository: "o/app", Outcome: seat.PipelineOutcomeRunning}
p.Steps, _ = Place(p, reg, entries, time.Unix(1, 0), "the-runner")
Repository: "o/app", Outcome: seat.PipelineOutcomeRunning, Started: &started}
p.Steps, _ = Place(p, reg, entries, started, "the-runner")
return p
}
main4, main5, beta6 := placed("main", 4), placed("main", 5), placed("beta", 6)
@@ -325,12 +325,12 @@ func (v Verbs) askOf(p seat.Pipeline, s seat.StepResult, caller string, now time
return nil, fmt.Errorf("the catalog half of %s is not held yet: the step is asked when it is", p.Branch)
}
_, placed := Place(p, reg, half.Entries, now, v.Starter.Name)
base := AskID(p.Pipeline, s.Artifact, s.Step)
base := AskID(startOf(p, now), p.Pipeline, s.Artifact, s.Step)
for _, a := range placed {
if a.ID != base {
continue
}
a.ID = AskID(p.Pipeline, s.Artifact, s.Step+"\nrerun\n"+now.Format(time.RFC3339Nano))
a.ID = AskID(now, p.Pipeline, s.Artifact, s.Step)
switch b := a.Ask.(type) {
case nodebuildgo.StepAsk:
b.ID = a.ID