Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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/... \
Expand Down
9 changes: 9 additions & 0 deletions stovepipe/extension/projectissues/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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"],
)
17 changes: 17 additions & 0 deletions stovepipe/extension/projectissues/README.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 13 additions & 0 deletions stovepipe/extension/projectissues/mock/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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",
],
)
97 changes: 97 additions & 0 deletions stovepipe/extension/projectissues/mock/projectissues_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions stovepipe/extension/projectissues/noop/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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",
],
)
44 changes: 44 additions & 0 deletions stovepipe/extension/projectissues/noop/noop.go
Original file line number Diff line number Diff line change
@@ -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
}
39 changes: 39 additions & 0 deletions stovepipe/extension/projectissues/noop/noop_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
55 changes: 55 additions & 0 deletions stovepipe/extension/projectissues/projectissues.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading