Skip to content

Commit 50a856e

Browse files
kevinlnewclaude
andcommitted
feat(runway): add Merger extension interface, mock, and noop impl
Add the pluggable VCS extension for Runway with the Merger interface (CheckMergeability + Merge), using the landed proto-based MergeRequest/ MergeResult types from api/runway/messagequeue. Includes noop impl with synthetic output IDs, generated mock, and table-driven tests. Addresses review feedback: renamed VCS -> Merger, Land -> Merge. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7f545fa commit 50a856e

9 files changed

Lines changed: 403 additions & 0 deletions

File tree

runway/extension/BUILD.bazel

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
load("@rules_go//go:def.bzl", "go_library")
2+
3+
go_library(
4+
name = "extension",
5+
srcs = ["extension.go"],
6+
importpath = "github.com/uber/submitqueue/runway/extension",
7+
visibility = ["//visibility:public"],
8+
)

runway/extension/extension.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) 2025 Uber Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package extension holds Runway-specific extension implementations.
16+
package extension

runway/extension/vcs/BUILD.bazel

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
load("@rules_go//go:def.bzl", "go_library")
2+
3+
go_library(
4+
name = "vcs",
5+
srcs = ["vcs.go"],
6+
importpath = "github.com/uber/submitqueue/runway/extension/vcs",
7+
visibility = ["//visibility:public"],
8+
deps = ["//api/runway/messagequeue"],
9+
)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
load("@rules_go//go:def.bzl", "go_library")
2+
3+
go_library(
4+
name = "mock",
5+
srcs = ["vcs_mock.go"],
6+
importpath = "github.com/uber/submitqueue/runway/extension/vcs/mock",
7+
visibility = ["//visibility:public"],
8+
deps = [
9+
"//api/runway/messagequeue",
10+
"//runway/extension/vcs",
11+
"@org_uber_go_mock//gomock",
12+
],
13+
)

runway/extension/vcs/mock/vcs_mock.go

Lines changed: 112 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
load("@rules_go//go:def.bzl", "go_library", "go_test")
2+
3+
go_library(
4+
name = "noop",
5+
srcs = ["noop.go"],
6+
importpath = "github.com/uber/submitqueue/runway/extension/vcs/noop",
7+
visibility = ["//visibility:public"],
8+
deps = [
9+
"//api/runway/messagequeue",
10+
"//api/runway/messagequeue/protopb",
11+
"//runway/extension/vcs",
12+
],
13+
)
14+
15+
go_test(
16+
name = "noop_test",
17+
srcs = ["noop_test.go"],
18+
embed = [":noop"],
19+
deps = [
20+
"//api/base/change/protopb",
21+
"//api/base/mergestrategy/protopb",
22+
"//api/runway/messagequeue",
23+
"//api/runway/messagequeue/protopb",
24+
"@com_github_stretchr_testify//assert",
25+
"@com_github_stretchr_testify//require",
26+
],
27+
)

