diff --git a/Makefile b/Makefile index 25e5097ed..fdca00055 100644 --- a/Makefile +++ b/Makefile @@ -579,7 +579,7 @@ local-stovepipe-stop: ## Stop the Stovepipe service mocks: ## Generate mock files using mockgen @echo "Generating mocks..." - @$(BAZEL) run @rules_go//go -- generate ./submitqueue/extension/storage/... ./submitqueue/extension/buildrunner/... ./submitqueue/extension/changeprovider/... ./platform/extension/counter/... ./platform/extension/consumergate/... ./platform/extension/hook/... ./platform/extension/messagequeue/... ./submitqueue/extension/queueconfig/... ./submitqueue/extension/mergechecker/... ./submitqueue/extension/conflict/... ./submitqueue/extension/speculation/... ./submitqueue/extension/validator/... ./platform/consumer/... ./stovepipe/core/requestlog/... ./stovepipe/extension/storage/... ./stovepipe/extension/sourcecontrol/... + @$(BAZEL) run @rules_go//go -- generate ./submitqueue/extension/storage/... ./submitqueue/extension/buildrunner/... ./submitqueue/extension/changeprovider/... ./platform/extension/counter/... ./platform/extension/consumergate/... ./platform/extension/hook/... ./platform/extension/messagequeue/... ./submitqueue/extension/queueconfig/... ./submitqueue/extension/conflict/... ./submitqueue/extension/speculation/... ./submitqueue/extension/validator/... ./platform/consumer/... ./stovepipe/core/requestlog/... ./stovepipe/extension/storage/... ./stovepipe/extension/sourcecontrol/... @echo "Mocks generated successfully!" proto: ## Generate protobuf files from .proto definitions diff --git a/submitqueue/entity/BUILD.bazel b/submitqueue/entity/BUILD.bazel index a2dc0a684..419a98a61 100644 --- a/submitqueue/entity/BUILD.bazel +++ b/submitqueue/entity/BUILD.bazel @@ -13,9 +13,7 @@ go_library( "conflict.go", "land.go", "list.go", - "merge_result.go", "path_build.go", - "push_result.go", "queue_batch_state.go", "queue_config.go", "request.go", diff --git a/submitqueue/entity/merge_result.go b/submitqueue/entity/merge_result.go deleted file mode 100644 index ad5078201..000000000 --- a/submitqueue/entity/merge_result.go +++ /dev/null @@ -1,24 +0,0 @@ -// 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 entity - -// MergeResult holds the outcome of a mergeability check. -type MergeResult struct { - // Mergeable is true if the request's changes are expected to merge cleanly. - Mergeable bool - // Reason is a human-readable explanation when Mergeable is false. - // Empty when Mergeable is true. - Reason string -} diff --git a/submitqueue/entity/push_result.go b/submitqueue/entity/push_result.go deleted file mode 100644 index 0b1352c0e..000000000 --- a/submitqueue/entity/push_result.go +++ /dev/null @@ -1,67 +0,0 @@ -// 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 entity - -import "github.com/uber/submitqueue/platform/base/change" - -// OutcomeStatus describes what happened to a single Change during a push. -type OutcomeStatus string - -const ( - // OutcomeStatusUnknown is the unreachable zero value, set by default - // when the structure is initialized. It should never be seen in the system. - OutcomeStatusUnknown OutcomeStatus = "" - // OutcomeStatusCommitted means the change produced one or more commits - // on the target branch. CommitSHAs lists those commits in apply order. - OutcomeStatusCommitted OutcomeStatus = "committed" - // OutcomeStatusAlreadyExisted means the change produced no commits - // because every part of it is already present in the target branch - // (e.g. it previously landed via another path, or a prior change in - // the same push subsumed it). CommitSHAs is empty for this status. - // In git terms this is what a `cherry-pick` surfaces as "rebased out". - OutcomeStatusAlreadyExisted OutcomeStatus = "already_existed" -) - -// ChangeOutcome describes what happened to a single Change inside a push. -type ChangeOutcome struct { - // Change is the input change this outcome corresponds to. - Change change.Change - // Status describes whether the change produced commits or was already - // present on the target branch. - Status OutcomeStatus - // CommitSHAs lists the commits this change produced on the target - // branch, in apply order. A single Change may produce multiple commits - // (e.g. a stack of PRs). Empty when Status is OutcomeStatusAlreadyExisted. - CommitSHAs []string -} - -// BatchOutcome groups the per-change outcomes for a single pushed batch, so a -// merge-train push (several batches in one call) stays correlatable back to the -// batch each change belonged to. There is no per-batch status: a push is -// all-or-nothing across the whole call, so a per-batch pass/fail would be -// uniformly redundant. -type BatchOutcome struct { - // BatchID is the input batch this outcome corresponds to. - BatchID string - // Outcomes is one entry per change in the batch, in apply order. - Outcomes []ChangeOutcome -} - -// PushResult is the outcome of a successful push. -type PushResult struct { - // Batches is one entry per pushed batch, in the same order as the batches - // passed to the push. The slice length equals the input length. - Batches []BatchOutcome -} diff --git a/submitqueue/extension/mergechecker/BUILD.bazel b/submitqueue/extension/mergechecker/BUILD.bazel deleted file mode 100644 index 23ecace72..000000000 --- a/submitqueue/extension/mergechecker/BUILD.bazel +++ /dev/null @@ -1,9 +0,0 @@ -load("@rules_go//go:def.bzl", "go_library") - -go_library( - name = "go_default_library", - srcs = ["mergechecker.go"], - importpath = "github.com/uber/submitqueue/submitqueue/extension/mergechecker", - visibility = ["//visibility:public"], - deps = ["//submitqueue/entity:go_default_library"], -) diff --git a/submitqueue/extension/mergechecker/fake/BUILD.bazel b/submitqueue/extension/mergechecker/fake/BUILD.bazel deleted file mode 100644 index d1c85e0ce..000000000 --- a/submitqueue/extension/mergechecker/fake/BUILD.bazel +++ /dev/null @@ -1,26 +0,0 @@ -load("@rules_go//go:def.bzl", "go_library", "go_test") - -go_library( - name = "go_default_library", - srcs = ["fake.go"], - importpath = "github.com/uber/submitqueue/submitqueue/extension/mergechecker/fake", - visibility = ["//visibility:public"], - deps = [ - "//platform/fakemarker:go_default_library", - "//submitqueue/entity:go_default_library", - "//submitqueue/extension/mergechecker:go_default_library", - ], -) - -go_test( - name = "go_default_test", - srcs = ["fake_test.go"], - embed = [":go_default_library"], - deps = [ - "//platform/base/change:go_default_library", - "//submitqueue/entity:go_default_library", - "//submitqueue/extension/mergechecker:go_default_library", - "@com_github_stretchr_testify//assert:go_default_library", - "@com_github_stretchr_testify//require:go_default_library", - ], -) diff --git a/submitqueue/extension/mergechecker/fake/fake.go b/submitqueue/extension/mergechecker/fake/fake.go deleted file mode 100644 index effffc5dd..000000000 --- a/submitqueue/extension/mergechecker/fake/fake.go +++ /dev/null @@ -1,68 +0,0 @@ -// 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 fake provides a mergechecker.MergeChecker whose outcome is driven by -// the input change. With no marker it reports every change as mergeable, -// behaving as a best-case stub for wiring and baselines. A failure can be -// injected end-to-end (e.g. from an e2e land request) by embedding a marker -// token in a change URI of the form "sq-fake=": -// -// sq-fake=unmergeable -> Result{Mergeable: false} -// sq-fake=mergecheck-error -> non-nil error -// -// This lets a single running stack exercise negative paths purely by varying -// request payloads. It is intended for examples and tests only, never -// production. -package fake - -import ( - "context" - "fmt" - - "github.com/uber/submitqueue/platform/fakemarker" - "github.com/uber/submitqueue/submitqueue/entity" - "github.com/uber/submitqueue/submitqueue/extension/mergechecker" -) - -// Recognized marker tokens. See the package doc for the convention. -const ( - tokenUnmergeable = "unmergeable" - tokenError = "mergecheck-error" -) - -// checker is a mergechecker.MergeChecker that reports changes as mergeable -// unless a marker token in a change URI requests otherwise. -type checker struct { - // cfg is the per-queue identity this checker was built for. - cfg mergechecker.Config -} - -// New returns a mergechecker.MergeChecker bound to the queue named in cfg that -// defaults to mergeable and honors marker tokens embedded in change URIs. -func New(cfg mergechecker.Config) mergechecker.MergeChecker { - return checker{cfg: cfg} -} - -// Check reports the change as mergeable unless a recognized marker token is -// present in one of the request's change URIs. -func (checker) Check(_ context.Context, request entity.Request) (entity.MergeResult, error) { - switch fakemarker.Token(request.Change.URIs) { - case tokenUnmergeable: - return entity.MergeResult{Mergeable: false, Reason: "fake: marked unmergeable"}, nil - case tokenError: - return entity.MergeResult{}, fmt.Errorf("fake: marked merge-check error") - default: - return entity.MergeResult{Mergeable: true}, nil - } -} diff --git a/submitqueue/extension/mergechecker/fake/fake_test.go b/submitqueue/extension/mergechecker/fake/fake_test.go deleted file mode 100644 index 5fb833d41..000000000 --- a/submitqueue/extension/mergechecker/fake/fake_test.go +++ /dev/null @@ -1,82 +0,0 @@ -// 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 fake - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/uber/submitqueue/platform/base/change" - "github.com/uber/submitqueue/submitqueue/entity" - "github.com/uber/submitqueue/submitqueue/extension/mergechecker" -) - -// testCfg is the per-queue identity used by every case in this file. -var testCfg = mergechecker.Config{QueueName: "test-queue"} - -func TestNew_ImplementsInterface(t *testing.T) { - var _ mergechecker.MergeChecker = New(testCfg) -} - -func TestChecker_Check(t *testing.T) { - tests := []struct { - name string - uris []string - wantMergeable bool - wantErr bool - }{ - { - name: "no marker is mergeable", - uris: []string{"github://github.example.com/owner/repo/pull/1/abc"}, - wantMergeable: true, - }, - { - name: "no URIs is mergeable", - uris: nil, - wantMergeable: true, - }, - { - name: "unmergeable marker", - uris: []string{"github://github.example.com/owner/repo/pull/1/abc?sq-fake=unmergeable"}, - }, - { - name: "error marker", - uris: []string{"github://github.example.com/owner/repo/pull/1/abc?sq-fake=mergecheck-error"}, - wantErr: true, - }, - { - name: "marker on second uri", - uris: []string{ - "github://github.example.com/owner/repo/pull/1/abc", - "github://github.example.com/owner/repo/pull/2/def?sq-fake=unmergeable", - }, - }, - } - - c := New(testCfg) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - res, err := c.Check(context.Background(), entity.Request{Change: change.Change{URIs: tt.uris}}) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - assert.Equal(t, tt.wantMergeable, res.Mergeable) - }) - } -} diff --git a/submitqueue/extension/mergechecker/github/BUILD.bazel b/submitqueue/extension/mergechecker/github/BUILD.bazel deleted file mode 100644 index faf87e605..000000000 --- a/submitqueue/extension/mergechecker/github/BUILD.bazel +++ /dev/null @@ -1,41 +0,0 @@ -load("@rules_go//go:def.bzl", "go_library", "go_test") - -go_library( - name = "go_default_library", - srcs = [ - "checker.go", - "graphql.go", - "validate.go", - ], - importpath = "github.com/uber/submitqueue/submitqueue/extension/mergechecker/github", - visibility = ["//visibility:public"], - deps = [ - "//platform/base/change/github:go_default_library", - "//platform/metrics:go_default_library", - "//submitqueue/entity:go_default_library", - "//submitqueue/extension/mergechecker:go_default_library", - "@com_github_uber_go_tally//:go_default_library", - "@org_uber_go_zap//:go_default_library", - ], -) - -go_test( - name = "go_default_test", - srcs = [ - "checker_test.go", - "graphql_test.go", - "validate_test.go", - ], - embed = [":go_default_library"], - deps = [ - "//platform/base/change:go_default_library", - "//platform/base/change/github:go_default_library", - "//platform/http:go_default_library", - "//submitqueue/entity:go_default_library", - "//submitqueue/extension/mergechecker:go_default_library", - "@com_github_stretchr_testify//assert:go_default_library", - "@com_github_stretchr_testify//require:go_default_library", - "@com_github_uber_go_tally//:go_default_library", - "@org_uber_go_zap//zaptest:go_default_library", - ], -) diff --git a/submitqueue/extension/mergechecker/github/checker.go b/submitqueue/extension/mergechecker/github/checker.go deleted file mode 100644 index 7d01c1063..000000000 --- a/submitqueue/extension/mergechecker/github/checker.go +++ /dev/null @@ -1,151 +0,0 @@ -// 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 github - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - - "github.com/uber-go/tally" - entitygithub "github.com/uber/submitqueue/platform/base/change/github" - "github.com/uber/submitqueue/platform/metrics" - "github.com/uber/submitqueue/submitqueue/entity" - "github.com/uber/submitqueue/submitqueue/extension/mergechecker" - "go.uber.org/zap" -) - -// Params holds the dependencies for the GitHub MergeChecker. -type Params struct { - // Config is the per-queue identity handed to the Factory that built this - // merge checker. - Config mergechecker.Config - // HTTPClient is a pre-configured HTTP client. The caller is responsible for - // configuring the base URL (via BaseURLTransport) and auth (via a transport layer). - HTTPClient *http.Client - // Logger is the structured logger. - Logger *zap.SugaredLogger - // MetricsScope is the metrics scope for instrumentation. - MetricsScope tally.Scope -} - -// mergeChecker implements the mergechecker.MergeChecker interface using the GitHub GraphQL API. -type mergeChecker struct { - // cfg is the per-queue identity this checker was built for. - cfg mergechecker.Config - httpClient *http.Client - logger *zap.SugaredLogger - metricsScope tally.Scope -} - -// Verify mergeChecker implements mergechecker.MergeChecker at compile time. -var _ mergechecker.MergeChecker = (*mergeChecker)(nil) - -// NewMergeChecker creates a new GitHub MergeChecker bound to the queue named in -// params.Config. -func NewMergeChecker(params Params) mergechecker.MergeChecker { - return &mergeChecker{ - cfg: params.Config, - httpClient: params.HTTPClient, - logger: params.Logger.Named("github_mergechecker"), - metricsScope: params.MetricsScope.SubScope("github_mergechecker"), - } -} - -// Check assesses whether a request's change can merge cleanly using the GitHub GraphQL API. -func (c *mergeChecker) Check(ctx context.Context, request entity.Request) (result entity.MergeResult, retErr error) { - const opName = "check" - - op := metrics.Begin(c.metricsScope, opName, metrics.LongLatencyBuckets) - defer func() { op.Complete(retErr) }() - - change := request.Change - - // Parse all change IDs - // TODO: classify parse errors as user errors (non-retryable) vs system errors. - changeIDs := make([]entitygithub.ChangeID, 0, len(change.URIs)) - for _, rawID := range change.URIs { - cid, err := entitygithub.ParseChangeID(rawID) - if err != nil { - metrics.NamedCounter(c.metricsScope, opName, "parse_errors", 1) - return result, fmt.Errorf("failed to parse change ID %q: %w", rawID, err) - } - changeIDs = append(changeIDs, cid) - } - - // Fetch PR info from GitHub GraphQL API - prInfoMap, err := c.fetchPRInfo(ctx, changeIDs) - if err != nil { - metrics.NamedCounter(c.metricsScope, opName, "graphql_errors", 1) - return result, fmt.Errorf("failed to fetch PR info: %w", err) - } - - // Validate PR mergeability - mergeable, reason, err := validatePRs(changeIDs, prInfoMap) - if err != nil { - metrics.NamedCounter(c.metricsScope, opName, "validation_errors", 1) - return result, err - } - - if !mergeable { - metrics.NamedCounter(c.metricsScope, opName, "not_mergeable", 1) - c.logger.Infow("change not mergeable", - "reason", reason, - "change_uris", change.URIs, - ) - } else { - metrics.NamedCounter(c.metricsScope, opName, "mergeable", 1) - } - - result.Mergeable = mergeable - result.Reason = reason - return result, nil -} - -// fetchPRInfo executes a batched GraphQL query to fetch PR info for all change IDs. -func (c *mergeChecker) fetchPRInfo(ctx context.Context, changeIDs []entitygithub.ChangeID) (map[int]PRInfo, error) { - query := buildGraphQLQuery(changeIDs) - - reqBody, err := json.Marshal(graphQLRequest{Query: query}) - if err != nil { - return nil, fmt.Errorf("failed to marshal graphql request: %w", err) - } - - httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, "/graphql", bytes.NewReader(reqBody)) - if err != nil { - return nil, fmt.Errorf("failed to create http request: %w", err) - } - httpReq.Header.Set("Content-Type", "application/json") - - resp, err := c.httpClient.Do(httpReq) - if err != nil { - return nil, fmt.Errorf("graphql request failed: %w", err) - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read graphql response: %w", err) - } - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("graphql request returned status %d: %s", resp.StatusCode, string(body)) - } - - return parseGraphQLResponse(body, changeIDs) -} diff --git a/submitqueue/extension/mergechecker/github/checker_test.go b/submitqueue/extension/mergechecker/github/checker_test.go deleted file mode 100644 index e38f1b389..000000000 --- a/submitqueue/extension/mergechecker/github/checker_test.go +++ /dev/null @@ -1,170 +0,0 @@ -// 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 github - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/uber-go/tally" - "github.com/uber/submitqueue/platform/base/change" - phttp "github.com/uber/submitqueue/platform/http" - "github.com/uber/submitqueue/submitqueue/entity" - "github.com/uber/submitqueue/submitqueue/extension/mergechecker" - "go.uber.org/zap/zaptest" -) - -func newTestMergeChecker(t *testing.T, serverURL string) mergechecker.MergeChecker { - t.Helper() - client, err := phttp.NewClient(serverURL) - require.NoError(t, err) - return NewMergeChecker(Params{ - HTTPClient: client, - Logger: zaptest.NewLogger(t).Sugar(), - MetricsScope: tally.NoopScope, - }) -} - -// Sample 40-char lowercase hex SHAs used across the test cases. -const ( - sha1Full = "1111111111111111111111111111111111111111" - sha2Full = "2222222222222222222222222222222222222222" - shaAFull = "abcdef0123456789abcdef0123456789abcdef01" - shaOldFull = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" - shaNewFull = "feedfacefeedfacefeedfacefeedfacefeedface" -) - -func graphQLHandler(t *testing.T, prInfos []PRInfo) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - t.Helper() - - data := make(map[string]json.RawMessage, len(prInfos)) - for i, pr := range prInfos { - alias := fmt.Sprintf("pr%d", i) - prJSON, err := json.Marshal(map[string]any{ - "pullRequest": map[string]any{ - "number": pr.Number, - "mergeable": string(pr.Mergeable), - "baseRefName": pr.BaseRefName, - "headRefName": pr.HeadRefName, - "headRefOid": pr.HeadRefOid, - "state": string(pr.State), - }, - }) - require.NoError(t, err) - data[alias] = json.RawMessage(prJSON) - } - - resp := graphQLResponse{Data: data} - w.Header().Set("Content-Type", "application/json") - err := json.NewEncoder(w).Encode(resp) - require.NoError(t, err) - } -} - -func TestMergeChecker_Check(t *testing.T) { - tests := []struct { - name string - handler http.HandlerFunc - change change.Change - wantMergeable bool - wantReason string - wantErr bool - }{ - { - name: "single PR mergeable", - handler: graphQLHandler(t, []PRInfo{ - {Number: 1, Mergeable: PRMergeableStateMergeable, BaseRefName: "main", HeadRefName: "feature-1", HeadRefOid: shaAFull, State: PRStateOpen}, - }), - change: change.Change{URIs: []string{"github://github.example.com/uber/repo/pull/1/" + shaAFull}}, - wantMergeable: true, - }, - { - name: "single PR conflicting", - handler: graphQLHandler(t, []PRInfo{ - {Number: 1, Mergeable: PRMergeableStateConflicting, BaseRefName: "main", HeadRefName: "feature-1", HeadRefOid: shaAFull, State: PRStateOpen}, - }), - change: change.Change{URIs: []string{"github://github.example.com/uber/repo/pull/1/" + shaAFull}}, - wantMergeable: false, - wantReason: "PR #1 has merge conflicts", - }, - { - name: "stack of two PRs mergeable", - handler: graphQLHandler(t, []PRInfo{ - {Number: 1, Mergeable: PRMergeableStateMergeable, BaseRefName: "main", HeadRefName: "feature-1", HeadRefOid: sha1Full, State: PRStateOpen}, - {Number: 2, Mergeable: PRMergeableStateMergeable, BaseRefName: "feature-1", HeadRefName: "feature-2", HeadRefOid: sha2Full, State: PRStateOpen}, - }), - change: change.Change{URIs: []string{"github://github.example.com/uber/repo/pull/1/" + sha1Full, "github://github.example.com/uber/repo/pull/2/" + sha2Full}}, - wantMergeable: true, - }, - { - name: "unknown mergeability returns error", - handler: graphQLHandler(t, []PRInfo{ - {Number: 1, Mergeable: PRMergeableStateUnknown, BaseRefName: "main", HeadRefName: "feature-1", HeadRefOid: shaAFull, State: PRStateOpen}, - }), - change: change.Change{URIs: []string{"github://github.example.com/uber/repo/pull/1/" + shaAFull}}, - wantErr: true, - }, - { - name: "stale SHA not mergeable", - handler: graphQLHandler(t, []PRInfo{ - {Number: 1, Mergeable: PRMergeableStateMergeable, BaseRefName: "main", HeadRefName: "feature-1", HeadRefOid: shaNewFull, State: PRStateOpen}, - }), - change: change.Change{URIs: []string{"github://github.example.com/uber/repo/pull/1/" + shaOldFull}}, - wantMergeable: false, - wantReason: fmt.Sprintf("PR #1 head SHA changed: expected %s, got %s", shaOldFull, shaNewFull), - }, - { - name: "invalid change ID", - handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - t.Fatal("should not reach server") - }), - change: change.Change{URIs: []string{"invalid-change-id"}}, - wantErr: true, - }, - { - name: "server error", - handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte("internal server error")) - }), - change: change.Change{URIs: []string{"github://github.example.com/uber/repo/pull/1/" + shaAFull}}, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - server := httptest.NewServer(tt.handler) - defer server.Close() - - mc := newTestMergeChecker(t, server.URL) - result, err := mc.Check(context.Background(), entity.Request{Change: tt.change}) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - assert.Equal(t, tt.wantMergeable, result.Mergeable) - assert.Equal(t, tt.wantReason, result.Reason) - }) - } -} diff --git a/submitqueue/extension/mergechecker/github/graphql.go b/submitqueue/extension/mergechecker/github/graphql.go deleted file mode 100644 index 1c33c8065..000000000 --- a/submitqueue/extension/mergechecker/github/graphql.go +++ /dev/null @@ -1,131 +0,0 @@ -// 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 github - -import ( - "encoding/json" - "fmt" - "strings" - - entitygithub "github.com/uber/submitqueue/platform/base/change/github" -) - -// graphQLRequest is the request body for the GitHub GraphQL API. -type graphQLRequest struct { - // Query is the GraphQL query string. - Query string `json:"query"` -} - -// graphQLResponse is the top-level response from the GitHub GraphQL API. -type graphQLResponse struct { - // Data contains the query results keyed by alias. - Data map[string]json.RawMessage `json:"data"` - // Errors contains any GraphQL errors. - Errors []graphQLError `json:"errors"` -} - -// graphQLError represents a single GraphQL error. -type graphQLError struct { - // Message is the error message. - Message string `json:"message"` -} - -// repositoryResponse represents a repository query result. -type repositoryResponse struct { - // PullRequest contains the PR data. - PullRequest prResponse `json:"pullRequest"` -} - -// prResponse represents the fields fetched for a single pull request. -type prResponse struct { - // Number is the PR number. - Number int `json:"number"` - // Mergeable is the mergeability state. - Mergeable string `json:"mergeable"` - // BaseRefName is the base branch name. - BaseRefName string `json:"baseRefName"` - // HeadRefName is the head branch name. - HeadRefName string `json:"headRefName"` - // HeadRefOid is the head commit SHA. - HeadRefOid string `json:"headRefOid"` - // State is the PR state (OPEN, CLOSED, MERGED). - State string `json:"state"` -} - -// buildGraphQLQuery builds a batched GraphQL query for multiple PRs. -// Each PR gets an alias like "pr0", "pr1", etc. -func buildGraphQLQuery(changeIDs []entitygithub.ChangeID) string { - var sb strings.Builder - sb.WriteString("query {") - - for i, cid := range changeIDs { - fmt.Fprintf(&sb, ` - pr%d: repository(owner: %q, name: %q) { - pullRequest(number: %d) { - number - mergeable - baseRefName - headRefName - headRefOid - state - } - }`, i, cid.Org, cid.Repo, cid.PRNumber) - } - - sb.WriteString("\n}") - return sb.String() -} - -// parseGraphQLResponse parses the GraphQL response body and returns a map of PR number to PRInfo. -func parseGraphQLResponse(body []byte, changeIDs []entitygithub.ChangeID) (map[int]PRInfo, error) { - var resp graphQLResponse - if err := json.Unmarshal(body, &resp); err != nil { - return nil, fmt.Errorf("failed to parse GraphQL response: %w", err) - } - - if len(resp.Errors) > 0 { - messages := make([]string, len(resp.Errors)) - for i, e := range resp.Errors { - messages[i] = e.Message - } - return nil, fmt.Errorf("GraphQL errors: %s", strings.Join(messages, "; ")) - } - - result := make(map[int]PRInfo, len(changeIDs)) - for i := range changeIDs { - alias := fmt.Sprintf("pr%d", i) - raw, ok := resp.Data[alias] - if !ok { - return nil, fmt.Errorf("missing alias %q in GraphQL response", alias) - } - - var repoResp repositoryResponse - if err := json.Unmarshal(raw, &repoResp); err != nil { - return nil, fmt.Errorf("failed to parse alias %q: %w", alias, err) - } - - pr := repoResp.PullRequest - result[pr.Number] = PRInfo{ - Number: pr.Number, - Mergeable: PRMergeableState(pr.Mergeable), - BaseRefName: pr.BaseRefName, - HeadRefName: pr.HeadRefName, - HeadRefOid: pr.HeadRefOid, - State: PRState(pr.State), - } - } - - return result, nil -} diff --git a/submitqueue/extension/mergechecker/github/graphql_test.go b/submitqueue/extension/mergechecker/github/graphql_test.go deleted file mode 100644 index 909114fc9..000000000 --- a/submitqueue/extension/mergechecker/github/graphql_test.go +++ /dev/null @@ -1,141 +0,0 @@ -// 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 github - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - entitygithub "github.com/uber/submitqueue/platform/base/change/github" -) - -func TestBuildGraphQLQuery(t *testing.T) { - tests := []struct { - name string - changeIDs []entitygithub.ChangeID - wantParts []string - }{ - { - name: "single PR", - changeIDs: []entitygithub.ChangeID{ - {Scheme: "github", Org: "uber", Repo: "submitqueue", PRNumber: 42, HeadCommitSHA: "abc123"}, - }, - wantParts: []string{ - "query {", - `pr0: repository(owner: "uber", name: "submitqueue")`, - "pullRequest(number: 42)", - "number", "mergeable", "baseRefName", "headRefName", "headRefOid", "state", - }, - }, - { - name: "multiple PRs across repos", - changeIDs: []entitygithub.ChangeID{ - {Scheme: "github", Org: "uber", Repo: "repo", PRNumber: 1, HeadCommitSHA: "sha1"}, - {Scheme: "github", Org: "uber", Repo: "repo", PRNumber: 2, HeadCommitSHA: "sha2"}, - {Scheme: "github", Org: "corp", Repo: "app", PRNumber: 99, HeadCommitSHA: "sha99"}, - }, - wantParts: []string{ - `pr0: repository(owner: "uber", name: "repo")`, - "pullRequest(number: 1)", - `pr1: repository(owner: "uber", name: "repo")`, - "pullRequest(number: 2)", - `pr2: repository(owner: "corp", name: "app")`, - "pullRequest(number: 99)", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - query := buildGraphQLQuery(tt.changeIDs) - for _, part := range tt.wantParts { - assert.Contains(t, query, part) - } - }) - } -} - -func TestParseGraphQLResponse(t *testing.T) { - tests := []struct { - name string - body string - changeIDs []entitygithub.ChangeID - want map[int]PRInfo - wantErr bool - }{ - { - name: "success with two PRs", - changeIDs: []entitygithub.ChangeID{ - {Scheme: "github", Org: "uber", Repo: "repo", PRNumber: 1, HeadCommitSHA: "sha1"}, - {Scheme: "github", Org: "uber", Repo: "repo", PRNumber: 2, HeadCommitSHA: "sha2"}, - }, - body: mustMarshalGraphQLResponse(t, map[string]json.RawMessage{ - "pr0": json.RawMessage(`{"pullRequest":{"number":1,"mergeable":"MERGEABLE","baseRefName":"main","headRefName":"feature-1","headRefOid":"sha1","state":"OPEN"}}`), - "pr1": json.RawMessage(`{"pullRequest":{"number":2,"mergeable":"CONFLICTING","baseRefName":"feature-1","headRefName":"feature-2","headRefOid":"sha2","state":"OPEN"}}`), - }), - want: map[int]PRInfo{ - 1: {Number: 1, Mergeable: PRMergeableStateMergeable, BaseRefName: "main", HeadRefName: "feature-1", HeadRefOid: "sha1", State: PRStateOpen}, - 2: {Number: 2, Mergeable: PRMergeableStateConflicting, BaseRefName: "feature-1", HeadRefName: "feature-2", HeadRefOid: "sha2", State: PRStateOpen}, - }, - }, - { - name: "GraphQL errors", - changeIDs: []entitygithub.ChangeID{ - {Scheme: "github", Org: "uber", Repo: "repo", PRNumber: 1, HeadCommitSHA: "sha1"}, - }, - body: `{"data":null,"errors":[{"message":"Not Found"},{"message":"Forbidden"}]}`, - wantErr: true, - }, - { - name: "invalid JSON", - changeIDs: []entitygithub.ChangeID{ - {Scheme: "github", Org: "uber", Repo: "repo", PRNumber: 1, HeadCommitSHA: "sha1"}, - }, - body: `invalid`, - wantErr: true, - }, - { - name: "missing alias", - changeIDs: []entitygithub.ChangeID{ - {Scheme: "github", Org: "uber", Repo: "repo", PRNumber: 1, HeadCommitSHA: "sha1"}, - }, - body: `{"data":{}}`, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := parseGraphQLResponse([]byte(tt.body), tt.changeIDs) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - assert.Equal(t, tt.want, result) - }) - } -} - -// mustMarshalGraphQLResponse is a test helper to build a GraphQL response body. -func mustMarshalGraphQLResponse(t *testing.T, data map[string]json.RawMessage) string { - t.Helper() - resp := graphQLResponse{Data: data} - body, err := json.Marshal(resp) - require.NoError(t, err) - return string(body) -} diff --git a/submitqueue/extension/mergechecker/github/validate.go b/submitqueue/extension/mergechecker/github/validate.go deleted file mode 100644 index a5163b76b..000000000 --- a/submitqueue/extension/mergechecker/github/validate.go +++ /dev/null @@ -1,101 +0,0 @@ -// 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 github - -import ( - "fmt" - - entitygithub "github.com/uber/submitqueue/platform/base/change/github" -) - -// PRMergeableState represents the mergeability state of a pull request. -type PRMergeableState string - -const ( - // PRMergeableStateMergeable indicates the PR can be merged cleanly. - PRMergeableStateMergeable PRMergeableState = "MERGEABLE" - // PRMergeableStateConflicting indicates the PR has merge conflicts. - PRMergeableStateConflicting PRMergeableState = "CONFLICTING" - // PRMergeableStateUnknown indicates GitHub hasn't computed mergeability yet. - // GitHub computes mergeability asynchronously after pushes. The GraphQL API - // returns UNKNOWN when the computation hasn't finished, even though the API - // call itself is synchronous. Callers should retry after a short delay. - PRMergeableStateUnknown PRMergeableState = "UNKNOWN" -) - -// PRState represents the state of a pull request. -type PRState string - -const ( - // PRStateOpen indicates the PR is open. - PRStateOpen PRState = "OPEN" - // PRStateClosed indicates the PR is closed. - PRStateClosed PRState = "CLOSED" - // PRStateMerged indicates the PR has been merged. - PRStateMerged PRState = "MERGED" -) - -// PRInfo holds the relevant pull request information fetched from GitHub. -type PRInfo struct { - // Number is the pull request number. - Number int - // Mergeable is the mergeability state of the PR. - Mergeable PRMergeableState - // BaseRefName is the base branch the PR targets. - BaseRefName string - // HeadRefName is the head branch of the PR. - HeadRefName string - // HeadRefOid is the current head commit SHA of the PR. - HeadRefOid string - // State is the current state of the PR (OPEN, CLOSED, MERGED). - State PRState -} - -// validatePRs validates that all PRs are open, individually mergeable, and not stale. -// Returns (true, "", nil) if all PRs pass validation. -// Returns (false, reason, nil) if definitively not mergeable (conflicts, closed, stale SHA). -// Returns (false, "", error) if mergeability is UNKNOWN (retryable — GitHub hasn't computed yet). -func validatePRs(changeIDs []entitygithub.ChangeID, prInfoMap map[int]PRInfo) (bool, string, error) { - for _, cid := range changeIDs { - pr, ok := prInfoMap[cid.PRNumber] - if !ok { - return false, "", fmt.Errorf("PR #%d not found in API response", cid.PRNumber) - } - - // Check PR is open - if pr.State != PRStateOpen { - return false, fmt.Sprintf("PR #%d is %s", cid.PRNumber, pr.State), nil - } - - // Check mergeability - switch pr.Mergeable { - case PRMergeableStateConflicting: - return false, fmt.Sprintf("PR #%d has merge conflicts", cid.PRNumber), nil - case PRMergeableStateUnknown: - return false, "", fmt.Errorf("mergeability unknown for PR #%d, retry later", cid.PRNumber) - case PRMergeableStateMergeable: - // OK, continue - default: - return false, "", fmt.Errorf("unexpected mergeable state %q for PR #%d", pr.Mergeable, cid.PRNumber) - } - - // Check head commit SHA matches (staleness check) - if pr.HeadRefOid != cid.HeadCommitSHA { - return false, fmt.Sprintf("PR #%d head SHA changed: expected %s, got %s", cid.PRNumber, cid.HeadCommitSHA, pr.HeadRefOid), nil - } - } - - return true, "", nil -} diff --git a/submitqueue/extension/mergechecker/github/validate_test.go b/submitqueue/extension/mergechecker/github/validate_test.go deleted file mode 100644 index efe622558..000000000 --- a/submitqueue/extension/mergechecker/github/validate_test.go +++ /dev/null @@ -1,149 +0,0 @@ -// 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 github - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - entitygithub "github.com/uber/submitqueue/platform/base/change/github" -) - -func TestValidatePRs(t *testing.T) { - tests := []struct { - name string - changeIDs []entitygithub.ChangeID - prInfoMap map[int]PRInfo - wantOK bool - wantReason string - wantErr bool - }{ - { - name: "single PR mergeable", - changeIDs: []entitygithub.ChangeID{ - {Scheme: "github", Org: "uber", Repo: "repo", PRNumber: 1, HeadCommitSHA: "abc123"}, - }, - prInfoMap: map[int]PRInfo{ - 1: {Number: 1, Mergeable: PRMergeableStateMergeable, BaseRefName: "main", HeadRefName: "feature-1", HeadRefOid: "abc123", State: PRStateOpen}, - }, - wantOK: true, - }, - { - name: "stack of three PRs all mergeable", - changeIDs: []entitygithub.ChangeID{ - {Scheme: "github", Org: "uber", Repo: "repo", PRNumber: 1, HeadCommitSHA: "sha1"}, - {Scheme: "github", Org: "uber", Repo: "repo", PRNumber: 2, HeadCommitSHA: "sha2"}, - {Scheme: "github", Org: "uber", Repo: "repo", PRNumber: 3, HeadCommitSHA: "sha3"}, - }, - prInfoMap: map[int]PRInfo{ - 1: {Number: 1, Mergeable: PRMergeableStateMergeable, BaseRefName: "main", HeadRefName: "feature-1", HeadRefOid: "sha1", State: PRStateOpen}, - 2: {Number: 2, Mergeable: PRMergeableStateMergeable, BaseRefName: "feature-1", HeadRefName: "feature-2", HeadRefOid: "sha2", State: PRStateOpen}, - 3: {Number: 3, Mergeable: PRMergeableStateMergeable, BaseRefName: "feature-2", HeadRefName: "feature-3", HeadRefOid: "sha3", State: PRStateOpen}, - }, - wantOK: true, - }, - { - name: "PR closed", - changeIDs: []entitygithub.ChangeID{ - {Scheme: "github", Org: "uber", Repo: "repo", PRNumber: 1, HeadCommitSHA: "sha1"}, - }, - prInfoMap: map[int]PRInfo{ - 1: {Number: 1, Mergeable: PRMergeableStateMergeable, BaseRefName: "main", HeadRefName: "feature-1", HeadRefOid: "sha1", State: PRStateClosed}, - }, - wantOK: false, - wantReason: "PR #1 is CLOSED", - }, - { - name: "PR already merged", - changeIDs: []entitygithub.ChangeID{ - {Scheme: "github", Org: "uber", Repo: "repo", PRNumber: 1, HeadCommitSHA: "sha1"}, - }, - prInfoMap: map[int]PRInfo{ - 1: {Number: 1, Mergeable: PRMergeableStateMergeable, BaseRefName: "main", HeadRefName: "feature-1", HeadRefOid: "sha1", State: PRStateMerged}, - }, - wantOK: false, - wantReason: "PR #1 is MERGED", - }, - { - name: "PR has conflicts", - changeIDs: []entitygithub.ChangeID{ - {Scheme: "github", Org: "uber", Repo: "repo", PRNumber: 1, HeadCommitSHA: "sha1"}, - }, - prInfoMap: map[int]PRInfo{ - 1: {Number: 1, Mergeable: PRMergeableStateConflicting, BaseRefName: "main", HeadRefName: "feature-1", HeadRefOid: "sha1", State: PRStateOpen}, - }, - wantOK: false, - wantReason: "PR #1 has merge conflicts", - }, - { - name: "unknown mergeability returns error", - changeIDs: []entitygithub.ChangeID{ - {Scheme: "github", Org: "uber", Repo: "repo", PRNumber: 1, HeadCommitSHA: "sha1"}, - }, - prInfoMap: map[int]PRInfo{ - 1: {Number: 1, Mergeable: PRMergeableStateUnknown, BaseRefName: "main", HeadRefName: "feature-1", HeadRefOid: "sha1", State: PRStateOpen}, - }, - wantOK: false, - wantErr: true, - }, - { - name: "stale SHA", - changeIDs: []entitygithub.ChangeID{ - {Scheme: "github", Org: "uber", Repo: "repo", PRNumber: 1, HeadCommitSHA: "old_sha"}, - }, - prInfoMap: map[int]PRInfo{ - 1: {Number: 1, Mergeable: PRMergeableStateMergeable, BaseRefName: "main", HeadRefName: "feature-1", HeadRefOid: "new_sha", State: PRStateOpen}, - }, - wantOK: false, - wantReason: "PR #1 head SHA changed: expected old_sha, got new_sha", - }, - { - name: "PR not found in map", - changeIDs: []entitygithub.ChangeID{ - {Scheme: "github", Org: "uber", Repo: "repo", PRNumber: 999, HeadCommitSHA: "sha1"}, - }, - prInfoMap: map[int]PRInfo{}, - wantOK: false, - wantErr: true, - }, - { - name: "second PR in stack conflicting", - changeIDs: []entitygithub.ChangeID{ - {Scheme: "github", Org: "uber", Repo: "repo", PRNumber: 1, HeadCommitSHA: "sha1"}, - {Scheme: "github", Org: "uber", Repo: "repo", PRNumber: 2, HeadCommitSHA: "sha2"}, - }, - prInfoMap: map[int]PRInfo{ - 1: {Number: 1, Mergeable: PRMergeableStateMergeable, BaseRefName: "main", HeadRefName: "feature-1", HeadRefOid: "sha1", State: PRStateOpen}, - 2: {Number: 2, Mergeable: PRMergeableStateConflicting, BaseRefName: "feature-1", HeadRefName: "feature-2", HeadRefOid: "sha2", State: PRStateOpen}, - }, - wantOK: false, - wantReason: "PR #2 has merge conflicts", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ok, reason, err := validatePRs(tt.changeIDs, tt.prInfoMap) - if tt.wantErr { - require.Error(t, err) - } else { - require.NoError(t, err) - } - assert.Equal(t, tt.wantOK, ok) - assert.Equal(t, tt.wantReason, reason) - }) - } -} diff --git a/submitqueue/extension/mergechecker/mergechecker.go b/submitqueue/extension/mergechecker/mergechecker.go deleted file mode 100644 index 7ad8c41c4..000000000 --- a/submitqueue/extension/mergechecker/mergechecker.go +++ /dev/null @@ -1,47 +0,0 @@ -// 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 mergechecker - -//go:generate mockgen -source=mergechecker.go -destination=mock/mergechecker_mock.go -package=mock - -import ( - "context" - - "github.com/uber/submitqueue/submitqueue/entity" -) - -// MergeChecker predicts whether a request's changes can merge cleanly. -type MergeChecker interface { - // Check is a fail-fast mergeability check that optimistically assesses - // whether the request's changes can be merged. It is handed the request - // identity and reads request.Change itself. A positive result does not - // guarantee that the changes will apply cleanly at merge time. - Check(ctx context.Context, request entity.Request) (entity.MergeResult, error) -} - -// Config carries the per-queue identity handed to a Factory. The system knows -// only the queue name; everything an implementation needs is injected at -// construction by the integrator. -type Config struct { - // QueueName identifies the queue this MergeChecker serves. - QueueName string -} - -// Factory builds the MergeChecker for a queue. Implementations are provided by -// integrators (and tests) and inject whatever they need at construction. -type Factory interface { - // For returns the MergeChecker for the given queue. - For(cfg Config) (MergeChecker, error) -} diff --git a/submitqueue/extension/mergechecker/mock/BUILD.bazel b/submitqueue/extension/mergechecker/mock/BUILD.bazel deleted file mode 100644 index 1b958a953..000000000 --- a/submitqueue/extension/mergechecker/mock/BUILD.bazel +++ /dev/null @@ -1,13 +0,0 @@ -load("@rules_go//go:def.bzl", "go_library") - -go_library( - name = "go_default_library", - srcs = ["mergechecker_mock.go"], - importpath = "github.com/uber/submitqueue/submitqueue/extension/mergechecker/mock", - visibility = ["//visibility:public"], - deps = [ - "//submitqueue/entity:go_default_library", - "//submitqueue/extension/mergechecker:go_default_library", - "@org_uber_go_mock//gomock:go_default_library", - ], -) diff --git a/submitqueue/extension/mergechecker/mock/mergechecker_mock.go b/submitqueue/extension/mergechecker/mock/mergechecker_mock.go deleted file mode 100644 index 48b1237c8..000000000 --- a/submitqueue/extension/mergechecker/mock/mergechecker_mock.go +++ /dev/null @@ -1,97 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: mergechecker.go -// -// Generated by this command: -// -// mockgen -source=mergechecker.go -destination=mock/mergechecker_mock.go -package=mock -// - -// Package mock is a generated GoMock package. -package mock - -import ( - context "context" - reflect "reflect" - - entity "github.com/uber/submitqueue/submitqueue/entity" - mergechecker "github.com/uber/submitqueue/submitqueue/extension/mergechecker" - gomock "go.uber.org/mock/gomock" -) - -// MockMergeChecker is a mock of MergeChecker interface. -type MockMergeChecker struct { - ctrl *gomock.Controller - recorder *MockMergeCheckerMockRecorder - isgomock struct{} -} - -// MockMergeCheckerMockRecorder is the mock recorder for MockMergeChecker. -type MockMergeCheckerMockRecorder struct { - mock *MockMergeChecker -} - -// NewMockMergeChecker creates a new mock instance. -func NewMockMergeChecker(ctrl *gomock.Controller) *MockMergeChecker { - mock := &MockMergeChecker{ctrl: ctrl} - mock.recorder = &MockMergeCheckerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockMergeChecker) EXPECT() *MockMergeCheckerMockRecorder { - return m.recorder -} - -// Check mocks base method. -func (m *MockMergeChecker) Check(ctx context.Context, request entity.Request) (entity.MergeResult, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Check", ctx, request) - ret0, _ := ret[0].(entity.MergeResult) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Check indicates an expected call of Check. -func (mr *MockMergeCheckerMockRecorder) Check(ctx, request any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Check", reflect.TypeOf((*MockMergeChecker)(nil).Check), ctx, request) -} - -// 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 mergechecker.Config) (mergechecker.MergeChecker, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "For", cfg) - ret0, _ := ret[0].(mergechecker.MergeChecker) - 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) -}