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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions service/stovepipe/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ import (
// StovepipeServer wraps the controllers and implements the gRPC service interface.
type StovepipeServer struct {
pb.UnimplementedStovepipeServer
pingController *controller.PingController
ingestController *controller.IngestController
pingController *controller.PingController
ingestController *controller.IngestController
getProjectStatusByURIController *controller.GetProjectStatusByURIController
}

// Ping delegates to the controller.
Expand All @@ -84,6 +85,15 @@ func (s *StovepipeServer) Ingest(ctx context.Context, req *pb.IngestRequest) (*p
return mapper.IngestResultToProto(result), nil
}

// GetProjectStatusByURI returns current repository validation for an exact commit URI.
func (s *StovepipeServer) GetProjectStatusByURI(ctx context.Context, req *pb.GetProjectStatusByURIRequest) (*pb.GetProjectStatusByURIResponse, error) {
result, err := s.getProjectStatusByURIController.GetProjectStatusByURI(ctx, mapper.ProtoToGetProjectStatusByURIRequest(req))
if err != nil {
return nil, err
}
return mapper.GetProjectStatusByURIResultToProto(result)
}

// 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).
Expand Down Expand Up @@ -336,9 +346,11 @@ func run() error {
storageFty,
registry,
)
getProjectStatusByURIController := controller.NewGetProjectStatusByURIController(logger.Sugar(), scope, storageFty)
srv := &StovepipeServer{
pingController: pingController,
ingestController: ingestController,
pingController: pingController,
ingestController: ingestController,
getProjectStatusByURIController: getProjectStatusByURIController,
}
pb.RegisterStovepipeServer(grpcServer, srv)

Expand Down
11 changes: 9 additions & 2 deletions service/stovepipe/server/mapper/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ load("@rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "go_default_library",
srcs = ["ingest.go"],
srcs = [
"get_project_status_by_uri.go",
"ingest.go",
],
importpath = "github.com/uber/submitqueue/service/stovepipe/server/mapper",
visibility = ["//visibility:public"],
deps = [
Expand All @@ -13,11 +16,15 @@ go_library(

go_test(
name = "go_default_test",
srcs = ["ingest_test.go"],
srcs = [
"get_project_status_by_uri_test.go",
"ingest_test.go",
],
embed = [":go_default_library"],
deps = [
"//api/stovepipe/protopb:go_default_library",
"//stovepipe/entity:go_default_library",
"@com_github_stretchr_testify//assert:go_default_library",
"@com_github_stretchr_testify//require:go_default_library",
],
)
75 changes: 75 additions & 0 deletions service/stovepipe/server/mapper/get_project_status_by_uri.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// 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 (
"fmt"

pb "github.com/uber/submitqueue/api/stovepipe/protopb"
"github.com/uber/submitqueue/stovepipe/entity"
)

// ProtoToGetProjectStatusByURIRequest maps the 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(),
}
if req.Project != nil {
result.Project = req.GetProject()
result.HasProject = true
}
return result
}

// GetProjectStatusByURIResultToProto maps a domain projection to the wire response.
func GetProjectStatusByURIResultToProto(result entity.GetProjectStatusByURIResult) (*pb.GetProjectStatusByURIResponse, error) {
requestState, err := projectStatusRequestStateToProto(result.Request.State)
if err != nil {
return nil, err
}
response := &pb.GetProjectStatusByURIResponse{
RequestId: result.Request.ID,
Queue: result.Request.Queue,
ChangeUri: result.Request.URI,
BaseUri: result.Request.BaseURI,
RequestState: requestState,
}
if result.HasRepositoryValidationFact {
response.RepositoryBreakageDegree = &result.RepositoryValidationFact.Degree
}
return response, nil
}

func projectStatusRequestStateToProto(state entity.RequestState) (pb.RequestState, error) {
switch state {
case entity.RequestStateAccepted:
return pb.RequestState_REQUEST_STATE_ACCEPTED, nil
case entity.RequestStateProcessing:
return pb.RequestState_REQUEST_STATE_PROCESSING, nil
case entity.RequestStateSucceeded:
return pb.RequestState_REQUEST_STATE_SUCCEEDED, nil
case entity.RequestStateFailed:
return pb.RequestState_REQUEST_STATE_FAILED, nil
case entity.RequestStateCancelled:
return pb.RequestState_REQUEST_STATE_CANCELLED, nil
case entity.RequestStateSuperseded:
return pb.RequestState_REQUEST_STATE_SUPERSEDED, nil
default:
return pb.RequestState_REQUEST_STATE_UNSPECIFIED, fmt.Errorf("request state %q cannot be represented by GetProjectStatusByURI", state)
}
}
115 changes: 115 additions & 0 deletions service/stovepipe/server/mapper/get_project_status_by_uri_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// 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"
"github.com/stretchr/testify/require"
pb "github.com/uber/submitqueue/api/stovepipe/protopb"
"github.com/uber/submitqueue/stovepipe/entity"
)

