From 1264ec03c04bf5a32d7b71b4f0cb89722c7ba3df Mon Sep 17 00:00:00 2001 From: "prath.shenoy" Date: Tue, 20 Oct 2026 20:26:14 +0000 Subject: [PATCH] feat(stovepipe): Add build analysis contract --- Makefile | 1 + stovepipe/entity/BUILD.bazel | 1 + stovepipe/entity/build_issue.go | 24 +++++ stovepipe/extension/buildissue/BUILD.bazel | 9 ++ stovepipe/extension/buildissue/README.md | 13 +++ stovepipe/extension/buildissue/buildissue.go | 45 +++++++++ .../extension/buildissue/mock/BUILD.bazel | 13 +++ .../buildissue/mock/buildissue_mock.go | 97 +++++++++++++++++++ 8 files changed, 203 insertions(+) create mode 100644 stovepipe/entity/build_issue.go create mode 100644 stovepipe/extension/buildissue/BUILD.bazel create mode 100644 stovepipe/extension/buildissue/README.md create mode 100644 stovepipe/extension/buildissue/buildissue.go create mode 100644 stovepipe/extension/buildissue/mock/BUILD.bazel create mode 100644 stovepipe/extension/buildissue/mock/buildissue_mock.go diff --git a/Makefile b/Makefile index 0861212ac..24597829b 100644 --- a/Makefile +++ b/Makefile @@ -627,6 +627,7 @@ GO_GENERATE_PACKAGES := \ ./runway/extension/merger/... \ ./stovepipe/controller/... \ ./stovepipe/core/requestlog/... \ + ./stovepipe/extension/buildissue/... \ ./stovepipe/extension/buildrunner/... \ ./stovepipe/extension/projectstatus/... \ ./stovepipe/extension/queueconfig/... \ diff --git a/stovepipe/entity/BUILD.bazel b/stovepipe/entity/BUILD.bazel index 3b255e96b..f5269f909 100644 --- a/stovepipe/entity/BUILD.bazel +++ b/stovepipe/entity/BUILD.bazel @@ -4,6 +4,7 @@ go_library( name = "go_default_library", srcs = [ "build.go", + "build_issue.go", "ingest.go", "list.go", "project_status.go", diff --git a/stovepipe/entity/build_issue.go b/stovepipe/entity/build_issue.go new file mode 100644 index 000000000..9d32a8e00 --- /dev/null +++ b/stovepipe/entity/build_issue.go @@ -0,0 +1,24 @@ +// Copyright (c) 2026 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package entity + +// BuildIssue identifies a validation failure by task and optional unit. +type BuildIssue struct { + // TaskID identifies the logical validation task, stable across builds and retries. + TaskID string `json:"task_id"` + // UnitID identifies the failed unit within the task; empty for a task-wide issue. + // Repeated observations of the same condition use the same TaskID and UnitID. + UnitID string `json:"unit_id"` +} diff --git a/stovepipe/extension/buildissue/BUILD.bazel b/stovepipe/extension/buildissue/BUILD.bazel new file mode 100644 index 000000000..8453bf936 --- /dev/null +++ b/stovepipe/extension/buildissue/BUILD.bazel @@ -0,0 +1,9 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["buildissue.go"], + importpath = "github.com/uber/submitqueue/stovepipe/extension/buildissue", + visibility = ["//visibility:public"], + deps = ["//stovepipe/entity:go_default_library"], +) diff --git a/stovepipe/extension/buildissue/README.md b/stovepipe/extension/buildissue/README.md new file mode 100644 index 000000000..ac96ce3bc --- /dev/null +++ b/stovepipe/extension/buildissue/README.md @@ -0,0 +1,13 @@ +# Build issue detection + +`Detector` discovers confirmed validation issues from a build. It returns failure observations, not a complete view of build state. + +Issue values are defined in the [domain model](../../entity/build_issue.go). A validation task is a logical validation operation. A validation unit is an identifiable piece of work within a task. + +Within a validation queue, TaskID and UnitID together identify the validation condition. Repeated observations of the same condition use the same pair across builds. UnitID is empty for a task-wide issue. + +Each detection returns issues discoverable at that time and may repeat earlier observations. Detection does not mark issues as handled. An issue's absence from a detection result does not mean it is resolved. + +Detectors must support concurrent calls. Dependency failures return plain errors; issue results returned with an error must not be used. + +`Factory` resolves the detector for a validation queue. diff --git a/stovepipe/extension/buildissue/buildissue.go b/stovepipe/extension/buildissue/buildissue.go new file mode 100644 index 000000000..c2d64d8cc --- /dev/null +++ b/stovepipe/extension/buildissue/buildissue.go @@ -0,0 +1,45 @@ +// Copyright (c) 2026 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package buildissue defines confirmed-issue discovery for active or +// completed builds, independently of build status polling. +package buildissue + +//go:generate mockgen -source=buildissue.go -destination=mock/buildissue_mock.go -package=mock + +import ( + "context" + + "github.com/uber/submitqueue/stovepipe/entity" +) + +// Detector discovers confirmed issues without waiting for build completion. +// Implementations own confirmation and retry rules and must be concurrency-safe. +// Detection does not mark issues as handled. +// An issue's absence from a detection result does not mean it is resolved. +// Return plain dependency errors; issue results returned with an error must not be used. +type Detector interface { + Detect(ctx context.Context, buildID entity.BuildID) ([]entity.BuildIssue, error) +} + +// Config identifies the queue whose issue detector is requested. +type Config struct { + // QueueName identifies the queue. + QueueName string +} + +// Factory resolves a queue's detector; implementations and routing belong to host wiring. +type Factory interface { + For(cfg Config) (Detector, error) +} diff --git a/stovepipe/extension/buildissue/mock/BUILD.bazel b/stovepipe/extension/buildissue/mock/BUILD.bazel new file mode 100644 index 000000000..592386072 --- /dev/null +++ b/stovepipe/extension/buildissue/mock/BUILD.bazel @@ -0,0 +1,13 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["buildissue_mock.go"], + importpath = "github.com/uber/submitqueue/stovepipe/extension/buildissue/mock", + visibility = ["//visibility:public"], + deps = [ + "//stovepipe/entity:go_default_library", + "//stovepipe/extension/buildissue:go_default_library", + "@org_uber_go_mock//gomock:go_default_library", + ], +) diff --git a/stovepipe/extension/buildissue/mock/buildissue_mock.go b/stovepipe/extension/buildissue/mock/buildissue_mock.go new file mode 100644 index 000000000..3cca63ce7 --- /dev/null +++ b/stovepipe/extension/buildissue/mock/buildissue_mock.go @@ -0,0 +1,97 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: buildissue.go +// +// Generated by this command: +// +// mockgen -source=buildissue.go -destination=mock/buildissue_mock.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + entity "github.com/uber/submitqueue/stovepipe/entity" + buildissue "github.com/uber/submitqueue/stovepipe/extension/buildissue" + gomock "go.uber.org/mock/gomock" +) + +// MockDetector is a mock of Detector interface. +type MockDetector struct { + ctrl *gomock.Controller + recorder *MockDetectorMockRecorder + isgomock struct{} +} + +// MockDetectorMockRecorder is the mock recorder for MockDetector. +type MockDetectorMockRecorder struct { + mock *MockDetector +} + +// NewMockDetector creates a new mock instance. +func NewMockDetector(ctrl *gomock.Controller) *MockDetector { + mock := &MockDetector{ctrl: ctrl} + mock.recorder = &MockDetectorMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDetector) EXPECT() *MockDetectorMockRecorder { + return m.recorder +} + +// Detect mocks base method. +func (m *MockDetector) Detect(ctx context.Context, buildID entity.BuildID) ([]entity.BuildIssue, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Detect", ctx, buildID) + ret0, _ := ret[0].([]entity.BuildIssue) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Detect indicates an expected call of Detect. +func (mr *MockDetectorMockRecorder) Detect(ctx, buildID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Detect", reflect.TypeOf((*MockDetector)(nil).Detect), ctx, buildID) +} + +// MockFactory is a mock of Factory interface. +type MockFactory struct { + ctrl *gomock.Controller + recorder *MockFactoryMockRecorder + isgomock struct{} +} + +// MockFactoryMockRecorder is the mock recorder for MockFactory. +type MockFactoryMockRecorder struct { + mock *MockFactory +} + +// NewMockFactory creates a new mock instance. +func NewMockFactory(ctrl *gomock.Controller) *MockFactory { + mock := &MockFactory{ctrl: ctrl} + mock.recorder = &MockFactoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockFactory) EXPECT() *MockFactoryMockRecorder { + return m.recorder +} + +// For mocks base method. +func (m *MockFactory) For(cfg buildissue.Config) (buildissue.Detector, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "For", cfg) + ret0, _ := ret[0].(buildissue.Detector) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// For indicates an expected call of For. +func (mr *MockFactoryMockRecorder) For(cfg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "For", reflect.TypeOf((*MockFactory)(nil).For), cfg) +}