From a939137728aded2ed1431b5f8d414eacc6a93866 Mon Sep 17 00:00:00 2001 From: "prath.shenoy" Date: Sun, 8 Nov 2026 22:48:44 +0000 Subject: [PATCH] feat(stovepipe): Define issue project attribution ## Summary **What**: - Define project attribution for confirmed build issues. **Why**: - Identify which failures affect each project. Existing project-status results describe a project's health but do not identify the individual issues behind that result. This contract provides the per-issue project associations needed to build project issue views. ## Test Plan - [x] Add unit tests. ## Revert Plan - Revert this PR. No runtime wiring or stored data changes depend on this contract yet. ## Issue Links - [CODEM-607](https://linear.app/uber/issue/CODEM-607) --- Makefile | 1 + stovepipe/extension/projectissues/BUILD.bazel | 9 ++ stovepipe/extension/projectissues/README.md | 17 ++++ .../extension/projectissues/mock/BUILD.bazel | 13 +++ .../projectissues/mock/projectissues_mock.go | 97 +++++++++++++++++++ .../extension/projectissues/noop/BUILD.bazel | 23 +++++ .../extension/projectissues/noop/noop.go | 44 +++++++++ .../extension/projectissues/noop/noop_test.go | 39 ++++++++ .../extension/projectissues/projectissues.go | 55 +++++++++++ 9 files changed, 298 insertions(+) create mode 100644 stovepipe/extension/projectissues/BUILD.bazel create mode 100644 stovepipe/extension/projectissues/README.md create mode 100644 stovepipe/extension/projectissues/mock/BUILD.bazel create mode 100644 stovepipe/extension/projectissues/mock/projectissues_mock.go create mode 100644 stovepipe/extension/projectissues/noop/BUILD.bazel create mode 100644 stovepipe/extension/projectissues/noop/noop.go create mode 100644 stovepipe/extension/projectissues/noop/noop_test.go create mode 100644 stovepipe/extension/projectissues/projectissues.go diff --git a/Makefile b/Makefile index be6c4b2fb..2d5df917c 100644 --- a/Makefile +++ b/Makefile @@ -654,6 +654,7 @@ GO_GENERATE_PACKAGES := \ ./stovepipe/extension/buildissue/... \ ./stovepipe/extension/buildrunner/... \ ./stovepipe/extension/fixer/... \ + ./stovepipe/extension/projectissues/... \ ./stovepipe/extension/projectstatus/... \ ./stovepipe/extension/queueconfig/... \ ./stovepipe/extension/sourcecontrol/... \ diff --git a/stovepipe/extension/projectissues/BUILD.bazel b/stovepipe/extension/projectissues/BUILD.bazel new file mode 100644 index 000000000..1f026bb8c --- /dev/null +++ b/stovepipe/extension/projectissues/BUILD.bazel @@ -0,0 +1,9 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["projectissues.go"], + importpath = "github.com/uber/submitqueue/stovepipe/extension/projectissues", + visibility = ["//visibility:public"], + deps = ["//stovepipe/entity:go_default_library"], +) diff --git a/stovepipe/extension/projectissues/README.md b/stovepipe/extension/projectissues/README.md new file mode 100644 index 000000000..86e1a4d5c --- /dev/null +++ b/stovepipe/extension/projectissues/README.md @@ -0,0 +1,17 @@ +# Issue project attribution + +Issue attribution answers which projects each confirmed build issue affects. The project names use the same identity as project status results. + +## Inputs and results + +A queue-bound attributer accepts the validation request. The request supplies the commit, baseline, build strategy, and build identity. Implementations load confirmed build issues, dependency graphs, and project configuration through injected dependencies; callers do not supply issue lists or graph data. For terminal validation, the issues belong to the request's terminal build and attribution must wait until terminal build analysis has finished recording them. + +Each result identifies a unit-level build issue by task and unit and lists its affected projects. Projects must be both covered by that validation and affected by the issue. A dependency-based implementation follows the graph at the validated commit. + +A completed assessment returns all results in one call, with one entry per distinct build issue, including an empty project list when none apply. The noop implementation returns no results when attribution is unconfigured. + +## Errors and concurrency + +Attribution is all-or-nothing: any task-wide failure, incomplete validation evidence, or dependency failure returns an error for the entire validation. Callers must discard results returned alongside an error. + +Implementations must support concurrent calls, leave inputs unchanged, and not consume validation evidence. diff --git a/stovepipe/extension/projectissues/mock/BUILD.bazel b/stovepipe/extension/projectissues/mock/BUILD.bazel new file mode 100644 index 000000000..6fa1fef0f --- /dev/null +++ b/stovepipe/extension/projectissues/mock/BUILD.bazel @@ -0,0 +1,13 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["projectissues_mock.go"], + importpath = "github.com/uber/submitqueue/stovepipe/extension/projectissues/mock", + visibility = ["//visibility:public"], + deps = [ + "//stovepipe/entity:go_default_library", + "//stovepipe/extension/projectissues:go_default_library", + "@org_uber_go_mock//gomock:go_default_library", + ], +) diff --git a/stovepipe/extension/projectissues/mock/projectissues_mock.go b/stovepipe/extension/projectissues/mock/projectissues_mock.go new file mode 100644 index 000000000..8df4cdfb5 --- /dev/null +++ b/stovepipe/extension/projectissues/mock/projectissues_mock.go @@ -0,0 +1,97 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: projectissues.go +// +// Generated by this command: +// +// mockgen -source=projectissues.go -destination=mock/projectissues_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" + projectissues "github.com/uber/submitqueue/stovepipe/extension/projectissues" + gomock "go.uber.org/mock/gomock" +) + +// MockAttributer is a mock of Attributer interface. +type MockAttributer struct { + ctrl *gomock.Controller + recorder *MockAttributerMockRecorder + isgomock struct{} +} + +// MockAttributerMockRecorder is the mock recorder for MockAttributer. +type MockAttributerMockRecorder struct { + mock *MockAttributer +} + +// NewMockAttributer creates a new mock instance. +func NewMockAttributer(ctrl *gomock.Controller) *MockAttributer { + mock := &MockAttributer{ctrl: ctrl} + mock.recorder = &MockAttributerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAttributer) EXPECT() *MockAttributerMockRecorder { + return m.recorder +} + +// Attribute mocks base method. +func (m *MockAttributer) Attribute(ctx context.Context, request entity.Request) ([]projectissues.Attribution, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Attribute", ctx, request) + ret0, _ := ret[0].([]projectissues.Attribution) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Attribute indicates an expected call of Attribute. +func (mr *MockAttributerMockRecorder) Attribute(ctx, request any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Attribute", reflect.TypeOf((*MockAttributer)(nil).Attribute), ctx, request) +} + +// MockAttributerFactory is a mock of AttributerFactory interface. +type MockAttributerFactory struct { + ctrl *gomock.Controller + recorder *MockAttributerFactoryMockRecorder + isgomock struct{} +} + +// MockAttributerFactoryMockRecorder is the mock recorder for MockAttributerFactory. +type MockAttributerFactoryMockRecorder struct { + mock *MockAttributerFactory +} + +// NewMockAttributerFactory creates a new mock instance. +func NewMockAttributerFactory(ctrl *gomock.Controller) *MockAttributerFactory { + mock := &MockAttributerFactory{ctrl: ctrl} + mock.recorder = &MockAttributerFactoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAttributerFactory) EXPECT() *MockAttributerFactoryMockRecorder { + return m.recorder +} + +// For mocks base method. +func (m *MockAttributerFactory) For(cfg projectissues.AttributerConfig) (projectissues.Attributer, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "For", cfg) + ret0, _ := ret[0].(projectissues.Attributer) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// For indicates an expected call of For. +func (mr *MockAttributerFactoryMockRecorder) For(cfg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "For", reflect.TypeOf((*MockAttributerFactory)(nil).For), cfg) +} diff --git a/stovepipe/extension/projectissues/noop/BUILD.bazel b/stovepipe/extension/projectissues/noop/BUILD.bazel new file mode 100644 index 000000000..d5330db2a --- /dev/null +++ b/stovepipe/extension/projectissues/noop/BUILD.bazel @@ -0,0 +1,23 @@ +load("@rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["noop.go"], + importpath = "github.com/uber/submitqueue/stovepipe/extension/projectissues/noop", + visibility = ["//visibility:public"], + deps = [ + "//stovepipe/entity:go_default_library", + "//stovepipe/extension/projectissues:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["noop_test.go"], + embed = [":go_default_library"], + deps = [ + "//stovepipe/entity:go_default_library", + "//stovepipe/extension/projectissues:go_default_library", + "@com_github_stretchr_testify//require:go_default_library", + ], +) diff --git a/stovepipe/extension/projectissues/noop/noop.go b/stovepipe/extension/projectissues/noop/noop.go new file mode 100644 index 000000000..c51fcc765 --- /dev/null +++ b/stovepipe/extension/projectissues/noop/noop.go @@ -0,0 +1,44 @@ +// 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 noop skips project attribution when no backend is configured. +package noop + +import ( + "context" + + "github.com/uber/submitqueue/stovepipe/entity" + "github.com/uber/submitqueue/stovepipe/extension/projectissues" +) + +var _ projectissues.AttributerFactory = AttributerFactory{} + +// AttributerFactory returns an attributer that performs no assessment. +type AttributerFactory struct{} + +// New returns a no-op attribution factory. +func New() AttributerFactory { + return AttributerFactory{} +} + +// For returns an attributer that skips attribution. +func (AttributerFactory) For(projectissues.AttributerConfig) (projectissues.Attributer, error) { + return attributer{}, nil +} + +type attributer struct{} + +func (attributer) Attribute(context.Context, entity.Request) ([]projectissues.Attribution, error) { + return nil, nil +} diff --git a/stovepipe/extension/projectissues/noop/noop_test.go b/stovepipe/extension/projectissues/noop/noop_test.go new file mode 100644 index 000000000..efdca196a --- /dev/null +++ b/stovepipe/extension/projectissues/noop/noop_test.go @@ -0,0 +1,39 @@ +// 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 noop + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "github.com/uber/submitqueue/stovepipe/entity" + "github.com/uber/submitqueue/stovepipe/extension/projectissues" +) + +func TestAttributerSkipsAttribution(t *testing.T) { + attributer, err := New().For(projectissues.AttributerConfig{QueueName: "example"}) + require.NoError(t, err) + request := entity.Request{ + ID: "1", Queue: "example", URI: "git://example/head", + BaseURI: "git://example/base", BuildStrategy: entity.BuildStrategyIncrementalSinceGreen, + State: entity.RequestStateFailed, BuildID: "build-1", TerminalBuildID: "build-1", + } + + attributions, err := attributer.Attribute(context.Background(), request) + + require.NoError(t, err) + require.Empty(t, attributions) +} diff --git a/stovepipe/extension/projectissues/projectissues.go b/stovepipe/extension/projectissues/projectissues.go new file mode 100644 index 000000000..2b6e909e1 --- /dev/null +++ b/stovepipe/extension/projectissues/projectissues.go @@ -0,0 +1,55 @@ +// 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 projectissues defines project attribution for individual build issues. +package projectissues + +//go:generate mockgen -source=projectissues.go -destination=mock/projectissues_mock.go -package=mock + +import ( + "context" + + "github.com/uber/submitqueue/stovepipe/entity" +) + +// Attribution identifies the projects affected by one issue within a validation's scope. +type Attribution struct { + // TaskID identifies the issue's validation task and must be nonempty. + TaskID string + // UnitID identifies the failed unit within the task and must be nonempty. + UnitID string + // Projects contains distinct, nonempty project names, using the same identity as project status. + // An empty list means the issue affects no projects within this validation's scope. + Projects []string +} + +// Attributer attributes confirmed build issues within the request's validation scope. +// Return an error for incomplete attribution, including any task-wide failure. +// Returning no results is valid. +// Calls must be concurrency-safe and leave inputs unchanged. +// Results returned with an error must not be used. +type Attributer interface { + Attribute(ctx context.Context, request entity.Request) ([]Attribution, error) +} + +// AttributerConfig identifies the queue whose attributer is requested. +type AttributerConfig struct { + // QueueName identifies the validation queue. + QueueName string +} + +// AttributerFactory provides an attributer for one queue. +type AttributerFactory interface { + For(cfg AttributerConfig) (Attributer, error) +}