diff --git a/go/buildstep/askrun.go b/go/buildstep/askrun.go index 1415331..06156e1 100644 --- a/go/buildstep/askrun.go +++ b/go/buildstep/askrun.go @@ -107,21 +107,32 @@ func (s AskSteps) fetch(ctx context.Context, args map[string]any) (json.RawMessa return r, f, nil } +// onToolchain is the toolchain this node builds with when the ask names one and this module reports its own, nil when +// either is absent. A node on another toolchain than the ask names answers a Declining: it is not on the agreed one and +// sits out, for a node that is to take the ask (novox/hq ADR 0584 clause 2). +func (s AskSteps) onToolchain(ctx context.Context, want *delivery.ToolchainUsed) (*delivery.ToolchainUsed, error) { + if want == nil || s.Toolchain == nil { + return nil, nil + } + tc, err := s.Toolchain(ctx) + if err != nil { + return nil, err + } + if tc.Version != want.Version || tc.Recipe != want.Recipe { + return nil, &Declining{Lacks: []string{fmt.Sprintf("the agreed toolchain %s (recipe %s)", want.Version, want.Recipe)}, + Text: fmt.Sprintf("the ask is for %s (recipe %s) and this node builds with %s (recipe %s): it sits out, and the ask is left to a node on the agreed toolchain", + want.Version, want.Recipe, tc.Version, tc.Recipe)} + } + return &tc, nil +} + func (s AskSteps) build(ctx context.Context, job delivery.BuildJob) (any, error) { answer := delivery.BuildAnswer{} - if job.Toolchain != nil && s.Toolchain != nil { - tc, err := s.Toolchain(ctx) - if err != nil { - return nil, err - } - if tc.Version != job.Toolchain.Version || tc.Recipe != job.Toolchain.Recipe { - // This node is not on the agreed toolchain: it declines the ask for a node that is (novox/hq ADR 0584 clause 2). - return nil, &Declining{Lacks: []string{fmt.Sprintf("the agreed toolchain %s (recipe %s)", job.Toolchain.Version, job.Toolchain.Recipe)}, - Text: fmt.Sprintf("the ask is for %s (recipe %s) and this node builds with %s (recipe %s): it sits out, and the ask is left to a node on the agreed toolchain", - job.Toolchain.Version, job.Toolchain.Recipe, tc.Version, tc.Recipe)} - } - answer.Toolchain = &tc + tc, err := s.onToolchain(ctx, job.Toolchain) + if err != nil { + return nil, err } + answer.Toolchain = tc var f fetched defer func() { s.release(f.Workspace) }() for _, a := range job.Artifacts { @@ -183,6 +194,9 @@ func (s AskSteps) build(ctx context.Context, job delivery.BuildJob) (any, error) } func (s AskSteps) check(ctx context.Context, job delivery.CheckJob) (any, error) { + if _, err := s.onToolchain(ctx, job.Toolchain); err != nil { + return nil, err + } r, f, err := s.fetch(ctx, job.Fetch) if err != nil { return nil, err diff --git a/go/buildstep/askrun_test.go b/go/buildstep/askrun_test.go index 343bdc4..30d4072 100644 --- a/go/buildstep/askrun_test.go +++ b/go/buildstep/askrun_test.go @@ -116,6 +116,12 @@ func TestRunAskRefusesAnotherToolchain(t *testing.T) { if !errors.As(err, &dec) || len(dec.Lacks) != 1 || len(r.calls) != 0 { t.Fatalf("err %v, calls %v", err, r.calls) } + // A check ask names the toolchain too (ADR 0584), and a node on another declines it before it fetches. + _, err = RunAsk(context.Background(), s, ask(delivery.CheckJob{Fetch: map[string]any{"repository": "novox/x", "commit": runCommit}, Levels: []string{delivery.LevelUnit}, + Toolchain: &delivery.ToolchainUsed{Version: "go1.26", Recipe: "3"}})) + if !errors.As(err, &dec) || len(r.calls) != 0 { + t.Fatalf("a check ask: err %v, calls %v", err, r.calls) + } } // 1887/A3, A5a: a check ask runs the levels of one check after one fetch: it digests the unit level, takes the kept diff --git a/go/delivery/stepasks.go b/go/delivery/stepasks.go index 17ec656..155a685 100644 --- a/go/delivery/stepasks.go +++ b/go/delivery/stepasks.go @@ -369,6 +369,9 @@ type CheckJob struct { // has to list them. The build step hands them to its test tool as the argument NodeNamesArg, and the build module // gives them to the tests in NodeNamesEnv. NodeNames []string `json:"node-names,omitempty"` + // Toolchain is the toolchain the checker agreed for the check's language (novox/hq ADR 0584): a node building with + // another declines the ask, so a pass is never recorded as run on a version it did not run on. Nil names none. + Toolchain *ToolchainUsed `json:"toolchain,omitempty"` } // NodeNamesArg is the test tool's argument that carries a check's node names, and NodeNamesEnv the environment