runway/extension/vcs/noop/noop.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright (c) 2025 Uber Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package noop provides a no-op Merger implementation for local development and
16+
// testing. CheckMergeability always reports success; Merge produces synthetic
17+
// output IDs from an atomic counter.
18+
package noop
19+
20+
import (
21+
"context"
22+
"fmt"
23+
"sync/atomic"
24+
25+
runwaymq "github.com/uber/submitqueue/api/runway/messagequeue"
26+
runwaypb "github.com/uber/submitqueue/api/runway/messagequeue/protopb"
27+
"github.com/uber/submitqueue/runway/extension/vcs"
28+
)
29+
30+
var _ vcs.Merger = (*Merger)(nil)
31+
32+
// Merger is a no-op implementation that always succeeds.
33+
type Merger struct {
34+
seq atomic.Uint64
35+
}
36+
37+
// New returns a new no-op Merger instance.
38+
func New() *Merger { return &Merger{} }
39+
40+
func (v *Merger) CheckMergeability(_ context.Context, req *runwaymq.MergeRequest) (*runwaymq.MergeResult, error) {
41+
steps := make([]*runwaymq.StepResult, len(req.GetSteps()))
42+
for i, s := range req.GetSteps() {
43+
steps[i] = &runwaymq.StepResult{StepId: s.GetStepId()}
44+
}
45+
return &runwaymq.MergeResult{
46+
Id: req.GetId(),
47+
Outcome: runwaypb.Outcome_SUCCEEDED,
48+
Steps: steps,
49+
}, nil
50+
}
51+
52+
func (v *Merger) Merge(_ context.Context, req *runwaymq.MergeRequest) (*runwaymq.MergeResult, error) {
53+
steps := make([]*runwaymq.StepResult, len(req.GetSteps()))
54+
for i, s := range req.GetSteps() {
55+
n := v.seq.Add(1)
56+
steps[i] = &runwaymq.StepResult{
57+
StepId: s.GetStepId(),
58+
Outputs: []*runwaymq.StepOutput{
59+
{Id: fmt.Sprintf("%040x", n)},
60+
},
61+
}
62+
}
63+
return &runwaymq.MergeResult{
64+
Id: req.GetId(),
65+
Outcome: runwaypb.Outcome_SUCCEEDED,
66+
Steps: steps,
67+
}, nil
68+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright (c) 2025 Uber Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package noop
16+
17+
import (
18+
"context"
19+
"testing"
20+
21+
"github.com/stretchr/testify/assert"
22+
"github.com/stretchr/testify/require"
23+
changepb "github.com/uber/submitqueue/api/base/change/protopb"
24+
strategypb "github.com/uber/submitqueue/api/base/mergestrategy/protopb"
25+
runwaymq "github.com/uber/submitqueue/api/runway/messagequeue"
26+
runwaypb "github.com/uber/submitqueue/api/runway/messagequeue/protopb"
27+
)
28+
29+
func testRequest() *runwaymq.MergeRequest {
30+
return &runwaymq.MergeRequest{
31+
Id: "queue-a/42",
32+
QueueName: "queue-a",
33+
Steps: []*runwaymq.MergeStep{
34+
{
35+
StepId: "queue-a/1",
36+
Changes: []*changepb.Change{{Uris: []string{"github://uber/repo/pull/1/abcdef0123456789abcdef0123456789abcdef01"}}},
37+
Strategy: strategypb.Strategy_REBASE,
38+
},
39+
{
40+
StepId: "queue-a/2",
41+
Changes: []*changepb.Change{{Uris: []string{"github://uber/repo/pull/2/89abcdef0123456789abcdef0123456789abcdef"}}},
42+
Strategy: strategypb.Strategy_MERGE,
43+
},
44+
},
45+
}
46+
}
47+
48+
func TestCheckMergeability(t *testing.T) {
49+
v := New()
50+
req := testRequest()
51+
52+
res, err := v.CheckMergeability(context.Background(), req)
53+
require.NoError(t, err)
54+
55+
assert.Equal(t, req.GetId(), res.GetId())
56+
assert.Equal(t, runwaypb.Outcome_SUCCEEDED, res.GetOutcome())
57+
require.Len(t, res.GetSteps(), 2)
58+
assert.Equal(t, "queue-a/1", res.GetSteps()[0].GetStepId())
59+
assert.Empty(t, res.GetSteps()[0].GetOutputs())
60+
assert.Equal(t, "queue-a/2", res.GetSteps()[1].GetStepId())
61+
assert.Empty(t, res.GetSteps()[1].GetOutputs())
62+
}
63+
64+
func TestMerge(t *testing.T) {
65+
v := New()
66+
req := testRequest()
67+
68+
res, err := v.Merge(context.Background(), req)
69+
require.NoError(t, err)
70+
71+
assert.Equal(t, req.GetId(), res.GetId())
72+
assert.Equal(t, runwaypb.Outcome_SUCCEEDED, res.GetOutcome())
73+
require.Len(t, res.GetSteps(), 2)
74+
assert.Equal(t, "queue-a/1", res.GetSteps()[0].GetStepId())
75+
require.Len(t, res.GetSteps()[0].GetOutputs(), 1)
76+
assert.NotEmpty(t, res.GetSteps()[0].GetOutputs()[0].GetId())
77+
assert.Equal(t, "queue-a/2", res.GetSteps()[1].GetStepId())
78+
require.Len(t, res.GetSteps()[1].GetOutputs(), 1)
79+
assert.NotEmpty(t, res.GetSteps()[1].GetOutputs()[0].GetId())
80+
}
81+
82+
func TestMerge_UniqueOutputIDs(t *testing.T) {
83+
v := New()
84+
req := testRequest()
85+
86+
res1, err := v.Merge(context.Background(), req)
87+
require.NoError(t, err)
88+
res2, err := v.Merge(context.Background(), req)
89+
require.NoError(t, err)
90+
91+
assert.NotEqual(t, res1.GetSteps()[0].GetOutputs()[0].GetId(), res2.GetSteps()[0].GetOutputs()[0].GetId())
92+
}

0 commit comments

Comments
 (0)