From 9cf403def167d08e170d28ff5c20ff39da77f2f1 Mon Sep 17 00:00:00 2001 From: "prath.shenoy" Date: Wed, 16 Sep 2026 18:56:58 +0000 Subject: [PATCH 1/3] doc(stovepipe): Add status lookup RFC **What**: - Define repository and project validation lookup semantics. - Specify staged rollout, consistency, completion, and pagination behavior. **Why**: - Establish a stable public contract before implementation begins. - Separate generic validation status behavior from repository-specific integrations. --- doc/rfc/index.md | 1 + .../get-project-status-by-uri-api.md | 87 +++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 doc/rfc/stovepipe/get-project-status-by-uri-api.md diff --git a/doc/rfc/index.md b/doc/rfc/index.md index 11dcb60d..1b25b63a 100644 --- a/doc/rfc/index.md +++ b/doc/rfc/index.md @@ -33,6 +33,7 @@ Design documents and technical proposals, grouped by scope. Shared/cross-cutting - [Record stage](stovepipe/steps/record.md) - Immutable validation facts keyed by `(queue, uri, project)`, monotonic last-green bookmark advancement and ref promotion, and the deferred hook-event and analyze handoffs - [Request Log](stovepipe/request-log.md) - Append-only request lifecycle log, durable source context, idempotent storage, and reliable write and repair paths - [Request History API](stovepipe/request-history-api.md) - Queue-scoped request-ID and URI lookup, public projection, materialization decision, ordering, and retention +- [GetProjectStatusByURI API](stovepipe/get-project-status-by-uri-api.md) - Queue-scoped current validation lookup for a commit, with repository and future project-level results ## Runway diff --git a/doc/rfc/stovepipe/get-project-status-by-uri-api.md b/doc/rfc/stovepipe/get-project-status-by-uri-api.md new file mode 100644 index 00000000..5901cfc9 --- /dev/null +++ b/doc/rfc/stovepipe/get-project-status-by-uri-api.md @@ -0,0 +1,87 @@ +# Stovepipe GetProjectStatusByURI API + +## Summary + +`GetProjectStatusByURI` exposes the current validation status for one exact commit URI in a queue. The response identifies the validation request, its baseline and lifecycle state, its whole-repository result when one has been recorded, and any project results the implementation exposes. + +A project is a consumer-defined deployable or consumable unit in the repository. The API does not define how projects are discovered, how validation work is selected, or how a project result is derived. Those are integration responsibilities outside this contract. + +The API is the durable source of truth for validation status. Lifecycle notifications are advisory: consumers can reconcile missed or duplicate notifications by querying this endpoint. + +## Contract + +The request and response fields have the following API-level meaning. The published protobuf is the authoritative field-level contract. + +| Request field | Required | Meaning | +| --- | --- | --- | +| `queue` | Yes | Identifies the queue in which to find the validation request. | +| `change_uri` | Yes | Identifies the exact commit URI under validation. | +| `projects` | Yes | Consumer-defined project IDs whose results are requested. At least one ID is required. | +| `page_size` | No | Limits one page of results for the requested projects. | +| `page_token` | No | Continues a previous page of results for the requested projects. | + +| Response field | Meaning | +| --- | --- | +| `request_id` | Identifies the resolved authoritative validation request. | +| `queue`, `change_uri`, `base_uri` | Return the validated scope and incremental-validation baseline. | +| `request_state` | Returns the request's public lifecycle state. | +| `updated_at_ms` | Records, in Unix milliseconds, the newest durable lifecycle or result record represented by the response. | +| `repository_breakage_degree` | Returns the whole-repository result when it is durably recorded. | +| `project_results_complete` | Indicates whether the implementation has durably finished producing results for the supplied project IDs. | +| `projects` | Returns recorded results for requested project IDs. | +| `next_page_token` | Continues requested-project result pagination when another page exists. | + +`request_state` is a stable public lifecycle vocabulary: `accepted`, `processing`, `succeeded`, `failed`, `cancelled`, or `superseded`. Clients must tolerate a future value. A terminal request state does not by itself mean that project results are complete. + +Repository and project breakage degrees are independent projections. A degree is on `[0.0, 1.0]`: `0.0` is green and any value above `0.0` is not green. An absent degree means no durable result exists; it must never be interpreted as green. The API does not derive one scope's degree from another scope's results. `updated_at_ms` is derived only from durable lifecycle and result records. + +| Response data | Durable source | +| --- | --- | +| Request identity, baseline, and lifecycle | Validation request | +| Repository breakage degree | Repository validation fact | +| Project results | Implementation-defined project-result records | +| Project completion and pagination | Project-result completion record and cursor | + +## Request Selection + +The lookup is queue-scoped. Stovepipe resolves `change_uri` through its request-URI mapping, loads the resulting request, and verifies that its queue and URI match the selector before reading validation facts. + +| Observed state | Result | +| --- | --- | +| No request-URI mapping | Not found | +| Mapping exists but the Request is not visible | Unavailable and retryable | +| Mapping and Request disagree | Internal consistency error | + +The initial request-URI mapping admits one request per `(queue, change_uri)`, so the endpoint returns one authoritative request. Revalidation support must introduce an explicit authoritative-request rule; it must not silently change this lookup's meaning. + +Queue, URI, request ID, and project ID comparisons are byte-exact and are limited to 255 bytes. An empty queue, URI, project list, or project ID is invalid. A request cannot contain duplicate project IDs. `page_size=0` selects the default of 50; the maximum is 200. Page tokens are opaque and bound to the selected request and requested project list. + +## Project Results and Pagination + +The caller supplies the project IDs for which it wants recorded results. The API does not define project discovery, validation selection, or attribution. A project absent from the response has no universal meaning and consumers must not infer that it is green. + +The response includes only results for the supplied project IDs, in the supplied order, and paginates that list. An implementation may record results for every requested project, only requested projects with a non-green result, or another documented subset. + +Each project result is an immutable validation fact keyed by queue, commit URI, and project ID. A result must identify the selected request. `project_results_complete` is false until the implementation has durably finished producing results for the supplied project IDs; a completed empty set is valid. + +Callers use `project_results_complete`, rather than the presence or absence of an individual project result, to determine whether the implementation has finished producing results for the requested projects. `next_page_token` is empty only on the final page. A terminal lifecycle notification is emitted only after the repository result and any applicable project completion record are durable. If notification delivery fails, consumers can recover the same result through this endpoint. + +## Rollout + +The initial implementation returns request lifecycle and whole-repository result only. It returns an empty `projects` list and `project_results_complete=false` because it does not yet produce project results. Its `updated_at_ms` value is returned only after a durable lifecycle record reflects the request state in the response. + +A later implementation can add durable project results, completion recording, and pagination using its documented result-set semantics. The public response shape remains unchanged. + +## Errors and Authorization + +- Invalid selectors, an empty project list, an invalid project ID, duplicate project IDs, or malformed page tokens are user errors. +- An unknown queue-scoped commit URI is not found. +- A request-URI mapping whose Request is not visible is retryable. +- A mapping and Request that disagree, a result belonging to another request, or a completion record with a missing project result is an internal consistency error. +- Authorization follows the queue policy applied to other Stovepipe reads. A commit URI or request ID does not bypass queue access control. + +## Testing + +The initial implementation must test request selection, queue isolation, the request visibility race, absent versus green repository facts, lifecycle projection, and invalid selectors. + +The project-result rollout additionally tests requested-project filtering, pagination and token binding, incomplete versus completed results, duplicate delivery, recovery after a partial write, and an unknown future lifecycle value. From 29e0808cbea46ba2164ccb5a2daf68154dac5c4c Mon Sep 17 00:00:00 2001 From: "prath.shenoy" Date: Wed, 16 Sep 2026 18:58:24 +0000 Subject: [PATCH 2/3] feat(stovepipe): Add status lookup contract **What**: - Publish a queue-scoped validation-status lookup contract. - Include repository status and future project-result response fields. **Why**: - Let consumers integrate against a stable validation-status interface. - Preserve a compatible path to project-level results. --- api/stovepipe/proto/stovepipe.proto | 48 +++ api/stovepipe/protopb/stovepipe.pb.go | 342 ++++++++++++++++++-- api/stovepipe/protopb/stovepipe.pb.yarpc.go | 142 +++++--- api/stovepipe/protopb/stovepipe_grpc.pb.go | 40 +++ 4 files changed, 515 insertions(+), 57 deletions(-) diff --git a/api/stovepipe/proto/stovepipe.proto b/api/stovepipe/proto/stovepipe.proto index 6da7ead7..951f45d2 100644 --- a/api/stovepipe/proto/stovepipe.proto +++ b/api/stovepipe/proto/stovepipe.proto @@ -107,6 +107,52 @@ message GetRequestHistoryByURIResponse { repeated RequestHistory histories = 1; } +// GetProjectStatusByURIRequest selects the validation status for an exact commit URI. +message GetProjectStatusByURIRequest { + // Logical queue containing the validation request. + string queue = 1; + // Exact VCS-agnostic URI of the commit under validation. + string change_uri = 2; + // Required consumer-defined project IDs whose recorded results are requested. + repeated string projects = 3; + // Maximum projects to return when projects is empty. Zero selects the server default. + int32 page_size = 4; + // Opaque continuation token for full project-result pagination when projects is empty. + string page_token = 5; +} + +// ProjectValidation contains one reported project and its recorded result, if any. +message ProjectValidation { + // Stable consumer-defined project identifier. + string project = 1; + // Breakage degree on [0.0, 1.0]. Zero is green; absence means no result is recorded yet. + optional double breakage_degree = 2; +} + +// GetProjectStatusByURIResponse contains the selected validation's current projection. +message GetProjectStatusByURIResponse { + // Globally unique identifier of the validation request. + string request_id = 1; + // Logical queue containing the validation request. + string queue = 2; + // VCS-agnostic URI of the commit under validation. + string change_uri = 3; + // Baseline URI for incremental validation. Empty for a full build. + string base_uri = 4; + // Stable public lifecycle state of the validation request. + string request_state = 5; + // Unix millisecond timestamp of the newest durable lifecycle or result record. + int64 updated_at_ms = 10; + // Whole-repository breakage degree. Absent until the result is durable. + optional double repository_breakage_degree = 6; + // True after the implementation has durably finished producing relevant project results. + bool project_results_complete = 7; + // Recorded results for requested projects, or one full-result page when none are requested. + repeated ProjectValidation projects = 8; + // Opaque continuation token for another full-result page. Empty on the final page. + string next_page_token = 9; +} + // Stovepipe provides the Stovepipe API. service Stovepipe { // Ping returns a response indicating the service is alive @@ -118,4 +164,6 @@ service Stovepipe { rpc GetRequestHistoryByID(GetRequestHistoryByIDRequest) returns (GetRequestHistoryByIDResponse) {} // GetRequestHistoryByURI returns retained histories for an exact commit URI. rpc GetRequestHistoryByURI(GetRequestHistoryByURIRequest) returns (GetRequestHistoryByURIResponse) {} + // GetProjectStatusByURI returns the current validation status for an exact commit URI. + rpc GetProjectStatusByURI(GetProjectStatusByURIRequest) returns (GetProjectStatusByURIResponse) {} } diff --git a/api/stovepipe/protopb/stovepipe.pb.go b/api/stovepipe/protopb/stovepipe.pb.go index c7556c88..d6725600 100644 --- a/api/stovepipe/protopb/stovepipe.pb.go +++ b/api/stovepipe/protopb/stovepipe.pb.go @@ -14,7 +14,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 +// protoc-gen-go v1.36.11 // protoc v5.29.3 // source: stovepipe.proto @@ -621,6 +621,270 @@ func (x *GetRequestHistoryByURIResponse) GetHistories() []*RequestHistory { return nil } +// GetProjectStatusByURIRequest selects the validation status for an exact commit URI. +type GetProjectStatusByURIRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Logical queue containing the validation request. + Queue string `protobuf:"bytes,1,opt,name=queue,proto3" json:"queue,omitempty"` + // Exact VCS-agnostic URI of the commit under validation. + ChangeUri string `protobuf:"bytes,2,opt,name=change_uri,json=changeUri,proto3" json:"change_uri,omitempty"` + // Required consumer-defined project IDs whose recorded results are requested. + Projects []string `protobuf:"bytes,3,rep,name=projects,proto3" json:"projects,omitempty"` + // Maximum projects to return when projects is empty. Zero selects the server default. + PageSize int32 `protobuf:"varint,4,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Opaque continuation token for full project-result pagination when projects is empty. + PageToken string `protobuf:"bytes,5,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetProjectStatusByURIRequest) Reset() { + *x = GetProjectStatusByURIRequest{} + mi := &file_stovepipe_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetProjectStatusByURIRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetProjectStatusByURIRequest) ProtoMessage() {} + +func (x *GetProjectStatusByURIRequest) ProtoReflect() protoreflect.Message { + mi := &file_stovepipe_proto_msgTypes[10] + 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 GetProjectStatusByURIRequest.ProtoReflect.Descriptor instead. +func (*GetProjectStatusByURIRequest) Descriptor() ([]byte, []int) { + return file_stovepipe_proto_rawDescGZIP(), []int{10} +} + +func (x *GetProjectStatusByURIRequest) GetQueue() string { + if x != nil { + return x.Queue + } + return "" +} + +func (x *GetProjectStatusByURIRequest) GetChangeUri() string { + if x != nil { + return x.ChangeUri + } + return "" +} + +func (x *GetProjectStatusByURIRequest) GetProjects() []string { + if x != nil { + return x.Projects + } + return nil +} + +func (x *GetProjectStatusByURIRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *GetProjectStatusByURIRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +// ProjectValidation contains one reported project and its recorded result, if any. +type ProjectValidation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Stable consumer-defined project identifier. + Project string `protobuf:"bytes,1,opt,name=project,proto3" json:"project,omitempty"` + // Breakage degree on [0.0, 1.0]. Zero is green; absence means no result is recorded yet. + BreakageDegree *float64 `protobuf:"fixed64,2,opt,name=breakage_degree,json=breakageDegree,proto3,oneof" json:"breakage_degree,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProjectValidation) Reset() { + *x = ProjectValidation{} + mi := &file_stovepipe_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProjectValidation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProjectValidation) ProtoMessage() {} + +func (x *ProjectValidation) ProtoReflect() protoreflect.Message { + mi := &file_stovepipe_proto_msgTypes[11] + 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 ProjectValidation.ProtoReflect.Descriptor instead. +func (*ProjectValidation) Descriptor() ([]byte, []int) { + return file_stovepipe_proto_rawDescGZIP(), []int{11} +} + +func (x *ProjectValidation) GetProject() string { + if x != nil { + return x.Project + } + return "" +} + +func (x *ProjectValidation) GetBreakageDegree() float64 { + if x != nil && x.BreakageDegree != nil { + return *x.BreakageDegree + } + return 0 +} + +// GetProjectStatusByURIResponse contains the selected validation's current projection. +type GetProjectStatusByURIResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Globally unique identifier of the validation request. + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + // Logical queue containing the validation request. + Queue string `protobuf:"bytes,2,opt,name=queue,proto3" json:"queue,omitempty"` + // VCS-agnostic URI of the commit under validation. + ChangeUri string `protobuf:"bytes,3,opt,name=change_uri,json=changeUri,proto3" json:"change_uri,omitempty"` + // Baseline URI for incremental validation. Empty for a full build. + BaseUri string `protobuf:"bytes,4,opt,name=base_uri,json=baseUri,proto3" json:"base_uri,omitempty"` + // Stable public lifecycle state of the validation request. + RequestState string `protobuf:"bytes,5,opt,name=request_state,json=requestState,proto3" json:"request_state,omitempty"` + // Unix millisecond timestamp of the newest durable lifecycle or result record. + UpdatedAtMs int64 `protobuf:"varint,10,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"` + // Whole-repository breakage degree. Absent until the result is durable. + RepositoryBreakageDegree *float64 `protobuf:"fixed64,6,opt,name=repository_breakage_degree,json=repositoryBreakageDegree,proto3,oneof" json:"repository_breakage_degree,omitempty"` + // True after the implementation has durably finished producing relevant project results. + ProjectResultsComplete bool `protobuf:"varint,7,opt,name=project_results_complete,json=projectResultsComplete,proto3" json:"project_results_complete,omitempty"` + // Recorded results for requested projects, or one full-result page when none are requested. + Projects []*ProjectValidation `protobuf:"bytes,8,rep,name=projects,proto3" json:"projects,omitempty"` + // Opaque continuation token for another full-result page. Empty on the final page. + NextPageToken string `protobuf:"bytes,9,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetProjectStatusByURIResponse) Reset() { + *x = GetProjectStatusByURIResponse{} + mi := &file_stovepipe_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetProjectStatusByURIResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetProjectStatusByURIResponse) ProtoMessage() {} + +func (x *GetProjectStatusByURIResponse) ProtoReflect() protoreflect.Message { + mi := &file_stovepipe_proto_msgTypes[12] + 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 GetProjectStatusByURIResponse.ProtoReflect.Descriptor instead. +func (*GetProjectStatusByURIResponse) Descriptor() ([]byte, []int) { + return file_stovepipe_proto_rawDescGZIP(), []int{12} +} + +func (x *GetProjectStatusByURIResponse) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *GetProjectStatusByURIResponse) GetQueue() string { + if x != nil { + return x.Queue + } + return "" +} + +func (x *GetProjectStatusByURIResponse) GetChangeUri() string { + if x != nil { + return x.ChangeUri + } + return "" +} + +func (x *GetProjectStatusByURIResponse) GetBaseUri() string { + if x != nil { + return x.BaseUri + } + return "" +} + +func (x *GetProjectStatusByURIResponse) GetRequestState() string { + if x != nil { + return x.RequestState + } + return "" +} + +func (x *GetProjectStatusByURIResponse) GetUpdatedAtMs() int64 { + if x != nil { + return x.UpdatedAtMs + } + return 0 +} + +func (x *GetProjectStatusByURIResponse) GetRepositoryBreakageDegree() float64 { + if x != nil && x.RepositoryBreakageDegree != nil { + return *x.RepositoryBreakageDegree + } + return 0 +} + +func (x *GetProjectStatusByURIResponse) GetProjectResultsComplete() bool { + if x != nil { + return x.ProjectResultsComplete + } + return false +} + +func (x *GetProjectStatusByURIResponse) GetProjects() []*ProjectValidation { + if x != nil { + return x.Projects + } + return nil +} + +func (x *GetProjectStatusByURIResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + var File_stovepipe_proto protoreflect.FileDescriptor const file_stovepipe_proto_rawDesc = "" + @@ -659,12 +923,40 @@ const file_stovepipe_proto_rawDesc = "" + "\x1dGetRequestHistoryByIDResponse\x12@\n" + "\x06events\x18\x01 \x03(\v2(.uber.submitqueue.stovepipe.HistoryEventR\x06events\"j\n" + "\x1eGetRequestHistoryByURIResponse\x12H\n" + - "\thistories\x18\x01 \x03(\v2*.uber.submitqueue.stovepipe.RequestHistoryR\thistories2\xf0\x03\n" + + "\thistories\x18\x01 \x03(\v2*.uber.submitqueue.stovepipe.RequestHistoryR\thistories\"\xab\x01\n" + + "\x1cGetProjectStatusByURIRequest\x12\x14\n" + + "\x05queue\x18\x01 \x01(\tR\x05queue\x12\x1d\n" + + "\n" + + "change_uri\x18\x02 \x01(\tR\tchangeUri\x12\x1a\n" + + "\bprojects\x18\x03 \x03(\tR\bprojects\x12\x1b\n" + + "\tpage_size\x18\x04 \x01(\x05R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x05 \x01(\tR\tpageToken\"o\n" + + "\x11ProjectValidation\x12\x18\n" + + "\aproject\x18\x01 \x01(\tR\aproject\x12,\n" + + "\x0fbreakage_degree\x18\x02 \x01(\x01H\x00R\x0ebreakageDegree\x88\x01\x01B\x12\n" + + "\x10_breakage_degree\"\xe6\x03\n" + + "\x1dGetProjectStatusByURIResponse\x12\x1d\n" + + "\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12\x14\n" + + "\x05queue\x18\x02 \x01(\tR\x05queue\x12\x1d\n" + + "\n" + + "change_uri\x18\x03 \x01(\tR\tchangeUri\x12\x19\n" + + "\bbase_uri\x18\x04 \x01(\tR\abaseUri\x12#\n" + + "\rrequest_state\x18\x05 \x01(\tR\frequestState\x12\"\n" + + "\rupdated_at_ms\x18\n" + + " \x01(\x03R\vupdatedAtMs\x12A\n" + + "\x1arepository_breakage_degree\x18\x06 \x01(\x01H\x00R\x18repositoryBreakageDegree\x88\x01\x01\x128\n" + + "\x18project_results_complete\x18\a \x01(\bR\x16projectResultsComplete\x12I\n" + + "\bprojects\x18\b \x03(\v2-.uber.submitqueue.stovepipe.ProjectValidationR\bprojects\x12&\n" + + "\x0fnext_page_token\x18\t \x01(\tR\rnextPageTokenB\x1d\n" + + "\x1b_repository_breakage_degree2\x81\x05\n" + "\tStovepipe\x12[\n" + "\x04Ping\x12'.uber.submitqueue.stovepipe.PingRequest\x1a(.uber.submitqueue.stovepipe.PingResponse\"\x00\x12a\n" + "\x06Ingest\x12).uber.submitqueue.stovepipe.IngestRequest\x1a*.uber.submitqueue.stovepipe.IngestResponse\"\x00\x12\x8e\x01\n" + "\x15GetRequestHistoryByID\x128.uber.submitqueue.stovepipe.GetRequestHistoryByIDRequest\x1a9.uber.submitqueue.stovepipe.GetRequestHistoryByIDResponse\"\x00\x12\x91\x01\n" + - "\x16GetRequestHistoryByURI\x129.uber.submitqueue.stovepipe.GetRequestHistoryByURIRequest\x1a:.uber.submitqueue.stovepipe.GetRequestHistoryByURIResponse\"\x00Be\n" + + "\x16GetRequestHistoryByURI\x129.uber.submitqueue.stovepipe.GetRequestHistoryByURIRequest\x1a:.uber.submitqueue.stovepipe.GetRequestHistoryByURIResponse\"\x00\x12\x8e\x01\n" + + "\x15GetProjectStatusByURI\x128.uber.submitqueue.stovepipe.GetProjectStatusByURIRequest\x1a9.uber.submitqueue.stovepipe.GetProjectStatusByURIResponse\"\x00Be\n" + "\x1ecom.uber.submitqueue.stovepipeB\x0eStovepipeProtoP\x01Z1github.com/uber/submitqueue/api/stovepipe/protopbb\x06proto3" var ( @@ -679,7 +971,7 @@ func file_stovepipe_proto_rawDescGZIP() []byte { return file_stovepipe_proto_rawDescData } -var file_stovepipe_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_stovepipe_proto_msgTypes = make([]protoimpl.MessageInfo, 13) var file_stovepipe_proto_goTypes = []any{ (*PingRequest)(nil), // 0: uber.submitqueue.stovepipe.PingRequest (*PingResponse)(nil), // 1: uber.submitqueue.stovepipe.PingResponse @@ -691,24 +983,30 @@ var file_stovepipe_proto_goTypes = []any{ (*RequestHistory)(nil), // 7: uber.submitqueue.stovepipe.RequestHistory (*GetRequestHistoryByIDResponse)(nil), // 8: uber.submitqueue.stovepipe.GetRequestHistoryByIDResponse (*GetRequestHistoryByURIResponse)(nil), // 9: uber.submitqueue.stovepipe.GetRequestHistoryByURIResponse + (*GetProjectStatusByURIRequest)(nil), // 10: uber.submitqueue.stovepipe.GetProjectStatusByURIRequest + (*ProjectValidation)(nil), // 11: uber.submitqueue.stovepipe.ProjectValidation + (*GetProjectStatusByURIResponse)(nil), // 12: uber.submitqueue.stovepipe.GetProjectStatusByURIResponse } var file_stovepipe_proto_depIdxs = []int32{ - 6, // 0: uber.submitqueue.stovepipe.RequestHistory.events:type_name -> uber.submitqueue.stovepipe.HistoryEvent - 6, // 1: uber.submitqueue.stovepipe.GetRequestHistoryByIDResponse.events:type_name -> uber.submitqueue.stovepipe.HistoryEvent - 7, // 2: uber.submitqueue.stovepipe.GetRequestHistoryByURIResponse.histories:type_name -> uber.submitqueue.stovepipe.RequestHistory - 0, // 3: uber.submitqueue.stovepipe.Stovepipe.Ping:input_type -> uber.submitqueue.stovepipe.PingRequest - 2, // 4: uber.submitqueue.stovepipe.Stovepipe.Ingest:input_type -> uber.submitqueue.stovepipe.IngestRequest - 4, // 5: uber.submitqueue.stovepipe.Stovepipe.GetRequestHistoryByID:input_type -> uber.submitqueue.stovepipe.GetRequestHistoryByIDRequest - 5, // 6: uber.submitqueue.stovepipe.Stovepipe.GetRequestHistoryByURI:input_type -> uber.submitqueue.stovepipe.GetRequestHistoryByURIRequest - 1, // 7: uber.submitqueue.stovepipe.Stovepipe.Ping:output_type -> uber.submitqueue.stovepipe.PingResponse - 3, // 8: uber.submitqueue.stovepipe.Stovepipe.Ingest:output_type -> uber.submitqueue.stovepipe.IngestResponse - 8, // 9: uber.submitqueue.stovepipe.Stovepipe.GetRequestHistoryByID:output_type -> uber.submitqueue.stovepipe.GetRequestHistoryByIDResponse - 9, // 10: uber.submitqueue.stovepipe.Stovepipe.GetRequestHistoryByURI:output_type -> uber.submitqueue.stovepipe.GetRequestHistoryByURIResponse - 7, // [7:11] is the sub-list for method output_type - 3, // [3:7] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 6, // 0: uber.submitqueue.stovepipe.RequestHistory.events:type_name -> uber.submitqueue.stovepipe.HistoryEvent + 6, // 1: uber.submitqueue.stovepipe.GetRequestHistoryByIDResponse.events:type_name -> uber.submitqueue.stovepipe.HistoryEvent + 7, // 2: uber.submitqueue.stovepipe.GetRequestHistoryByURIResponse.histories:type_name -> uber.submitqueue.stovepipe.RequestHistory + 11, // 3: uber.submitqueue.stovepipe.GetProjectStatusByURIResponse.projects:type_name -> uber.submitqueue.stovepipe.ProjectValidation + 0, // 4: uber.submitqueue.stovepipe.Stovepipe.Ping:input_type -> uber.submitqueue.stovepipe.PingRequest + 2, // 5: uber.submitqueue.stovepipe.Stovepipe.Ingest:input_type -> uber.submitqueue.stovepipe.IngestRequest + 4, // 6: uber.submitqueue.stovepipe.Stovepipe.GetRequestHistoryByID:input_type -> uber.submitqueue.stovepipe.GetRequestHistoryByIDRequest + 5, // 7: uber.submitqueue.stovepipe.Stovepipe.GetRequestHistoryByURI:input_type -> uber.submitqueue.stovepipe.GetRequestHistoryByURIRequest + 10, // 8: uber.submitqueue.stovepipe.Stovepipe.GetProjectStatusByURI:input_type -> uber.submitqueue.stovepipe.GetProjectStatusByURIRequest + 1, // 9: uber.submitqueue.stovepipe.Stovepipe.Ping:output_type -> uber.submitqueue.stovepipe.PingResponse + 3, // 10: uber.submitqueue.stovepipe.Stovepipe.Ingest:output_type -> uber.submitqueue.stovepipe.IngestResponse + 8, // 11: uber.submitqueue.stovepipe.Stovepipe.GetRequestHistoryByID:output_type -> uber.submitqueue.stovepipe.GetRequestHistoryByIDResponse + 9, // 12: uber.submitqueue.stovepipe.Stovepipe.GetRequestHistoryByURI:output_type -> uber.submitqueue.stovepipe.GetRequestHistoryByURIResponse + 12, // 13: uber.submitqueue.stovepipe.Stovepipe.GetProjectStatusByURI:output_type -> uber.submitqueue.stovepipe.GetProjectStatusByURIResponse + 9, // [9:14] is the sub-list for method output_type + 4, // [4:9] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name } func init() { file_stovepipe_proto_init() } @@ -720,13 +1018,15 @@ func file_stovepipe_proto_init() { (*HistoryEvent_RequestState)(nil), (*HistoryEvent_Event)(nil), } + file_stovepipe_proto_msgTypes[11].OneofWrappers = []any{} + file_stovepipe_proto_msgTypes[12].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_stovepipe_proto_rawDesc), len(file_stovepipe_proto_rawDesc)), NumEnums: 0, - NumMessages: 10, + NumMessages: 13, NumExtensions: 0, NumServices: 1, }, diff --git a/api/stovepipe/protopb/stovepipe.pb.yarpc.go b/api/stovepipe/protopb/stovepipe.pb.yarpc.go index 6fc49d53..351649b4 100644 --- a/api/stovepipe/protopb/stovepipe.pb.yarpc.go +++ b/api/stovepipe/protopb/stovepipe.pb.yarpc.go @@ -25,6 +25,7 @@ type StovepipeYARPCClient interface { Ingest(context.Context, *IngestRequest, ...yarpc.CallOption) (*IngestResponse, error) GetRequestHistoryByID(context.Context, *GetRequestHistoryByIDRequest, ...yarpc.CallOption) (*GetRequestHistoryByIDResponse, error) GetRequestHistoryByURI(context.Context, *GetRequestHistoryByURIRequest, ...yarpc.CallOption) (*GetRequestHistoryByURIResponse, error) + GetProjectStatusByURI(context.Context, *GetProjectStatusByURIRequest, ...yarpc.CallOption) (*GetProjectStatusByURIResponse, error) } func newStovepipeYARPCClient(clientConfig transport.ClientConfig, anyResolver v2.AnyResolver, options ...v2.ClientOption) StovepipeYARPCClient { @@ -49,6 +50,7 @@ type StovepipeYARPCServer interface { Ingest(context.Context, *IngestRequest) (*IngestResponse, error) GetRequestHistoryByID(context.Context, *GetRequestHistoryByIDRequest) (*GetRequestHistoryByIDResponse, error) GetRequestHistoryByURI(context.Context, *GetRequestHistoryByURIRequest) (*GetRequestHistoryByURIResponse, error) + GetProjectStatusByURI(context.Context, *GetProjectStatusByURIRequest) (*GetProjectStatusByURIResponse, error) } type buildStovepipeYARPCProceduresParams struct { @@ -102,6 +104,16 @@ func buildStovepipeYARPCProcedures(params buildStovepipeYARPCProceduresParams) [ }, ), }, + { + MethodName: "GetProjectStatusByURI", + Handler: v2.NewUnaryHandler( + v2.UnaryHandlerParams{ + Handle: handler.GetProjectStatusByURI, + NewRequest: newStovepipeServiceGetProjectStatusByURIYARPCRequest, + AnyResolver: params.AnyResolver, + }, + ), + }, }, OnewayHandlerParams: []v2.BuildProceduresOnewayHandlerParams{}, StreamHandlerParams: []v2.BuildProceduresStreamHandlerParams{}, @@ -262,6 +274,18 @@ func (c *_StovepipeYARPCCaller) GetRequestHistoryByURI(ctx context.Context, requ return response, err } +func (c *_StovepipeYARPCCaller) GetProjectStatusByURI(ctx context.Context, request *GetProjectStatusByURIRequest, options ...yarpc.CallOption) (*GetProjectStatusByURIResponse, error) { + responseMessage, err := c.streamClient.Call(ctx, "GetProjectStatusByURI", request, newStovepipeServiceGetProjectStatusByURIYARPCResponse, options...) + if responseMessage == nil { + return nil, err + } + response, ok := responseMessage.(*GetProjectStatusByURIResponse) + if !ok { + return nil, v2.CastError(emptyStovepipeServiceGetProjectStatusByURIYARPCResponse, responseMessage) + } + return response, err +} + type _StovepipeYARPCHandler struct { server StovepipeYARPCServer } @@ -330,6 +354,22 @@ func (h *_StovepipeYARPCHandler) GetRequestHistoryByURI(ctx context.Context, req return response, err } +func (h *_StovepipeYARPCHandler) GetProjectStatusByURI(ctx context.Context, requestMessage proto.Message) (proto.Message, error) { + var request *GetProjectStatusByURIRequest + var ok bool + if requestMessage != nil { + request, ok = requestMessage.(*GetProjectStatusByURIRequest) + if !ok { + return nil, v2.CastError(emptyStovepipeServiceGetProjectStatusByURIYARPCRequest, requestMessage) + } + } + response, err := h.server.GetProjectStatusByURI(ctx, request) + if response == nil { + return nil, err + } + return response, err +} + func newStovepipeServicePingYARPCRequest() proto.Message { return &PingRequest{} } @@ -362,6 +402,14 @@ func newStovepipeServiceGetRequestHistoryByURIYARPCResponse() proto.Message { return &GetRequestHistoryByURIResponse{} } +func newStovepipeServiceGetProjectStatusByURIYARPCRequest() proto.Message { + return &GetProjectStatusByURIRequest{} +} + +func newStovepipeServiceGetProjectStatusByURIYARPCResponse() proto.Message { + return &GetProjectStatusByURIResponse{} +} + var ( emptyStovepipeServicePingYARPCRequest = &PingRequest{} emptyStovepipeServicePingYARPCResponse = &PingResponse{} @@ -371,47 +419,69 @@ var ( emptyStovepipeServiceGetRequestHistoryByIDYARPCResponse = &GetRequestHistoryByIDResponse{} emptyStovepipeServiceGetRequestHistoryByURIYARPCRequest = &GetRequestHistoryByURIRequest{} emptyStovepipeServiceGetRequestHistoryByURIYARPCResponse = &GetRequestHistoryByURIResponse{} + emptyStovepipeServiceGetProjectStatusByURIYARPCRequest = &GetProjectStatusByURIRequest{} + emptyStovepipeServiceGetProjectStatusByURIYARPCResponse = &GetProjectStatusByURIResponse{} ) var yarpcFileDescriptorClosurefabdb6b3c0b09022 = [][]byte{ // stovepipe.proto []byte{ - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x55, 0x5d, 0x8f, 0xd2, 0x40, - 0x14, 0xa5, 0x74, 0x61, 0xb7, 0x97, 0x0f, 0xcd, 0x44, 0x37, 0xd8, 0xec, 0x6e, 0xb0, 0x09, 0x59, - 0xdc, 0x87, 0x12, 0xd7, 0x17, 0xf5, 0xc9, 0x10, 0xcd, 0xc2, 0x83, 0x86, 0x94, 0xf8, 0xa2, 0x0f, - 0xa4, 0xb4, 0x37, 0x30, 0x26, 0xed, 0x94, 0xce, 0x94, 0x64, 0x7f, 0x80, 0xaf, 0x26, 0xfe, 0x23, - 0x7f, 0x92, 0x3f, 0xc1, 0x74, 0x3a, 0x2d, 0xb0, 0x81, 0xaa, 0xbc, 0xcd, 0x1c, 0xce, 0x39, 0xf7, - 0xce, 0x99, 0xb9, 0x14, 0x1e, 0x71, 0xc1, 0xd6, 0x18, 0xd1, 0x08, 0xed, 0x28, 0x66, 0x82, 0x11, - 0x33, 0x99, 0x63, 0x6c, 0xf3, 0x64, 0x1e, 0x50, 0xb1, 0x4a, 0x30, 0x41, 0xbb, 0x60, 0x58, 0xd7, - 0xd0, 0x98, 0xd0, 0x70, 0xe1, 0xe0, 0x2a, 0x41, 0x2e, 0x48, 0x07, 0x4e, 0x03, 0xe4, 0xdc, 0x5d, - 0x60, 0x47, 0xeb, 0x6a, 0x7d, 0xc3, 0xc9, 0xb7, 0xd6, 0x77, 0x0d, 0x9a, 0x19, 0x93, 0x47, 0x2c, - 0xe4, 0x78, 0x98, 0x4a, 0x9e, 0x43, 0x93, 0x63, 0xbc, 0xa6, 0x1e, 0xce, 0x42, 0x37, 0xc0, 0x4e, - 0x55, 0xfe, 0xdc, 0x50, 0xd8, 0x27, 0x37, 0x40, 0x72, 0x01, 0x86, 0xa0, 0x01, 0x72, 0xe1, 0x06, - 0x51, 0x47, 0xef, 0x6a, 0x7d, 0xdd, 0xd9, 0x00, 0xc4, 0x84, 0xb3, 0x25, 0xe3, 0x42, 0x8a, 0x4f, - 0xa4, 0xb8, 0xd8, 0x5b, 0x3d, 0x68, 0x8d, 0xc3, 0x05, 0x72, 0x91, 0xb7, 0xfc, 0x04, 0x6a, 0xf2, - 0x50, 0xaa, 0x8b, 0x6c, 0x63, 0x75, 0xa1, 0x9d, 0xd3, 0x54, 0xbf, 0x6d, 0xa8, 0x52, 0x5f, 0x91, - 0xaa, 0xd4, 0xb7, 0xa6, 0x70, 0x71, 0x87, 0xb9, 0xcb, 0x88, 0x72, 0xc1, 0xe2, 0xfb, 0xe1, 0xfd, - 0xf8, 0x7d, 0xa9, 0x2f, 0xb9, 0x04, 0x88, 0x33, 0xc2, 0x8c, 0xfa, 0xea, 0x64, 0x86, 0x42, 0xc6, - 0xbe, 0x75, 0x07, 0x97, 0x7b, 0x4c, 0x3f, 0x3b, 0xe3, 0x72, 0xd7, 0xc7, 0xa0, 0x27, 0x31, 0x55, - 0x76, 0xe9, 0xd2, 0xfa, 0xa5, 0x41, 0x53, 0xe9, 0x3f, 0xac, 0x31, 0x14, 0xe4, 0x19, 0x9c, 0x61, - 0xba, 0x98, 0x15, 0x87, 0x38, 0x95, 0xfb, 0xb1, 0x9f, 0xe6, 0x5d, 0x64, 0x37, 0x0b, 0xb8, 0xb4, - 0xd1, 0x9d, 0x46, 0x81, 0x7d, 0xe4, 0xa4, 0x07, 0xad, 0xbc, 0x6d, 0x2e, 0x5c, 0x81, 0x32, 0x73, - 0x63, 0x54, 0x71, 0x9a, 0x0a, 0x9e, 0xa6, 0x28, 0x39, 0x87, 0x9a, 0x34, 0xcd, 0x52, 0x1f, 0x55, - 0x9c, 0x6c, 0x4b, 0x7a, 0xd0, 0x66, 0x89, 0xf0, 0x58, 0x80, 0xb3, 0x18, 0x5d, 0xce, 0xc2, 0x4e, - 0x4d, 0xb6, 0xd0, 0x52, 0xa8, 0x23, 0xc1, 0x61, 0x13, 0x80, 0x79, 0x5e, 0x12, 0xc7, 0x18, 0x7a, - 0x68, 0xad, 0xa0, 0xbd, 0x1b, 0xc4, 0x83, 0xf0, 0xb4, 0x07, 0xe1, 0x91, 0x77, 0x50, 0x97, 0xe5, - 0xd2, 0x13, 0xe8, 0xfd, 0xc6, 0x6d, 0xdf, 0x3e, 0xfc, 0x70, 0xed, 0xed, 0x70, 0x1c, 0xa5, 0xb3, - 0xdc, 0xbd, 0xf1, 0xa7, 0x77, 0xaa, 0x1e, 0xc1, 0xa6, 0x84, 0x76, 0x64, 0x89, 0x6f, 0x70, 0x75, - 0xe8, 0x86, 0x55, 0x8d, 0x11, 0x18, 0x4b, 0x89, 0x53, 0xcc, 0xcb, 0xdc, 0x94, 0x95, 0xd9, 0xf5, - 0x72, 0x36, 0xe2, 0xdb, 0xdf, 0x3a, 0x18, 0xd3, 0x9c, 0x47, 0xbe, 0xc2, 0x49, 0x3a, 0x80, 0xe4, - 0xba, 0xcc, 0x6c, 0x6b, 0x98, 0xcd, 0xfe, 0xdf, 0x89, 0x59, 0xcb, 0x56, 0x85, 0xb8, 0x50, 0xcf, - 0xe6, 0x85, 0xbc, 0x28, 0x53, 0xed, 0x8c, 0x9e, 0x79, 0xf3, 0x2f, 0xd4, 0xa2, 0xc4, 0x0f, 0x0d, - 0x9e, 0xee, 0xbd, 0x1d, 0xf2, 0xba, 0xcc, 0xa7, 0x6c, 0x48, 0xcd, 0x37, 0x47, 0x28, 0x8b, 0x86, - 0x7e, 0x6a, 0x70, 0xbe, 0xff, 0x2e, 0xc9, 0xff, 0xfa, 0x6e, 0x26, 0xdc, 0x7c, 0x7b, 0x8c, 0x34, - 0xef, 0x69, 0x88, 0x70, 0xe5, 0xb1, 0xa0, 0xc4, 0x62, 0xd8, 0x2e, 0x5e, 0xc4, 0x24, 0xfd, 0x77, - 0x9f, 0x68, 0x5f, 0x5e, 0x2e, 0xa8, 0x58, 0x26, 0x73, 0xdb, 0x63, 0xc1, 0x20, 0x15, 0x0e, 0xb6, - 0x84, 0x03, 0x37, 0xa2, 0x83, 0x42, 0x3c, 0x90, 0x1f, 0x84, 0x68, 0x3e, 0xaf, 0xcb, 0xc5, 0xab, - 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x29, 0x24, 0x96, 0x5e, 0x2c, 0x06, 0x00, 0x00, + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x9c, 0x56, 0x4d, 0x73, 0xdb, 0x44, + 0x18, 0xb6, 0xe2, 0x3a, 0xb6, 0xde, 0x38, 0x4e, 0xd9, 0x81, 0x8c, 0xea, 0xd6, 0x9d, 0x20, 0x26, + 0xd4, 0x74, 0xc0, 0x1e, 0xca, 0xa5, 0x70, 0xa2, 0xa6, 0x4c, 0xed, 0x43, 0x19, 0x8f, 0x42, 0x39, + 0xc0, 0x41, 0xb3, 0x96, 0xde, 0x71, 0xb6, 0x8d, 0xb4, 0x8a, 0x76, 0x95, 0xa1, 0xbd, 0x71, 0x60, + 0xa6, 0x27, 0x66, 0xf8, 0x3b, 0x9c, 0xf8, 0x35, 0xfc, 0x0e, 0x66, 0x3f, 0x24, 0x27, 0xae, 0xad, + 0xb4, 0xb9, 0x69, 0x9f, 0xf7, 0x73, 0x9f, 0x7d, 0xf6, 0x5d, 0xc1, 0x81, 0x90, 0xfc, 0x02, 0x33, + 0x96, 0xe1, 0x28, 0xcb, 0xb9, 0xe4, 0xa4, 0x5f, 0x2c, 0x30, 0x1f, 0x89, 0x62, 0x91, 0x30, 0x79, + 0x5e, 0x60, 0x81, 0xa3, 0xca, 0xc3, 0x7f, 0x00, 0x7b, 0x73, 0x96, 0x2e, 0x03, 0x3c, 0x2f, 0x50, + 0x48, 0xe2, 0x41, 0x3b, 0x41, 0x21, 0xe8, 0x12, 0x3d, 0xe7, 0xc8, 0x19, 0xba, 0x41, 0xb9, 0xf4, + 0xff, 0x74, 0xa0, 0x6b, 0x3c, 0x45, 0xc6, 0x53, 0x81, 0xdb, 0x5d, 0xc9, 0xa7, 0xd0, 0x15, 0x98, + 0x5f, 0xb0, 0x08, 0xc3, 0x94, 0x26, 0xe8, 0xed, 0x68, 0xf3, 0x9e, 0xc5, 0x7e, 0xa2, 0x09, 0x92, + 0x7b, 0xe0, 0x4a, 0x96, 0xa0, 0x90, 0x34, 0xc9, 0xbc, 0xe6, 0x91, 0x33, 0x6c, 0x06, 0x2b, 0x80, + 0xf4, 0xa1, 0x73, 0xca, 0x85, 0xd4, 0xc1, 0xb7, 0x74, 0x70, 0xb5, 0xf6, 0x8f, 0x61, 0x7f, 0x96, + 0x2e, 0x51, 0xc8, 0xb2, 0xe5, 0x8f, 0xa1, 0xa5, 0x37, 0x65, 0xbb, 0x30, 0x0b, 0xff, 0x08, 0x7a, + 0xa5, 0x9b, 0xed, 0xb7, 0x07, 0x3b, 0x2c, 0xb6, 0x4e, 0x3b, 0x2c, 0xf6, 0x4f, 0xe0, 0xde, 0x33, + 0x2c, 0xb3, 0x4c, 0x99, 0x90, 0x3c, 0x7f, 0x3d, 0x79, 0x3d, 0x7b, 0x5a, 0x9b, 0x97, 0x0c, 0x00, + 0x72, 0xe3, 0x10, 0xb2, 0xd8, 0xee, 0xcc, 0xb5, 0xc8, 0x2c, 0xf6, 0x9f, 0xc1, 0x60, 0x43, 0xd2, + 0x17, 0xc1, 0xac, 0x3e, 0xeb, 0x6d, 0x68, 0x16, 0x39, 0xb3, 0xe9, 0xd4, 0xa7, 0xff, 0xaf, 0x03, + 0x5d, 0x1b, 0xff, 0xe3, 0x05, 0xa6, 0x92, 0xdc, 0x81, 0x0e, 0xaa, 0x8f, 0xb0, 0xda, 0x44, 0x5b, + 0xaf, 0x67, 0xb1, 0xe2, 0xbb, 0xe2, 0x2e, 0x4c, 0x84, 0x4e, 0xd3, 0x0c, 0xf6, 0x2a, 0xec, 0xb9, + 0x20, 0xc7, 0xb0, 0x5f, 0xb6, 0x2d, 0x24, 0x95, 0xa8, 0x39, 0x77, 0xa7, 0x8d, 0xa0, 0x6b, 0xe1, + 0x13, 0x85, 0x92, 0x43, 0x68, 0xe9, 0xa4, 0x86, 0xf5, 0x69, 0x23, 0x30, 0x4b, 0x72, 0x0c, 0x3d, + 0x5e, 0xc8, 0x88, 0x27, 0x18, 0xe6, 0x48, 0x05, 0x4f, 0xbd, 0x96, 0x6e, 0x61, 0xdf, 0xa2, 0x81, + 0x06, 0x27, 0x5d, 0x00, 0x1e, 0x45, 0x45, 0x9e, 0x63, 0x1a, 0xa1, 0x7f, 0x0e, 0xbd, 0xab, 0x44, + 0xac, 0x91, 0xe7, 0xac, 0x91, 0x47, 0xbe, 0x87, 0x5d, 0x5d, 0x4e, 0xed, 0xa0, 0x39, 0xdc, 0x7b, + 0x34, 0x1c, 0x6d, 0x17, 0xee, 0xe8, 0x32, 0x39, 0x81, 0x8d, 0xf3, 0xe9, 0x46, 0xfa, 0xd5, 0x99, + 0x5a, 0x11, 0xac, 0x4a, 0x38, 0x37, 0x2c, 0xf1, 0x12, 0xee, 0x6f, 0x3b, 0x61, 0x5b, 0x63, 0x0a, + 0xee, 0xa9, 0xc6, 0x19, 0x96, 0x65, 0x1e, 0xd6, 0x95, 0xb9, 0x9a, 0x2b, 0x58, 0x05, 0xfb, 0xff, + 0x38, 0x5a, 0xa3, 0xf3, 0x9c, 0xbf, 0xc4, 0x48, 0x1f, 0x51, 0x21, 0xde, 0x43, 0x4d, 0x03, 0x80, + 0xe8, 0x94, 0xa6, 0x4b, 0x0c, 0x57, 0xa2, 0x72, 0x0d, 0xf2, 0x22, 0x67, 0x64, 0x00, 0xed, 0xcc, + 0x64, 0xac, 0x54, 0x50, 0x02, 0x6f, 0x1d, 0x87, 0xdc, 0x05, 0x37, 0xa3, 0x4b, 0x0c, 0x05, 0x7b, + 0x63, 0x6e, 0x5f, 0x2b, 0xe8, 0x28, 0xe0, 0x84, 0xbd, 0xd1, 0xa9, 0xb5, 0x51, 0xf2, 0x57, 0x58, + 0x8a, 0x40, 0xbb, 0xff, 0xac, 0x80, 0x09, 0x40, 0x27, 0xb4, 0xa9, 0x7c, 0x0e, 0x1f, 0xd9, 0xc6, + 0x7f, 0xa1, 0x67, 0x2c, 0xa6, 0x92, 0xf1, 0x54, 0x0d, 0x8d, 0xb2, 0xb6, 0x15, 0xb1, 0x5d, 0x92, + 0x2f, 0xe1, 0x60, 0x91, 0x23, 0x7d, 0xa5, 0xb2, 0xc7, 0xb8, 0xcc, 0xd1, 0xcc, 0x0d, 0x67, 0xda, + 0x08, 0x7a, 0xa5, 0xe1, 0xa9, 0xc6, 0xdf, 0x3a, 0xce, 0x84, 0xc0, 0xed, 0x70, 0xcd, 0xdd, 0xff, + 0xaf, 0xa9, 0x4f, 0x7f, 0x13, 0x5b, 0xf6, 0x64, 0xae, 0xd1, 0x5f, 0xc5, 0xe6, 0xce, 0x76, 0x36, + 0x9b, 0xeb, 0x6c, 0xde, 0x81, 0xce, 0x82, 0x0a, 0x63, 0x34, 0xb3, 0xaa, 0xad, 0xd6, 0xca, 0xf4, + 0xd9, 0xfa, 0xa5, 0x33, 0x7c, 0x5d, 0xbd, 0x72, 0x3e, 0xec, 0x17, 0x59, 0x4c, 0x25, 0xc6, 0x21, + 0x95, 0xea, 0xf6, 0x82, 0xb9, 0xbd, 0x16, 0x7c, 0x22, 0x9f, 0x0b, 0xf2, 0x04, 0xfa, 0x39, 0x66, + 0x5c, 0x30, 0x25, 0x90, 0xf5, 0x7d, 0x7b, 0xbb, 0x96, 0x26, 0x6f, 0xe5, 0x33, 0x59, 0x27, 0x8c, + 0x3c, 0x06, 0xcf, 0x32, 0x1d, 0xe6, 0x28, 0x8a, 0x33, 0x29, 0xc2, 0x88, 0x27, 0xd9, 0x19, 0x4a, + 0xf4, 0xda, 0x47, 0xce, 0xb0, 0x13, 0x1c, 0x5a, 0x7b, 0x60, 0xcc, 0x3f, 0x58, 0x2b, 0x99, 0x41, + 0xc7, 0x5a, 0x84, 0xd7, 0xd1, 0x6a, 0xfe, 0xaa, 0x4e, 0xcd, 0xef, 0x9c, 0x79, 0x50, 0x85, 0x93, + 0xcf, 0xe1, 0x20, 0xc5, 0xdf, 0x65, 0x78, 0x49, 0x42, 0xae, 0x99, 0x23, 0x0a, 0x9e, 0x57, 0x32, + 0x1a, 0xc0, 0xdd, 0x70, 0xfb, 0x86, 0x1f, 0xfd, 0xd1, 0x02, 0xf7, 0xa4, 0x2c, 0x48, 0x7e, 0x83, + 0x5b, 0xea, 0x5d, 0x22, 0x0f, 0x6a, 0xbb, 0x5a, 0xbd, 0x71, 0xfd, 0xe1, 0xf5, 0x8e, 0x46, 0x2f, + 0x7e, 0x83, 0x50, 0xd8, 0x35, 0xcf, 0x08, 0xf9, 0xa2, 0x2e, 0xea, 0xca, 0x8b, 0xd4, 0x7f, 0xf8, + 0x3e, 0xae, 0x55, 0x89, 0xbf, 0x1c, 0xf8, 0x64, 0xe3, 0xd0, 0x22, 0x8f, 0xeb, 0xf2, 0xd4, 0xbd, + 0x5d, 0xfd, 0x6f, 0x6f, 0x10, 0x59, 0x35, 0xf4, 0xb7, 0x03, 0x87, 0x9b, 0x47, 0x1c, 0xf9, 0xd0, + 0xbc, 0xab, 0x51, 0xd5, 0xff, 0xee, 0x26, 0xa1, 0xeb, 0x24, 0xbd, 0x7b, 0xb7, 0xaf, 0x25, 0x69, + 0xeb, 0xf0, 0xbc, 0x96, 0xa4, 0xed, 0x83, 0xc4, 0x6f, 0x4c, 0x10, 0xee, 0x47, 0x3c, 0xa9, 0xc9, + 0x30, 0xe9, 0x55, 0x12, 0x9d, 0xab, 0xbf, 0xb0, 0xb9, 0xf3, 0xeb, 0xd7, 0x4b, 0x26, 0x4f, 0x8b, + 0xc5, 0x28, 0xe2, 0xc9, 0x58, 0x05, 0x8e, 0x2f, 0x05, 0x8e, 0x69, 0xc6, 0xc6, 0x55, 0xf0, 0x58, + 0xff, 0xb8, 0x65, 0x8b, 0xc5, 0xae, 0xfe, 0xf8, 0xe6, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xac, + 0xda, 0x1e, 0xfe, 0xd4, 0x09, 0x00, 0x00, }, } diff --git a/api/stovepipe/protopb/stovepipe_grpc.pb.go b/api/stovepipe/protopb/stovepipe_grpc.pb.go index 3387135f..1ceefa4c 100644 --- a/api/stovepipe/protopb/stovepipe_grpc.pb.go +++ b/api/stovepipe/protopb/stovepipe_grpc.pb.go @@ -38,6 +38,7 @@ const ( Stovepipe_Ingest_FullMethodName = "/uber.submitqueue.stovepipe.Stovepipe/Ingest" Stovepipe_GetRequestHistoryByID_FullMethodName = "/uber.submitqueue.stovepipe.Stovepipe/GetRequestHistoryByID" Stovepipe_GetRequestHistoryByURI_FullMethodName = "/uber.submitqueue.stovepipe.Stovepipe/GetRequestHistoryByURI" + Stovepipe_GetProjectStatusByURI_FullMethodName = "/uber.submitqueue.stovepipe.Stovepipe/GetProjectStatusByURI" ) // StovepipeClient is the client API for Stovepipe service. @@ -55,6 +56,8 @@ type StovepipeClient interface { GetRequestHistoryByID(ctx context.Context, in *GetRequestHistoryByIDRequest, opts ...grpc.CallOption) (*GetRequestHistoryByIDResponse, error) // GetRequestHistoryByURI returns retained histories for an exact commit URI. GetRequestHistoryByURI(ctx context.Context, in *GetRequestHistoryByURIRequest, opts ...grpc.CallOption) (*GetRequestHistoryByURIResponse, error) + // GetProjectStatusByURI returns the current validation status for an exact commit URI. + GetProjectStatusByURI(ctx context.Context, in *GetProjectStatusByURIRequest, opts ...grpc.CallOption) (*GetProjectStatusByURIResponse, error) } type stovepipeClient struct { @@ -105,6 +108,16 @@ func (c *stovepipeClient) GetRequestHistoryByURI(ctx context.Context, in *GetReq return out, nil } +func (c *stovepipeClient) GetProjectStatusByURI(ctx context.Context, in *GetProjectStatusByURIRequest, opts ...grpc.CallOption) (*GetProjectStatusByURIResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetProjectStatusByURIResponse) + err := c.cc.Invoke(ctx, Stovepipe_GetProjectStatusByURI_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // StovepipeServer is the server API for Stovepipe service. // All implementations must embed UnimplementedStovepipeServer // for forward compatibility. @@ -120,6 +133,8 @@ type StovepipeServer interface { GetRequestHistoryByID(context.Context, *GetRequestHistoryByIDRequest) (*GetRequestHistoryByIDResponse, error) // GetRequestHistoryByURI returns retained histories for an exact commit URI. GetRequestHistoryByURI(context.Context, *GetRequestHistoryByURIRequest) (*GetRequestHistoryByURIResponse, error) + // GetProjectStatusByURI returns the current validation status for an exact commit URI. + GetProjectStatusByURI(context.Context, *GetProjectStatusByURIRequest) (*GetProjectStatusByURIResponse, error) mustEmbedUnimplementedStovepipeServer() } @@ -142,6 +157,9 @@ func (UnimplementedStovepipeServer) GetRequestHistoryByID(context.Context, *GetR func (UnimplementedStovepipeServer) GetRequestHistoryByURI(context.Context, *GetRequestHistoryByURIRequest) (*GetRequestHistoryByURIResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetRequestHistoryByURI not implemented") } +func (UnimplementedStovepipeServer) GetProjectStatusByURI(context.Context, *GetProjectStatusByURIRequest) (*GetProjectStatusByURIResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetProjectStatusByURI not implemented") +} func (UnimplementedStovepipeServer) mustEmbedUnimplementedStovepipeServer() {} func (UnimplementedStovepipeServer) testEmbeddedByValue() {} @@ -235,6 +253,24 @@ func _Stovepipe_GetRequestHistoryByURI_Handler(srv interface{}, ctx context.Cont return interceptor(ctx, in, info, handler) } +func _Stovepipe_GetProjectStatusByURI_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetProjectStatusByURIRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StovepipeServer).GetProjectStatusByURI(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Stovepipe_GetProjectStatusByURI_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StovepipeServer).GetProjectStatusByURI(ctx, req.(*GetProjectStatusByURIRequest)) + } + return interceptor(ctx, in, info, handler) +} + // Stovepipe_ServiceDesc is the grpc.ServiceDesc for Stovepipe service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -258,6 +294,10 @@ var Stovepipe_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetRequestHistoryByURI", Handler: _Stovepipe_GetRequestHistoryByURI_Handler, }, + { + MethodName: "GetProjectStatusByURI", + Handler: _Stovepipe_GetProjectStatusByURI_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "stovepipe.proto", From d8e8912913e0da382c2c56b1d58265178250a04b Mon Sep 17 00:00:00 2001 From: "prath.shenoy" Date: Wed, 16 Sep 2026 19:05:49 +0000 Subject: [PATCH 3/3] feat(stovepipe): Serve repository status **What**: - Return the current validation lifecycle and repository result for a commit. - Distinguish unknown, pending, completed, and inconsistent validation records. **Why**: - Give consumers an authoritative status lookup for a validation run. - Establish the read path before project-level results are available. --- service/stovepipe/server/main.go | 12 ++ service/stovepipe/server/mapper/BUILD.bazel | 2 + .../stovepipe/server/mapper/project_status.go | 49 +++++ .../server/mapper/project_status_test.go | 44 ++++ stovepipe/controller/BUILD.bazel | 2 + .../controller/get_project_status_by_uri.go | 190 ++++++++++++++++++ .../get_project_status_by_uri_test.go | 156 ++++++++++++++ stovepipe/entity/BUILD.bazel | 1 + stovepipe/entity/project_status.go | 43 ++++ 9 files changed, 499 insertions(+) create mode 100644 service/stovepipe/server/mapper/project_status.go create mode 100644 service/stovepipe/server/mapper/project_status_test.go create mode 100644 stovepipe/controller/get_project_status_by_uri.go create mode 100644 stovepipe/controller/get_project_status_by_uri_test.go create mode 100644 stovepipe/entity/project_status.go diff --git a/service/stovepipe/server/main.go b/service/stovepipe/server/main.go index ce5d2dcd..7783697d 100644 --- a/service/stovepipe/server/main.go +++ b/service/stovepipe/server/main.go @@ -71,6 +71,7 @@ type StovepipeServer struct { pingController *controller.PingController ingestController *controller.IngestController requestHistoryController controller.RequestHistoryController + projectStatusController *controller.GetProjectStatusByURIController } // Ping delegates to the controller. @@ -106,6 +107,15 @@ func (s *StovepipeServer) GetRequestHistoryByURI(ctx context.Context, req *pb.Ge return &pb.GetRequestHistoryByURIResponse{Histories: mapper.RequestHistoriesToProto(histories)}, nil } +// GetProjectStatusByURI returns the current repository validation status for a commit. +func (s *StovepipeServer) GetProjectStatusByURI(ctx context.Context, req *pb.GetProjectStatusByURIRequest) (*pb.GetProjectStatusByURIResponse, error) { + result, err := s.projectStatusController.GetProjectStatusByURI(ctx, mapper.ProtoToGetProjectStatusByURIRequest(req)) + if err != nil { + return nil, err + } + return mapper.GetProjectStatusByURIResultToProto(result), nil +} + // inMemoryCounter is a minimal, process-local counter.Counter used to wire the example // server. It is not durable; a real deployment supplies a persistent implementation // (e.g. platform/extension/counter/mysql). @@ -372,10 +382,12 @@ func run() error { tenants, ) requestHistoryController := controller.NewRequestHistoryController(logger.Sugar(), scope, storageFty) + projectStatusController := controller.NewGetProjectStatusByURIController(logger.Sugar(), scope, storageFty) srv := &StovepipeServer{ pingController: pingController, ingestController: ingestController, requestHistoryController: requestHistoryController, + projectStatusController: projectStatusController, } pb.RegisterStovepipeServer(grpcServer, srv) diff --git a/service/stovepipe/server/mapper/BUILD.bazel b/service/stovepipe/server/mapper/BUILD.bazel index e78b6a4d..992eca5a 100644 --- a/service/stovepipe/server/mapper/BUILD.bazel +++ b/service/stovepipe/server/mapper/BUILD.bazel @@ -4,6 +4,7 @@ go_library( name = "go_default_library", srcs = [ "ingest.go", + "project_status.go", "request_history.go", ], importpath = "github.com/uber/submitqueue/service/stovepipe/server/mapper", @@ -18,6 +19,7 @@ go_test( name = "go_default_test", srcs = [ "ingest_test.go", + "project_status_test.go", "request_history_test.go", ], embed = [":go_default_library"], diff --git a/service/stovepipe/server/mapper/project_status.go b/service/stovepipe/server/mapper/project_status.go new file mode 100644 index 00000000..2dbba534 --- /dev/null +++ b/service/stovepipe/server/mapper/project_status.go @@ -0,0 +1,49 @@ +// 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 mapper + +import ( + pb "github.com/uber/submitqueue/api/stovepipe/protopb" + "github.com/uber/submitqueue/stovepipe/entity" +) + +// ProtoToGetProjectStatusByURIRequest maps a wire selector to its domain form. +func ProtoToGetProjectStatusByURIRequest(req *pb.GetProjectStatusByURIRequest) entity.GetProjectStatusByURIRequest { + result := entity.GetProjectStatusByURIRequest{ + Queue: req.GetQueue(), + ChangeURI: req.GetChangeUri(), + PageSize: req.GetPageSize(), + PageToken: req.GetPageToken(), + } + result.Projects = req.GetProjects() + return result +} + +// GetProjectStatusByURIResultToProto maps a domain status projection to its wire response. +func GetProjectStatusByURIResultToProto(result entity.GetProjectStatusByURIResult) *pb.GetProjectStatusByURIResponse { + response := &pb.GetProjectStatusByURIResponse{ + RequestId: result.Request.ID, + Queue: result.Request.Queue, + ChangeUri: result.Request.URI, + BaseUri: result.Request.BaseURI, + RequestState: string(result.Request.State), + UpdatedAtMs: result.UpdatedAtMs, + ProjectResultsComplete: result.ProjectResultsComplete, + } + if result.HasRepositoryValidationFact { + response.RepositoryBreakageDegree = &result.RepositoryValidationFact.Degree + } + return response +} diff --git a/service/stovepipe/server/mapper/project_status_test.go b/service/stovepipe/server/mapper/project_status_test.go new file mode 100644 index 00000000..fb4e6939 --- /dev/null +++ b/service/stovepipe/server/mapper/project_status_test.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 mapper + +import ( + "testing" + + "github.com/stretchr/testify/assert" + pb "github.com/uber/submitqueue/api/stovepipe/protopb" + "github.com/uber/submitqueue/stovepipe/entity" +) + +func TestProjectStatusRequestAndResponseMapping(t *testing.T) { + request := &pb.GetProjectStatusByURIRequest{ + Queue: "queue", ChangeUri: "uri", Projects: []string{"project-a", "project-b"}, PageSize: 25, PageToken: "token", + } + assert.Equal(t, entity.GetProjectStatusByURIRequest{ + Queue: "queue", ChangeURI: "uri", Projects: []string{"project-a", "project-b"}, PageSize: 25, PageToken: "token", + }, ProtoToGetProjectStatusByURIRequest(request)) + + degree := entity.DegreeGreen + response := GetProjectStatusByURIResultToProto(entity.GetProjectStatusByURIResult{ + Request: entity.Request{ID: "request/1", Queue: "queue", URI: "uri", BaseURI: "base", State: entity.RequestStateSucceeded}, + RepositoryValidationFact: entity.ValidationFact{Degree: degree}, + HasRepositoryValidationFact: true, + }) + assert.Equal(t, "request/1", response.GetRequestId()) + assert.Equal(t, "succeeded", response.GetRequestState()) + assert.Equal(t, °ree, response.RepositoryBreakageDegree) + assert.False(t, response.GetProjectResultsComplete()) + assert.Empty(t, response.GetProjects()) +} diff --git a/stovepipe/controller/BUILD.bazel b/stovepipe/controller/BUILD.bazel index ffa83e23..9a0a3f2e 100644 --- a/stovepipe/controller/BUILD.bazel +++ b/stovepipe/controller/BUILD.bazel @@ -3,6 +3,7 @@ load("@rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", srcs = [ + "get_project_status_by_uri.go", "ingest.go", "ping.go", "read_errors.go", @@ -30,6 +31,7 @@ go_library( go_test( name = "go_default_test", srcs = [ + "get_project_status_by_uri_test.go", "ingest_test.go", "ping_test.go", "request_history_test.go", diff --git a/stovepipe/controller/get_project_status_by_uri.go b/stovepipe/controller/get_project_status_by_uri.go new file mode 100644 index 00000000..8dc30102 --- /dev/null +++ b/stovepipe/controller/get_project_status_by_uri.go @@ -0,0 +1,190 @@ +// 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 controller + +import ( + "context" + "errors" + "fmt" + "math" + + "github.com/uber-go/tally" + "github.com/uber/submitqueue/platform/errs" + "github.com/uber/submitqueue/platform/metrics" + "github.com/uber/submitqueue/stovepipe/entity" + "github.com/uber/submitqueue/stovepipe/extension/storage" + "go.uber.org/zap" +) + +const maxProjectStatusPageSize = 200 + +// ProjectStatusNotFoundError indicates that no validation request matches a lookup selector. +type ProjectStatusNotFoundError struct { + // Queue is the queue in the selector. + Queue string + // ChangeURI is the commit URI in the selector. + ChangeURI string +} + +func (e *ProjectStatusNotFoundError) Error() string { + return fmt.Sprintf("project status not found for queue %q and change URI %q", e.Queue, e.ChangeURI) +} + +// IsProjectStatusNotFound reports whether err represents an unknown validation request. +func IsProjectStatusNotFound(err error) bool { + var target *ProjectStatusNotFoundError + return errors.As(err, &target) +} + +// ProjectStatusConsistencyError indicates that persisted records disagree about a request. +type ProjectStatusConsistencyError struct { + message string +} + +func (e *ProjectStatusConsistencyError) Error() string { return e.message } + +// IsProjectStatusConsistency reports whether err represents inconsistent persisted state. +func IsProjectStatusConsistency(err error) bool { + var target *ProjectStatusConsistencyError + return errors.As(err, &target) +} + +// GetProjectStatusByURIController reads the durable repository-level validation projection. +// Project result reads are added when planned-project storage exists. +type GetProjectStatusByURIController struct { + logger *zap.SugaredLogger + metricsScope tally.Scope + stores storage.Factory +} + +// NewGetProjectStatusByURIController creates a controller for validation-status lookups. +func NewGetProjectStatusByURIController(logger *zap.SugaredLogger, scope tally.Scope, stores storage.Factory) *GetProjectStatusByURIController { + return &GetProjectStatusByURIController{ + logger: logger, + metricsScope: scope.SubScope("get_project_status_by_uri_controller"), + stores: stores, + } +} + +// GetProjectStatusByURI returns the selected request and its repository validation fact, if recorded. +func (c *GetProjectStatusByURIController) GetProjectStatusByURI(ctx context.Context, req entity.GetProjectStatusByURIRequest) (result entity.GetProjectStatusByURIResult, retErr error) { + op := metrics.Begin(c.metricsScope, "get_project_status_by_uri", metrics.StorageLatencyBuckets, metrics.TagsFromContext(ctx)...) + defer func() { op.Complete(retErr) }() + + if err := validateProjectStatusRequest(req); err != nil { + return entity.GetProjectStatusByURIResult{}, err + } + store, err := c.stores.For(storage.Config{QueueName: req.Queue}) + if err != nil { + return entity.GetProjectStatusByURIResult{}, fmt.Errorf("GetProjectStatusByURI failed to resolve storage for queue %q: %w", req.Queue, err) + } + + requestID, err := store.GetRequestURIStore().GetIDByURI(ctx, req.ChangeURI) + if err != nil { + if storage.IsNotFound(err) { + return entity.GetProjectStatusByURIResult{}, errs.NewUserError(&ProjectStatusNotFoundError{Queue: req.Queue, ChangeURI: req.ChangeURI}) + } + return entity.GetProjectStatusByURIResult{}, fmt.Errorf("GetProjectStatusByURI failed to resolve request for URI %q: %w", req.ChangeURI, err) + } + + request, err := store.GetRequestStore().Get(ctx, requestID) + if err != nil { + if storage.IsNotFound(err) { + // The URI mapping is created before the request, so this gap is retryable. + return entity.GetProjectStatusByURIResult{}, errs.NewRetryableError(fmt.Errorf("GetProjectStatusByURI request %q is not visible yet", requestID)) + } + return entity.GetProjectStatusByURIResult{}, fmt.Errorf("GetProjectStatusByURI failed to load request %q: %w", requestID, err) + } + if request.ID != requestID || request.Queue != req.Queue || request.URI != req.ChangeURI { + return entity.GetProjectStatusByURIResult{}, &ProjectStatusConsistencyError{message: "request URI mapping disagrees with stored request"} + } + result.Request = request + logs, err := store.GetRequestLogStore().List(ctx, request.ID) + if storage.IsNotFound(err) { + return entity.GetProjectStatusByURIResult{}, errs.NewRetryableError(fmt.Errorf("GetProjectStatusByURI request %q has no visible lifecycle record yet", request.ID)) + } + if err != nil { + return entity.GetProjectStatusByURIResult{}, fmt.Errorf("GetProjectStatusByURI failed to load request history for %q: %w", request.ID, err) + } + stateRecorded := false + for _, log := range logs { + if log.TimestampMs > result.UpdatedAtMs { + result.UpdatedAtMs = log.TimestampMs + } + if log.State == request.State && log.RequestVersion == request.Version { + stateRecorded = true + } + } + if !stateRecorded { + return entity.GetProjectStatusByURIResult{}, errs.NewRetryableError(fmt.Errorf("GetProjectStatusByURI request %q lifecycle record is not current", request.ID)) + } + fact, err := store.GetValidationFactStore().Get(ctx, request.URI, "") + if err != nil { + if storage.IsNotFound(err) { + return result, nil + } + return entity.GetProjectStatusByURIResult{}, fmt.Errorf("GetProjectStatusByURI failed to load repository fact for request %q: %w", request.ID, err) + } + if err := validateRepositoryFact(fact, request); err != nil { + return entity.GetProjectStatusByURIResult{}, err + } + result.RepositoryValidationFact = fact + result.HasRepositoryValidationFact = true + if fact.CreatedAt > result.UpdatedAtMs { + result.UpdatedAtMs = fact.CreatedAt + } + + c.logger.Debugw("project status retrieved", "request_id", request.ID, "queue", request.Queue, "change_uri", request.URI, "has_repository_result", true) + return result, nil +} + +func validateProjectStatusRequest(req entity.GetProjectStatusByURIRequest) error { + if err := validateHistoryIdentifier("queue", req.Queue); err != nil { + return fmt.Errorf("GetProjectStatusByURI invalid queue=%q: %w", req.Queue, err) + } + if err := validateHistoryIdentifier("change URI", req.ChangeURI); err != nil { + return fmt.Errorf("GetProjectStatusByURI invalid change_uri=%q: %w", req.ChangeURI, err) + } + if len(req.Projects) == 0 { + return fmt.Errorf("GetProjectStatusByURI projects must be non-empty: %w", ErrInvalidRequest) + } + seen := make(map[string]struct{}, len(req.Projects)) + for _, project := range req.Projects { + if err := validateHistoryIdentifier("project", project); err != nil { + return fmt.Errorf("GetProjectStatusByURI invalid project=%q: %w", project, err) + } + if _, ok := seen[project]; ok { + return fmt.Errorf("GetProjectStatusByURI project %q is duplicated: %w", project, ErrInvalidRequest) + } + seen[project] = struct{}{} + } + if req.PageSize < 0 || req.PageSize > maxProjectStatusPageSize { + return fmt.Errorf("GetProjectStatusByURI page_size must be between 0 and %d: %w", maxProjectStatusPageSize, ErrInvalidRequest) + } + if req.PageToken != "" { + return fmt.Errorf("GetProjectStatusByURI page_token is unsupported until project results are available: %w", ErrInvalidRequest) + } + return nil +} + +func validateRepositoryFact(fact entity.ValidationFact, request entity.Request) error { + if fact.URI != request.URI || fact.Project != "" || fact.RequestID != request.ID { + return &ProjectStatusConsistencyError{message: "repository validation fact disagrees with stored request"} + } + if math.IsNaN(fact.Degree) || fact.Degree < entity.DegreeGreen || fact.Degree > entity.DegreeBroken { + return &ProjectStatusConsistencyError{message: "repository validation fact has an invalid degree"} + } + return nil +} diff --git a/stovepipe/controller/get_project_status_by_uri_test.go b/stovepipe/controller/get_project_status_by_uri_test.go new file mode 100644 index 00000000..acb90415 --- /dev/null +++ b/stovepipe/controller/get_project_status_by_uri_test.go @@ -0,0 +1,156 @@ +// 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 controller + +import ( + "context" + "errors" + "math" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/uber-go/tally" + "github.com/uber/submitqueue/platform/errs" + "github.com/uber/submitqueue/stovepipe/entity" + "github.com/uber/submitqueue/stovepipe/extension/storage" + storagemock "github.com/uber/submitqueue/stovepipe/extension/storage/mock" + "go.uber.org/mock/gomock" + "go.uber.org/zap" +) + +const ( + projectStatusQueue = "monorepo/main" + projectStatusURI = "git://monorepo/main/abc" + projectStatusID = "request/monorepo/main/7" +) + +func TestGetProjectStatusByURI(t *testing.T) { + request := entity.Request{ID: projectStatusID, Queue: projectStatusQueue, URI: projectStatusURI, State: entity.RequestStateProcessing, Version: 1} + tests := []struct { + name string + request entity.GetProjectStatusByURIRequest + uriErr error + requestErr error + fact entity.ValidationFact + factErr error + wantFact bool + wantNotFound bool + wantRetryable bool + wantInvalid bool + wantConsistency bool + }{ + {name: "in progress without fact", request: entity.GetProjectStatusByURIRequest{Queue: projectStatusQueue, ChangeURI: projectStatusURI, Projects: []string{"project-a"}}, factErr: storage.ErrNotFound}, + {name: "recorded green fact", request: entity.GetProjectStatusByURIRequest{Queue: projectStatusQueue, ChangeURI: projectStatusURI, Projects: []string{"project-a"}}, fact: entity.ValidationFact{URI: projectStatusURI, RequestID: projectStatusID, Degree: entity.DegreeGreen}, wantFact: true}, + {name: "missing uri", request: entity.GetProjectStatusByURIRequest{Queue: projectStatusQueue, ChangeURI: projectStatusURI, Projects: []string{"project-a"}}, uriErr: storage.ErrNotFound, wantNotFound: true}, + {name: "request visibility gap", request: entity.GetProjectStatusByURIRequest{Queue: projectStatusQueue, ChangeURI: projectStatusURI, Projects: []string{"project-a"}}, requestErr: storage.ErrNotFound, wantRetryable: true}, + {name: "empty queue", request: entity.GetProjectStatusByURIRequest{ChangeURI: projectStatusURI}, wantInvalid: true}, + {name: "empty projects", request: entity.GetProjectStatusByURIRequest{Queue: projectStatusQueue, ChangeURI: projectStatusURI}, wantInvalid: true}, + {name: "empty project", request: entity.GetProjectStatusByURIRequest{Queue: projectStatusQueue, ChangeURI: projectStatusURI, Projects: []string{""}}, wantInvalid: true}, + {name: "duplicate projects", request: entity.GetProjectStatusByURIRequest{Queue: projectStatusQueue, ChangeURI: projectStatusURI, Projects: []string{"project-a", "project-a"}}, wantInvalid: true}, + {name: "invalid page size", request: entity.GetProjectStatusByURIRequest{Queue: projectStatusQueue, ChangeURI: projectStatusURI, PageSize: maxProjectStatusPageSize + 1}, wantInvalid: true}, + {name: "page token before project results", request: entity.GetProjectStatusByURIRequest{Queue: projectStatusQueue, ChangeURI: projectStatusURI, PageToken: "token"}, wantInvalid: true}, + {name: "invalid fact degree", request: entity.GetProjectStatusByURIRequest{Queue: projectStatusQueue, ChangeURI: projectStatusURI, Projects: []string{"project-a"}}, fact: entity.ValidationFact{URI: projectStatusURI, RequestID: projectStatusID, Degree: math.NaN()}, wantConsistency: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockCtrl := gomock.NewController(t) + factory := storagemock.NewMockFactory(mockCtrl) + store := storagemock.NewMockStorage(mockCtrl) + uriStore := storagemock.NewMockRequestURIStore(mockCtrl) + requestStore := storagemock.NewMockRequestStore(mockCtrl) + logStore := storagemock.NewMockRequestLogStore(mockCtrl) + factStore := storagemock.NewMockValidationFactStore(mockCtrl) + if !tt.wantInvalid { + factory.EXPECT().For(storage.Config{QueueName: projectStatusQueue}).Return(store, nil) + store.EXPECT().GetRequestURIStore().Return(uriStore) + uriStore.EXPECT().GetIDByURI(gomock.Any(), projectStatusURI).Return(projectStatusID, tt.uriErr) + if tt.uriErr == nil { + store.EXPECT().GetRequestStore().Return(requestStore) + requestStore.EXPECT().Get(gomock.Any(), projectStatusID).Return(request, tt.requestErr) + if tt.requestErr == nil { + store.EXPECT().GetRequestLogStore().Return(logStore) + logStore.EXPECT().List(gomock.Any(), projectStatusID).Return([]entity.RequestLog{{State: request.State, RequestVersion: request.Version, TimestampMs: 1}}, nil) + store.EXPECT().GetValidationFactStore().Return(factStore) + factStore.EXPECT().Get(gomock.Any(), projectStatusURI, "").Return(tt.fact, tt.factErr) + } + } + } + + controller := NewGetProjectStatusByURIController(zap.NewNop().Sugar(), tally.NoopScope, factory) + got, err := controller.GetProjectStatusByURI(context.Background(), tt.request) + + if tt.wantInvalid { + assert.True(t, IsInvalidRequest(err)) + } + assert.Equal(t, tt.wantNotFound, IsProjectStatusNotFound(err)) + assert.Equal(t, tt.wantRetryable, errs.IsRetryable(err)) + assert.Equal(t, tt.wantConsistency, IsProjectStatusConsistency(err)) + if tt.wantInvalid || tt.wantNotFound || tt.wantRetryable || tt.wantConsistency { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, request, got.Request) + assert.Equal(t, tt.wantFact, got.HasRepositoryValidationFact) + assert.False(t, got.ProjectResultsComplete) + if tt.wantFact { + assert.Equal(t, tt.fact, got.RepositoryValidationFact) + } + }) + } +} + +func TestGetProjectStatusByURIRequiresCurrentLifecycleRecord(t *testing.T) { + request := entity.Request{ID: projectStatusID, Queue: projectStatusQueue, URI: projectStatusURI, State: entity.RequestStateProcessing, Version: 2} + mockCtrl := gomock.NewController(t) + factory := storagemock.NewMockFactory(mockCtrl) + store := storagemock.NewMockStorage(mockCtrl) + uriStore := storagemock.NewMockRequestURIStore(mockCtrl) + requestStore := storagemock.NewMockRequestStore(mockCtrl) + logStore := storagemock.NewMockRequestLogStore(mockCtrl) + factory.EXPECT().For(storage.Config{QueueName: projectStatusQueue}).Return(store, nil) + store.EXPECT().GetRequestURIStore().Return(uriStore) + uriStore.EXPECT().GetIDByURI(gomock.Any(), projectStatusURI).Return(projectStatusID, nil) + store.EXPECT().GetRequestStore().Return(requestStore) + requestStore.EXPECT().Get(gomock.Any(), projectStatusID).Return(request, nil) + store.EXPECT().GetRequestLogStore().Return(logStore) + logStore.EXPECT().List(gomock.Any(), projectStatusID).Return([]entity.RequestLog{{State: entity.RequestStateAccepted, RequestVersion: 1, TimestampMs: 1}}, nil) + + controller := NewGetProjectStatusByURIController(zap.NewNop().Sugar(), tally.NoopScope, factory) + _, err := controller.GetProjectStatusByURI(context.Background(), entity.GetProjectStatusByURIRequest{Queue: projectStatusQueue, ChangeURI: projectStatusURI, Projects: []string{"project-a"}}) + require.Error(t, err) + assert.True(t, errs.IsRetryable(err)) +} + +func TestGetProjectStatusByURIRejectsInconsistentRequest(t *testing.T) { + mockCtrl := gomock.NewController(t) + factory := storagemock.NewMockFactory(mockCtrl) + store := storagemock.NewMockStorage(mockCtrl) + uriStore := storagemock.NewMockRequestURIStore(mockCtrl) + requestStore := storagemock.NewMockRequestStore(mockCtrl) + factory.EXPECT().For(storage.Config{QueueName: projectStatusQueue}).Return(store, nil) + store.EXPECT().GetRequestURIStore().Return(uriStore) + uriStore.EXPECT().GetIDByURI(gomock.Any(), projectStatusURI).Return(projectStatusID, nil) + store.EXPECT().GetRequestStore().Return(requestStore) + requestStore.EXPECT().Get(gomock.Any(), projectStatusID).Return(entity.Request{ID: projectStatusID, Queue: projectStatusQueue, URI: "other"}, nil) + + controller := NewGetProjectStatusByURIController(zap.NewNop().Sugar(), tally.NoopScope, factory) + _, err := controller.GetProjectStatusByURI(context.Background(), entity.GetProjectStatusByURIRequest{Queue: projectStatusQueue, ChangeURI: projectStatusURI, Projects: []string{"project-a"}}) + require.Error(t, err) + assert.True(t, IsProjectStatusConsistency(err)) + assert.False(t, errors.Is(err, storage.ErrNotFound)) +} diff --git a/stovepipe/entity/BUILD.bazel b/stovepipe/entity/BUILD.bazel index d4798469..9671b8fd 100644 --- a/stovepipe/entity/BUILD.bazel +++ b/stovepipe/entity/BUILD.bazel @@ -5,6 +5,7 @@ go_library( srcs = [ "build.go", "ingest.go", + "project_status.go", "queue.go", "queue_config.go", "request.go", diff --git a/stovepipe/entity/project_status.go b/stovepipe/entity/project_status.go new file mode 100644 index 00000000..09e067eb --- /dev/null +++ b/stovepipe/entity/project_status.go @@ -0,0 +1,43 @@ +// 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 + +// GetProjectStatusByURIRequest selects a validation request by queue and commit URI. +type GetProjectStatusByURIRequest struct { + // Queue identifies the queue containing the validation request. + Queue string + // ChangeURI identifies the exact commit under validation. + ChangeURI string + // Projects limits results to the supplied consumer-defined project IDs. + Projects []string + // PageSize is the requested maximum number of full-result projects. + PageSize int32 + // PageToken is an opaque continuation token for full project-result pagination. + PageToken string +} + +// GetProjectStatusByURIResult is the current validation projection for one request. +type GetProjectStatusByURIResult struct { + // Request is the authoritative validation request selected by the lookup. + Request Request + // RepositoryValidationFact is the repository result when HasRepositoryValidationFact is true. + RepositoryValidationFact ValidationFact + // HasRepositoryValidationFact distinguishes a missing fact from a recorded green result. + HasRepositoryValidationFact bool + // ProjectResultsComplete reports whether the implementation has finished its project-result set. + ProjectResultsComplete bool + // UpdatedAtMs is the newest durable lifecycle or repository-result timestamp. + UpdatedAtMs int64 +}