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
2 changes: 2 additions & 0 deletions stovepipe/entity/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ go_library(
"list_open_issues.go",
"open_issue.go",
"project_fact_by_degree.go",
"project_issue_attribution.go",
"project_status.go",
"queue.go",
"queue_config.go",
Expand Down Expand Up @@ -45,6 +46,7 @@ go_test(
"issue_summary_test.go",
"issue_tracking_state_test.go",
"project_fact_by_degree_test.go",
"project_issue_attribution_test.go",
"queue_state_test.go",
"request_id_test.go",
"request_issue_summary_test.go",
Expand Down
41 changes: 41 additions & 0 deletions stovepipe/entity/project_issue_attribution.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// 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

import "fmt"

// ProjectIssueAttribution is one immutable issue/project pair within an attribution result.
type ProjectIssueAttribution struct {
// TaskID identifies the validation task and must be nonempty.
TaskID string
// UnitID identifies the failed unit within the task and must be nonempty.
UnitID string
// Project is an affected project name and must be nonempty.
Project string
}

// Validate verifies the identity of a unit-level attribution.
func (a ProjectIssueAttribution) Validate() error {
if a.TaskID == "" {
return fmt.Errorf("project issue attribution task ID must not be empty")
}
if a.UnitID == "" {
return fmt.Errorf("project issue attribution unit ID must not be empty")
}
if a.Project == "" {
return fmt.Errorf("project issue attribution project must not be empty")
}
return nil
}
42 changes: 42 additions & 0 deletions stovepipe/entity/project_issue_attribution_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// 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

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestProjectIssueAttributionValidate(t *testing.T) {
for _, tt := range []struct {
name string
entry ProjectIssueAttribution
wantErr bool
}{
{name: "affected project", entry: ProjectIssueAttribution{TaskID: "test", UnitID: "//example:test", Project: "service"}},
{name: "missing project", entry: ProjectIssueAttribution{TaskID: "test", UnitID: "//example:test"}, wantErr: true},
{name: "missing task", entry: ProjectIssueAttribution{UnitID: "//example:test", Project: "service"}, wantErr: true},
{name: "task-wide failure", entry: ProjectIssueAttribution{TaskID: "test", Project: "service"}, wantErr: true},
} {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr {
require.Error(t, tt.entry.Validate())
} else {
require.NoError(t, tt.entry.Validate())
}
})
}
}
4 changes: 3 additions & 1 deletion stovepipe/entity/request_log.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ const (
RequestEventValidationFactRecorded RequestEvent = "validation_fact_recorded"
// RequestEventProjectFactsRecorded records that all project validation facts for a request were established.
RequestEventProjectFactsRecorded RequestEvent = "project_facts_recorded"
// RequestEventProjectIssuesRecorded records completed project-issue attribution for a request.
RequestEventProjectIssuesRecorded RequestEvent = "project_issues_recorded"
// RequestEventRecordAbandoned records that record-stage work stopped after exhausting retries.
RequestEventRecordAbandoned RequestEvent = "record_abandoned"
// RequestEventBuildAnalysisAbandoned records incomplete build analysis, not a validation outcome.
Expand Down Expand Up @@ -153,7 +155,7 @@ func (e RequestLog) validateEvent() error {
return fmt.Errorf("event log must not contain request-state context")
}
switch e.Event {
case RequestEventBuildTriggered, RequestEventBuildFinished, RequestEventValidationFactRecorded, RequestEventProjectFactsRecorded,
case RequestEventBuildTriggered, RequestEventBuildFinished, RequestEventValidationFactRecorded, RequestEventProjectFactsRecorded, RequestEventProjectIssuesRecorded,
RequestEventRecordAbandoned, RequestEventBuildAnalysisAbandoned, RequestEventBuildAnalysisCompleted,
RequestEventIssueTrackingStarted, RequestEventIssuesReconciled, RequestEventIssueTrackingAbandoned,
RequestEventFixSubmitted, RequestEventFixSubmissionSkipped, RequestEventFixSubmissionAbandoned:
Expand Down
10 changes: 10 additions & 0 deletions stovepipe/entity/request_log_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ func TestRequestLogValidate(t *testing.T) {
return entry
},
},
{
name: "project issues recorded",
mutate: func(entry RequestLog) RequestLog {
entry.State = RequestStateUnknown
entry.Event = RequestEventProjectIssuesRecorded
entry.RequestVersion = 0
entry.Metadata = map[string]string{"build_id": "42"}
return entry
},
},
{
name: "build event",
mutate: func(entry RequestLog) RequestLog {
Expand Down
1 change: 1 addition & 0 deletions stovepipe/extension/storage/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ go_library(
"request_acceptance_store.go",
"request_issue_summary_store.go",
"request_log_store.go",
"request_projects_by_issue_store.go",
"request_store.go",
"request_summary_store.go",
"request_uri_store.go",
Expand Down
8 changes: 8 additions & 0 deletions stovepipe/extension/storage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ Same design space as [`submitqueue/extension/storage`](../../../submitqueue/exte

`ProjectFactByDegreeStore` is the ordered lookup mapping for named project facts. Its immutable `(queue, URI, degree DESC, project ASC)` primary key stores the original degree and orders by descending degree and ascending bytewise project ID, so listing is a bounded key-range read. It holds no duplicate validation payload; readers hydrate facts by their original keys and verify the mapping. The record controller ensures mappings from persisted winning facts before recording project completion, including retries after partial writes. Create the table before deploying writers. The table starts empty: only results processed by index-aware writers receive mappings, with no historical population or read-time fallback.

## Request projects by issue

`RequestProjectsByIssueStore` retains source rows keyed by `(queue, request_id, task_id, unit_id, project)`. Each row records one affected project for a unit-level issue; task, unit, and project must be nonempty. Rows are immutable and duplicate creates return `ErrAlreadyExists`.

Attribution is assumed stable for a request. Retries recreate the same pairs and treat duplicate creates as success. All rows must be written before retaining the request's `project_issues_recorded` occurrence, whose metadata identifies the build. The occurrence establishes completion even when there are no associations. Without it, retained rows are incomplete.

Listing fixes the queue/request prefix and returns bounded pages in ascending bytewise task/unit/project order. The cursor excludes its key. An empty page ends that range; it does not establish that attribution completed. The MySQL key uses the issue stores' task/unit capacities and a 1020-byte project name, matching the byte capacity of existing project names. Create the source table before deploying writers. This store does not maintain current project-issue lookups.

## Build issue observations

`BuildIssueStore` accumulates observations identified by build, task, and unit within the bound queue. The build ID is supplied separately from the detected issue. Repeated observations return `ErrAlreadyExists`, which callers may treat as successful persistence. Build and task IDs must be nonempty; empty unit IDs represent task-wide observations.
Expand Down
1 change: 1 addition & 0 deletions stovepipe/extension/storage/mock/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ go_library(
"request_acceptance_store_mock.go",
"request_issue_summary_store_mock.go",
"request_log_store_mock.go",
"request_projects_by_issue_store_mock.go",
"request_store_mock.go",
"request_summary_store_mock.go",
"request_uri_store_mock.go",
Expand Down

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

14 changes: 14 additions & 0 deletions stovepipe/extension/storage/mock/storage_mock.go

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

2 changes: 2 additions & 0 deletions stovepipe/extension/storage/mysql/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ go_library(
"request_acceptance_store.go",
"request_issue_summary_store.go",
"request_log_store.go",
"request_projects_by_issue_store.go",
"request_store.go",
"request_summary_store.go",
"request_uri_store.go",
Expand Down Expand Up @@ -54,6 +55,7 @@ go_test(
"request_acceptance_store_test.go",
"request_issue_summary_store_test.go",
"request_log_store_test.go",
"request_projects_by_issue_store_test.go",
"request_store_test.go",
"request_summary_store_test.go",
"request_uri_store_test.go",
Expand Down
Loading
Loading