func TestProtoToGetProjectStatusByURIRequest(t *testing.T) {
project := ""
got := ProtoToGetProjectStatusByURIRequest(&pb.GetProjectStatusByURIRequest{
Queue: "monorepo/main", ChangeUri: "git://commit", Project: &project, PageSize: 10, PageToken: "token",
})

assert.Equal(t, entity.GetProjectStatusByURIRequest{
Queue: "monorepo/main", ChangeURI: "git://commit", Project: "", HasProject: true, PageSize: 10, PageToken: "token",
}, got)

omitted := ProtoToGetProjectStatusByURIRequest(&pb.GetProjectStatusByURIRequest{})
assert.False(t, omitted.HasProject)
}

func TestGetProjectStatusByURIResultToProto(t *testing.T) {
t.Run("preserves optional field presence", func(t *testing.T) {
result := entity.GetProjectStatusByURIResult{
Request: entity.Request{
ID: "request/monorepo/main/7", Queue: "monorepo/main", URI: "git://commit",
BaseURI: "git://base", State: entity.RequestStateSucceeded,
},
RepositoryValidationFact: entity.ValidationFact{Degree: entity.DegreeGreen},
HasRepositoryValidationFact: true,
}

got, err := GetProjectStatusByURIResultToProto(result)

require.NoError(t, err)
assert.Equal(t, result.Request.ID, got.GetRequestId())
assert.Equal(t, result.Request.Queue, got.GetQueue())
assert.Equal(t, result.Request.URI, got.GetChangeUri())
assert.Equal(t, result.Request.BaseURI, got.GetBaseUri())
assert.NotNil(t, got.RepositoryBreakageDegree)
assert.Equal(t, entity.DegreeGreen, got.GetRepositoryBreakageDegree())
assert.Equal(t, pb.RequestState_REQUEST_STATE_SUCCEEDED, got.GetRequestState())
assert.Empty(t, got.Projects)
assert.False(t, got.ProjectResultsComplete)
assert.Empty(t, got.NextPageToken)
})

t.Run("keeps missing repository fact absent", func(t *testing.T) {
got, err := GetProjectStatusByURIResultToProto(entity.GetProjectStatusByURIResult{
Request: entity.Request{State: entity.RequestStateAccepted},
})

require.NoError(t, err)
assert.Nil(t, got.RepositoryBreakageDegree)
assert.Empty(t, got.Projects)
})

t.Run("rejects an unrecognized request state", func(t *testing.T) {
got, err := GetProjectStatusByURIResultToProto(entity.GetProjectStatusByURIResult{
Request: entity.Request{State: "future"},
})

require.Error(t, err)
assert.Nil(t, got)
})
}

func TestProjectStatusRequestStateToProto(t *testing.T) {
tests := []struct {
name string
state entity.RequestState
want pb.RequestState
wantError bool
}{
{name: "accepted", state: entity.RequestStateAccepted, want: pb.RequestState_REQUEST_STATE_ACCEPTED},
{name: "processing", state: entity.RequestStateProcessing, want: pb.RequestState_REQUEST_STATE_PROCESSING},
{name: "succeeded", state: entity.RequestStateSucceeded, want: pb.RequestState_REQUEST_STATE_SUCCEEDED},
{name: "failed", state: entity.RequestStateFailed, want: pb.RequestState_REQUEST_STATE_FAILED},
{name: "cancelled", state: entity.RequestStateCancelled, want: pb.RequestState_REQUEST_STATE_CANCELLED},
{name: "superseded", state: entity.RequestStateSuperseded, want: pb.RequestState_REQUEST_STATE_SUPERSEDED},
{name: "unknown", state: entity.RequestStateUnknown, wantError: true},
{name: "unrecognized", state: "future", wantError: true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := projectStatusRequestStateToProto(tt.state)

if tt.wantError {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
3 changes: 3 additions & 0 deletions stovepipe/controller/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
Expand All @@ -27,13 +28,15 @@ go_library(
go_test(
name = "go_default_test",
srcs = [
"get_project_status_by_uri_test.go",
"ingest_test.go",
"ping_test.go",
],
embed = [":go_default_library"],
deps = [
"//api/stovepipe/protopb:go_default_library",
"//platform/consumer:go_default_library",
"//platform/errs:go_default_library",
"//platform/extension/counter:go_default_library",
"//platform/extension/counter/mock:go_default_library",
"//platform/extension/messagequeue/mock:go_default_library",
Expand Down
Loading