diff --git a/stovepipe/controller/projectanalysis/BUILD.bazel b/stovepipe/controller/projectanalysis/BUILD.bazel new file mode 100644 index 000000000..2c628f061 --- /dev/null +++ b/stovepipe/controller/projectanalysis/BUILD.bazel @@ -0,0 +1,18 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["projectanalysis.go"], + importpath = "github.com/uber/submitqueue/stovepipe/controller/projectanalysis", + visibility = ["//visibility:public"], + deps = [ + "//platform/consumer:go_default_library", + "//stovepipe/core/projectissues:go_default_library", + "//stovepipe/core/requestlog:go_default_library", + "//stovepipe/extension/projectissues:go_default_library", + "//stovepipe/extension/projectstatus:go_default_library", + "//stovepipe/extension/storage:go_default_library", + "@com_github_uber_go_tally//:go_default_library", + "@org_uber_go_zap//:go_default_library", + ], +) diff --git a/stovepipe/controller/projectanalysis/projectanalysis.go b/stovepipe/controller/projectanalysis/projectanalysis.go new file mode 100644 index 000000000..4e7f1a43b --- /dev/null +++ b/stovepipe/controller/projectanalysis/projectanalysis.go @@ -0,0 +1,69 @@ +// 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 projectanalysis owns terminal project processing. +package projectanalysis + +import ( + "github.com/uber-go/tally" + "github.com/uber/submitqueue/platform/consumer" + projectissuescore "github.com/uber/submitqueue/stovepipe/core/projectissues" + "github.com/uber/submitqueue/stovepipe/core/requestlog" + "github.com/uber/submitqueue/stovepipe/extension/projectissues" + "github.com/uber/submitqueue/stovepipe/extension/projectstatus" + "github.com/uber/submitqueue/stovepipe/extension/storage" + "go.uber.org/zap" +) + +type Controller struct { + logger *zap.SugaredLogger + scope tally.Scope + stores storage.Factory + requestLogMaterializer requestlog.Materializer + projectIssueMaterializer projectissuescore.Materializer + resolvers projectstatus.ResolverFactory + attributers projectissues.AttributerFactory + registry consumer.TopicRegistry + topicKey consumer.TopicKey + consumerGroup string +} + +func NewController( + logger *zap.SugaredLogger, + scope tally.Scope, + stores storage.Factory, + requestLogMaterializer requestlog.Materializer, + resolvers projectstatus.ResolverFactory, + attributers projectissues.AttributerFactory, + registry consumer.TopicRegistry, + topicKey consumer.TopicKey, + consumerGroup string, +) *Controller { + return &Controller{ + logger: logger.Named("projectanalysis_controller"), + scope: scope.SubScope("projectanalysis_controller"), + stores: stores, + requestLogMaterializer: requestLogMaterializer, + projectIssueMaterializer: projectissuescore.NewMaterializer(scope), + resolvers: resolvers, + attributers: attributers, + registry: registry, + topicKey: topicKey, + consumerGroup: consumerGroup, + } +} + +func (c *Controller) Name() string { return "projectanalysis" } +func (c *Controller) TopicKey() consumer.TopicKey { return c.topicKey } +func (c *Controller) ConsumerGroup() string { return c.consumerGroup } diff --git a/stovepipe/core/messagequeue/messagequeue.go b/stovepipe/core/messagequeue/messagequeue.go index 487d88cb6..d5fae1a8f 100644 --- a/stovepipe/core/messagequeue/messagequeue.go +++ b/stovepipe/core/messagequeue/messagequeue.go @@ -59,6 +59,9 @@ type ( // FixRequest identifies the build whose issues should be considered for repair. FixRequest = protopb.FixRequest + // ProjectAnalysis requests project-status resolution and issue attribution. + ProjectAnalysis = protopb.ProjectAnalysis + // Record is the payload buildsignal publishes to the record stage once a // build reaches a terminal status: the build id to record. Record = protopb.Record diff --git a/stovepipe/core/messagequeue/messagequeue_test.go b/stovepipe/core/messagequeue/messagequeue_test.go index c5b5af291..702cfba1e 100644 --- a/stovepipe/core/messagequeue/messagequeue_test.go +++ b/stovepipe/core/messagequeue/messagequeue_test.go @@ -80,7 +80,7 @@ func TestWireFormat(t *testing.T) { // no topic_keys option names an unknown key. func TestTopicKeysBindEveryTopicKey(t *testing.T) { bound := map[string]int{} - for _, m := range []proto.Message{&ProcessRequest{}, &BuildRequest{}, &BuildSignal{}, &BuildAnalysis{}, &IssueTrack{}, &FixRequest{}, &Record{}} { + for _, m := range []proto.Message{&ProcessRequest{}, &BuildRequest{}, &BuildSignal{}, &BuildAnalysis{}, &IssueTrack{}, &FixRequest{}, &ProjectAnalysis{}, &Record{}} { keys := TopicKeys(m) require.NotEmpty(t, keys, "message must declare a non-empty topic_keys option") for _, key := range keys { @@ -95,6 +95,7 @@ func TestTopicKeysBindEveryTopicKey(t *testing.T) { TopicKeyBuildAnalysis, TopicKeyIssueTrack, TopicKeyFix, + TopicKeyProjectAnalysis, TopicKeyRecord, } @@ -108,6 +109,18 @@ func TestTopicKeysBindEveryTopicKey(t *testing.T) { } } +func TestProjectAnalysisRoundTrip(t *testing.T) { + request := &ProjectAnalysis{Id: "42", QueueName: "validation/main"} + data, err := Marshal(request) + require.NoError(t, err) + + got := &ProjectAnalysis{} + require.NoError(t, Unmarshal(data, got)) + + assert.True(t, proto.Equal(request, got)) + assert.JSONEq(t, `{"id":"42","queue_name":"validation/main"}`, string(data)) +} + func TestBuildAnalysisRoundTrip(t *testing.T) { analysis := &BuildAnalysis{Id: "build/42", QueueName: "validation/main"} data, err := Marshal(analysis) diff --git a/stovepipe/core/messagequeue/proto/BUILD.bazel b/stovepipe/core/messagequeue/proto/BUILD.bazel index 162570f78..9d81fcaae 100644 --- a/stovepipe/core/messagequeue/proto/BUILD.bazel +++ b/stovepipe/core/messagequeue/proto/BUILD.bazel @@ -6,6 +6,7 @@ exports_files( "fix.proto", "issuetrack.proto", "process.proto", + "projectanalysis.proto", "record.proto", ], visibility = ["//tool/proto:__pkg__"], diff --git a/stovepipe/core/messagequeue/proto/projectanalysis.proto b/stovepipe/core/messagequeue/proto/projectanalysis.proto new file mode 100644 index 000000000..e09553c4c --- /dev/null +++ b/stovepipe/core/messagequeue/proto/projectanalysis.proto @@ -0,0 +1,35 @@ +// 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. + +syntax = "proto3"; + +package uber.stovepipe.messagequeue; + +import "api/base/messagequeue/proto/messagequeue.proto"; + +option go_package = "github.com/uber/submitqueue/stovepipe/core/messagequeue/protopb"; +option java_multiple_files = true; +option java_outer_classname = "ProjectAnalysisProto"; +option java_package = "com.uber.submitqueue.stovepipe.messagequeue"; + +// ProjectAnalysis requests project-status resolution and issue attribution. +// Partitioned by request ID. +message ProjectAnalysis { + option (uber.base.messagequeue.topic_keys) = "projectanalysis"; + + // id is the canonical positive decimal request ID within the queue. + string id = 1; + // queue_name identifies the configured validation queue. + string queue_name = 2; +} diff --git a/stovepipe/core/messagequeue/protopb/BUILD.bazel b/stovepipe/core/messagequeue/protopb/BUILD.bazel index 177796fe9..081624920 100644 --- a/stovepipe/core/messagequeue/protopb/BUILD.bazel +++ b/stovepipe/core/messagequeue/protopb/BUILD.bazel @@ -9,6 +9,7 @@ go_library( "fix.pb.go", "issuetrack.pb.go", "process.pb.go", + "projectanalysis.pb.go", "record.pb.go", ], importpath = "github.com/uber/submitqueue/stovepipe/core/messagequeue/protopb", diff --git a/stovepipe/core/messagequeue/protopb/projectanalysis.pb.go b/stovepipe/core/messagequeue/protopb/projectanalysis.pb.go new file mode 100644 index 000000000..4c06aba08 --- /dev/null +++ b/stovepipe/core/messagequeue/protopb/projectanalysis.pb.go @@ -0,0 +1,153 @@ +// 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. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v5.29.3 +// source: projectanalysis.proto + +package protopb + +import ( + reflect "reflect" + sync "sync" + unsafe "unsafe" + + _ "github.com/uber/submitqueue/api/base/messagequeue/protopb" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// ProjectAnalysis requests project-status resolution and issue attribution. +// Partitioned by request ID. +type ProjectAnalysis struct { + state protoimpl.MessageState `protogen:"open.v1"` + // id is the canonical positive decimal request ID within the queue. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // queue_name identifies the configured validation queue. + QueueName string `protobuf:"bytes,2,opt,name=queue_name,json=queueName,proto3" json:"queue_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProjectAnalysis) Reset() { + *x = ProjectAnalysis{} + mi := &file_projectanalysis_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProjectAnalysis) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProjectAnalysis) ProtoMessage() {} + +func (x *ProjectAnalysis) ProtoReflect() protoreflect.Message { + mi := &file_projectanalysis_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProjectAnalysis.ProtoReflect.Descriptor instead. +func (*ProjectAnalysis) Descriptor() ([]byte, []int) { + return file_projectanalysis_proto_rawDescGZIP(), []int{0} +} + +func (x *ProjectAnalysis) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ProjectAnalysis) GetQueueName() string { + if x != nil { + return x.QueueName + } + return "" +} + +var File_projectanalysis_proto protoreflect.FileDescriptor + +const file_projectanalysis_proto_rawDesc = "" + + "\n" + + "\x15projectanalysis.proto\x12\x1buber.stovepipe.messagequeue\x1a.api/base/messagequeue/proto/messagequeue.proto\"U\n" + + "\x0fProjectAnalysis\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x1d\n" + + "\n" + + "queue_name\x18\x02 \x01(\tR\tqueueName:\x13\x8a\xb5\x18\x0fprojectanalysisB\x86\x01\n" + + "+com.uber.submitqueue.stovepipe.messagequeueB\x14ProjectAnalysisProtoP\x01Z?github.com/uber/submitqueue/stovepipe/core/messagequeue/protopbb\x06proto3" + +var ( + file_projectanalysis_proto_rawDescOnce sync.Once + file_projectanalysis_proto_rawDescData []byte +) + +func file_projectanalysis_proto_rawDescGZIP() []byte { + file_projectanalysis_proto_rawDescOnce.Do(func() { + file_projectanalysis_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_projectanalysis_proto_rawDesc), len(file_projectanalysis_proto_rawDesc))) + }) + return file_projectanalysis_proto_rawDescData +} + +var file_projectanalysis_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_projectanalysis_proto_goTypes = []any{ + (*ProjectAnalysis)(nil), // 0: uber.stovepipe.messagequeue.ProjectAnalysis +} +var file_projectanalysis_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_projectanalysis_proto_init() } +func file_projectanalysis_proto_init() { + if File_projectanalysis_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_projectanalysis_proto_rawDesc), len(file_projectanalysis_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_projectanalysis_proto_goTypes, + DependencyIndexes: file_projectanalysis_proto_depIdxs, + MessageInfos: file_projectanalysis_proto_msgTypes, + }.Build() + File_projectanalysis_proto = out.File + file_projectanalysis_proto_goTypes = nil + file_projectanalysis_proto_depIdxs = nil +} diff --git a/stovepipe/core/messagequeue/topics.go b/stovepipe/core/messagequeue/topics.go index 5571f917d..2823b1c2e 100644 --- a/stovepipe/core/messagequeue/topics.go +++ b/stovepipe/core/messagequeue/topics.go @@ -51,6 +51,9 @@ const ( // TopicKeyFix carries build IDs for repair submission, partitioned by build ID. TopicKeyFix TopicKey = "fix" + // TopicKeyProjectAnalysis queues requests for project-status resolution and issue attribution. + TopicKeyProjectAnalysis TopicKey = "projectanalysis" + // TopicKeyRecord carries a request whose build reached a terminal status from // buildsignal to the record stage. The buildsignal controller publishes a // Record (the request id) here once, and only once, a build reaches a diff --git a/tool/proto/BUILD.bazel b/tool/proto/BUILD.bazel index 8c9b8a40b..0148bb056 100644 --- a/tool/proto/BUILD.bazel +++ b/tool/proto/BUILD.bazel @@ -108,6 +108,7 @@ go_proto_generated_files( "//stovepipe/core/messagequeue/proto:fix.proto", "//stovepipe/core/messagequeue/proto:issuetrack.proto", "//stovepipe/core/messagequeue/proto:process.proto", + "//stovepipe/core/messagequeue/proto:projectanalysis.proto", "//stovepipe/core/messagequeue/proto:record.proto", ], gen_services = False,