diff --git a/core/consumer/mock/controller_mock.go b/core/consumer/mock/controller_mock.go index 6dab2be4..2964bb05 100644 --- a/core/consumer/mock/controller_mock.go +++ b/core/consumer/mock/controller_mock.go @@ -1,5 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. // Source: controller.go +// +// Generated by this command: +// +// mockgen -source=controller.go -destination=mock/controller_mock.go -package=mock +// // Package mock is a generated GoMock package. package mock @@ -17,6 +22,7 @@ import ( type MockDelivery struct { ctrl *gomock.Controller recorder *MockDeliveryMockRecorder + isgomock struct{} } // MockDeliveryMockRecorder is the mock recorder for MockDelivery. @@ -73,7 +79,7 @@ func (m *MockDelivery) ExtendVisibilityTimeout(ctx context.Context, durationMill } // ExtendVisibilityTimeout indicates an expected call of ExtendVisibilityTimeout. -func (mr *MockDeliveryMockRecorder) ExtendVisibilityTimeout(ctx, durationMillis interface{}) *gomock.Call { +func (mr *MockDeliveryMockRecorder) ExtendVisibilityTimeout(ctx, durationMillis any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExtendVisibilityTimeout", reflect.TypeOf((*MockDelivery)(nil).ExtendVisibilityTimeout), ctx, durationMillis) } @@ -124,6 +130,7 @@ func (mr *MockDeliveryMockRecorder) ReceivedAt() *gomock.Call { type MockController struct { ctrl *gomock.Controller recorder *MockControllerMockRecorder + isgomock struct{} } // MockControllerMockRecorder is the mock recorder for MockController. @@ -180,7 +187,7 @@ func (m *MockController) Process(ctx context.Context, delivery consumer.Delivery } // Process indicates an expected call of Process. -func (mr *MockControllerMockRecorder) Process(ctx, delivery interface{}) *gomock.Call { +func (mr *MockControllerMockRecorder) Process(ctx, delivery any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Process", reflect.TypeOf((*MockController)(nil).Process), ctx, delivery) } diff --git a/runway/extension/BUILD.bazel b/runway/extension/BUILD.bazel new file mode 100644 index 00000000..3832145f --- /dev/null +++ b/runway/extension/BUILD.bazel @@ -0,0 +1,8 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "extension", + srcs = ["extension.go"], + importpath = "github.com/uber/submitqueue/runway/extension", + visibility = ["//visibility:public"], +) diff --git a/runway/extension/extension.go b/runway/extension/extension.go new file mode 100644 index 00000000..310bab4e --- /dev/null +++ b/runway/extension/extension.go @@ -0,0 +1,16 @@ +// Copyright (c) 2025 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 extension holds Runway-specific extension implementations. +package extension diff --git a/runway/extension/vcs/BUILD.bazel b/runway/extension/vcs/BUILD.bazel new file mode 100644 index 00000000..e8e37e16 --- /dev/null +++ b/runway/extension/vcs/BUILD.bazel @@ -0,0 +1,9 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "vcs", + srcs = ["vcs.go"], + importpath = "github.com/uber/submitqueue/runway/extension/vcs", + visibility = ["//visibility:public"], + deps = ["//runway/entity"], +) diff --git a/runway/extension/vcs/mock/BUILD.bazel b/runway/extension/vcs/mock/BUILD.bazel new file mode 100644 index 00000000..e04292b8 --- /dev/null +++ b/runway/extension/vcs/mock/BUILD.bazel @@ -0,0 +1,13 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "mock", + srcs = ["vcs_mock.go"], + importpath = "github.com/uber/submitqueue/runway/extension/vcs/mock", + visibility = ["//visibility:public"], + deps = [ + "//runway/entity", + "//runway/extension/vcs", + "@org_uber_go_mock//gomock", + ], +) diff --git a/runway/extension/vcs/mock/vcs_mock.go b/runway/extension/vcs/mock/vcs_mock.go new file mode 100644 index 00000000..15a20a02 --- /dev/null +++ b/runway/extension/vcs/mock/vcs_mock.go @@ -0,0 +1,112 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: vcs.go +// +// Generated by this command: +// +// mockgen -source=vcs.go -destination=mock/vcs_mock.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + entity "github.com/uber/submitqueue/runway/entity" + vcs "github.com/uber/submitqueue/runway/extension/vcs" + gomock "go.uber.org/mock/gomock" +) + +// MockVCS is a mock of VCS interface. +type MockVCS struct { + ctrl *gomock.Controller + recorder *MockVCSMockRecorder + isgomock struct{} +} + +// MockVCSMockRecorder is the mock recorder for MockVCS. +type MockVCSMockRecorder struct { + mock *MockVCS +} + +// NewMockVCS creates a new mock instance. +func NewMockVCS(ctrl *gomock.Controller) *MockVCS { + mock := &MockVCS{ctrl: ctrl} + mock.recorder = &MockVCSMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockVCS) EXPECT() *MockVCSMockRecorder { + return m.recorder +} + +// CheckMergeability mocks base method. +func (m *MockVCS) CheckMergeability(ctx context.Context, check entity.Check) ([]entity.MergeabilityResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CheckMergeability", ctx, check) + ret0, _ := ret[0].([]entity.MergeabilityResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CheckMergeability indicates an expected call of CheckMergeability. +func (mr *MockVCSMockRecorder) CheckMergeability(ctx, check any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckMergeability", reflect.TypeOf((*MockVCS)(nil).CheckMergeability), ctx, check) +} + +// Land mocks base method. +func (m *MockVCS) Land(ctx context.Context, job entity.Job) ([]entity.Outcome, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Land", ctx, job) + ret0, _ := ret[0].([]entity.Outcome) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Land indicates an expected call of Land. +func (mr *MockVCSMockRecorder) Land(ctx, job any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Land", reflect.TypeOf((*MockVCS)(nil).Land), ctx, job) +} + +// MockFactory is a mock of Factory interface. +type MockFactory struct { + ctrl *gomock.Controller + recorder *MockFactoryMockRecorder + isgomock struct{} +} + +// MockFactoryMockRecorder is the mock recorder for MockFactory. +type MockFactoryMockRecorder struct { + mock *MockFactory +} + +// NewMockFactory creates a new mock instance. +func NewMockFactory(ctrl *gomock.Controller) *MockFactory { + mock := &MockFactory{ctrl: ctrl} + mock.recorder = &MockFactoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockFactory) EXPECT() *MockFactoryMockRecorder { + return m.recorder +} + +// For mocks base method. +func (m *MockFactory) For(cfg vcs.Config) (vcs.VCS, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "For", cfg) + ret0, _ := ret[0].(vcs.VCS) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// For indicates an expected call of For. +func (mr *MockFactoryMockRecorder) For(cfg any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "For", reflect.TypeOf((*MockFactory)(nil).For), cfg) +} diff --git a/runway/extension/vcs/noop/BUILD.bazel b/runway/extension/vcs/noop/BUILD.bazel new file mode 100644 index 00000000..80db04bf --- /dev/null +++ b/runway/extension/vcs/noop/BUILD.bazel @@ -0,0 +1,25 @@ +load("@rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "noop", + srcs = ["noop.go"], + importpath = "github.com/uber/submitqueue/runway/extension/vcs/noop", + visibility = ["//visibility:public"], + deps = [ + "//runway/entity", + "//runway/extension/vcs", + ], +) + +go_test( + name = "noop_test", + srcs = ["noop_test.go"], + embed = [":noop"], + deps = [ + "//entity/change", + "//entity/mergestrategy", + "//runway/entity", + "@com_github_stretchr_testify//assert", + "@com_github_stretchr_testify//require", + ], +) diff --git a/runway/extension/vcs/noop/noop.go b/runway/extension/vcs/noop/noop.go new file mode 100644 index 00000000..5aadb6cf --- /dev/null +++ b/runway/extension/vcs/noop/noop.go @@ -0,0 +1,56 @@ +// Copyright (c) 2025 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package noop provides a VCS implementation that reports every check as +// mergeable and every land as committed with synthetic commit SHAs. It is +// intended for wiring and tests only, never production. +package noop + +import ( + "context" + "fmt" + "sync/atomic" + + "github.com/uber/submitqueue/runway/entity" + "github.com/uber/submitqueue/runway/extension/vcs" +) + +type noopVCS struct { + counter atomic.Uint64 +} + +// New returns a vcs.VCS that defaults to success for all operations. +func New() vcs.VCS { + return &noopVCS{} +} + +func (n *noopVCS) CheckMergeability(_ context.Context, check entity.Check) ([]entity.MergeabilityResult, error) { + results := make([]entity.MergeabilityResult, len(check.Changes)) + for i, c := range check.Changes { + results[i] = entity.MergeabilityResult{Change: c, Mergeable: true} + } + return results, nil +} + +func (n *noopVCS) Land(_ context.Context, job entity.Job) ([]entity.Outcome, error) { + outcomes := make([]entity.Outcome, len(job.Items)) + for i, item := range job.Items { + outcomes[i] = entity.Outcome{ + RequestID: item.RequestID, + Change: item.Change, + CommitSHAs: []string{fmt.Sprintf("noop-%d", n.counter.Add(1))}, + } + } + return outcomes, nil +} diff --git a/runway/extension/vcs/noop/noop_test.go b/runway/extension/vcs/noop/noop_test.go new file mode 100644 index 00000000..6109d29d --- /dev/null +++ b/runway/extension/vcs/noop/noop_test.go @@ -0,0 +1,105 @@ +// Copyright (c) 2025 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package noop + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/uber/submitqueue/entity/change" + "github.com/uber/submitqueue/entity/mergestrategy" + "github.com/uber/submitqueue/runway/entity" +) + +func TestNoopVCS_CheckMergeability(t *testing.T) { + v := New() + check := entity.Check{ + Queue: "q1", + RequestID: "q1/1", + Repo: "uber/repo", + TargetBranch: "main", + Changes: []change.Change{ + {URIs: []string{"github://uber/repo/pull/1/aaaa"}}, + {URIs: []string{"github://uber/repo/pull/2/bbbb"}}, + }, + Strategy: mergestrategy.MergeStrategyRebase, + } + + results, err := v.CheckMergeability(context.Background(), check) + require.NoError(t, err) + require.Len(t, results, 2) + + for _, r := range results { + assert.True(t, r.Mergeable) + assert.Empty(t, r.Reason) + } +} + +func TestNoopVCS_CheckMergeability_Empty(t *testing.T) { + v := New() + check := entity.Check{Changes: []change.Change{}} + + results, err := v.CheckMergeability(context.Background(), check) + require.NoError(t, err) + assert.Empty(t, results) +} + +func TestNoopVCS_Land(t *testing.T) { + v := New() + job := entity.Job{ + ID: "job-1", + BatchID: "q1/batch/1", + Queue: "q1", + Repo: "uber/repo", + TargetBranch: "main", + Items: []entity.JobItem{ + { + RequestID: "q1/10", + Change: change.Change{URIs: []string{"github://uber/repo/pull/10/aaaa"}}, + Strategy: mergestrategy.MergeStrategyRebase, + }, + { + RequestID: "q1/11", + Change: change.Change{URIs: []string{"github://uber/repo/pull/11/bbbb"}}, + Strategy: mergestrategy.MergeStrategySquashRebase, + }, + }, + } + + outcomes, err := v.Land(context.Background(), job) + require.NoError(t, err) + require.Len(t, outcomes, 2) + + assert.Equal(t, "q1/10", outcomes[0].RequestID) + assert.Equal(t, "q1/11", outcomes[1].RequestID) + assert.False(t, outcomes[0].AlreadyExisted) + assert.False(t, outcomes[1].AlreadyExisted) + + // Each outcome has a unique commit SHA. + assert.Len(t, outcomes[0].CommitSHAs, 1) + assert.Len(t, outcomes[1].CommitSHAs, 1) + assert.NotEqual(t, outcomes[0].CommitSHAs[0], outcomes[1].CommitSHAs[0]) +} + +func TestNoopVCS_Land_Empty(t *testing.T) { + v := New() + job := entity.Job{Items: []entity.JobItem{}} + + outcomes, err := v.Land(context.Background(), job) + require.NoError(t, err) + assert.Empty(t, outcomes) +} diff --git a/runway/extension/vcs/vcs.go b/runway/extension/vcs/vcs.go new file mode 100644 index 00000000..92d2ea45 --- /dev/null +++ b/runway/extension/vcs/vcs.go @@ -0,0 +1,62 @@ +// Copyright (c) 2025 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 vcs defines the pluggable VCS extension interface for the runway +// orchestrator. Implementations perform version-control operations (mergeability +// checking, landing, finalization) against a specific VCS backend. +package vcs + +//go:generate mockgen -source=vcs.go -destination=mock/vcs_mock.go -package=mock + +import ( + "context" + "errors" + + "github.com/uber/submitqueue/runway/entity" +) + +// ErrConflict is returned by VCS.Land when one or more changes fail to apply +// cleanly on top of the current tip of the target branch. Callers should treat +// conflicts as non-retryable expected outcomes. +var ErrConflict = errors.New("merge conflict") + +// VCS performs version-control operations for the runway orchestrator. +type VCS interface { + // CheckMergeability performs a read-only trial merge of the check's changes + // against the target branch. Returns per-change mergeability results. The + // result slice has one entry per change in check.Changes. This method does + // not mutate any external state. + CheckMergeability(ctx context.Context, check entity.Check) ([]entity.MergeabilityResult, error) + + // Land applies the job's items to the target branch, pushes, and finalizes + // (e.g., closes PRs). Returns per-item outcomes on success. Returns + // ErrConflict when any item fails to apply cleanly; callers should treat + // ErrConflict as non-retryable. + Land(ctx context.Context, job entity.Job) ([]entity.Outcome, error) +} + +// Config carries the per-invocation identity handed to a Factory. +type Config struct { + // Repo identifies the repository (e.g., "uber/submitqueue"). + Repo string + // TargetBranch identifies the target branch (e.g., "main"). + TargetBranch string +} + +// Factory builds a VCS for a given configuration. Implementations are provided +// by integrators (and tests) and inject whatever they need at construction. +type Factory interface { + // For returns the VCS for the given configuration. + For(cfg Config) (VCS, error) +}