From 03b7b6ed24210c1a52d81ac45bd9331b30b89fb5 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 18:50:29 +0100 Subject: [PATCH] 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 --- modules/mesh-delivery-pipeline/go.mod | 2 +- .../internal/pipeline/place.go | 24 ++++++++++++++----- .../internal/pipeline/place_test.go | 13 ++++++++-- .../internal/pipeline/verbs.go | 4 ++-- 4 files changed, 32 insertions(+), 11 deletions(-) diff --git a/modules/mesh-delivery-pipeline/go.mod b/modules/mesh-delivery-pipeline/go.mod index c6af02f8..a55bcf38 100644 --- a/modules/mesh-delivery-pipeline/go.mod +++ b/modules/mesh-delivery-pipeline/go.mod @@ -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 diff --git a/modules/mesh-delivery-pipeline/internal/pipeline/place.go b/modules/mesh-delivery-pipeline/internal/pipeline/place.go index 68f5864d..2037f55a 100644 --- a/modules/mesh-delivery-pipeline/internal/pipeline/place.go +++ b/modules/mesh-delivery-pipeline/internal/pipeline/place.go @@ -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}}) } diff --git a/modules/mesh-delivery-pipeline/internal/pipeline/place_test.go b/modules/mesh-delivery-pipeline/internal/pipeline/place_test.go index 1c26872d..e6c234dc 100644 --- a/modules/mesh-delivery-pipeline/internal/pipeline/place_test.go +++ b/modules/mesh-delivery-pipeline/internal/pipeline/place_test.go @@ -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) diff --git a/modules/mesh-delivery-pipeline/internal/pipeline/verbs.go b/modules/mesh-delivery-pipeline/internal/pipeline/verbs.go index 6ff1d86c..a0118b68 100644 --- a/modules/mesh-delivery-pipeline/internal/pipeline/verbs.go +++ b/modules/mesh-delivery-pipeline/internal/pipeline/verbs.go @@ -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