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
8 changes: 8 additions & 0 deletions runway/core/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
load("@rules_go//go:def.bzl", "go_library")

go_library(
name = "core",
srcs = ["core.go"],
importpath = "github.com/uber/submitqueue/runway/core",
visibility = ["//visibility:public"],
)
20 changes: 20 additions & 0 deletions runway/core/core.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// 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 core groups infrastructure shared across Runway's own services —
// the Runway-scoped analogue of the repo-level core/. Cross-domain
// infrastructure lives in the top-level core/; this package is for plumbing
// private to Runway. Subpackages are added here as shared needs emerge,
// mirroring submitqueue/core.
package core
9 changes: 9 additions & 0 deletions runway/core/topickey/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
load("@rules_go//go:def.bzl", "go_library")

go_library(
name = "topickey",
srcs = ["topickey.go"],
importpath = "github.com/uber/submitqueue/runway/core/topickey",
visibility = ["//visibility:public"],
deps = ["//core/consumer"],
)
32 changes: 32 additions & 0 deletions runway/core/topickey/topickey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// 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 topickey defines Runway pipeline stage identifiers.
package topickey

import "github.com/uber/submitqueue/core/consumer"

// TopicKey is the shared pipeline stage identifier type.
type TopicKey = consumer.TopicKey

const (
// TopicKeyCheck is the inbound topic where mergeability check requests arrive from SubmitQueue.
TopicKeyCheck TopicKey = "check"
// TopicKeyLand is the inbound topic where batch land jobs arrive from SubmitQueue.
TopicKeyLand TopicKey = "land"
// TopicKeyCheckResult is the outbound topic where check results are published back to SubmitQueue.
TopicKeyCheckResult TopicKey = "checkresult"
// TopicKeyLandResult is the outbound topic where land results are published back to SubmitQueue.
TopicKeyLandResult TopicKey = "landresult"
)
31 changes: 31 additions & 0 deletions runway/entity/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
load("@rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "entity",
srcs = [
"check.go",
"entity.go",
"job.go",
],
importpath = "github.com/uber/submitqueue/runway/entity",
visibility = ["//visibility:public"],
deps = [
"//entity/change",
"//entity/mergestrategy",
],
)

go_test(
name = "entity_test",
srcs = [
"check_test.go",
"job_test.go",
],
embed = [":entity"],
deps = [
"//entity/change",
"//entity/mergestrategy",
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
],
)
87 changes: 87 additions & 0 deletions runway/entity/check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// 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 (
"encoding/json"

"github.com/uber/submitqueue/entity/change"
"github.com/uber/submitqueue/entity/mergestrategy"
)

// Check is the inbound message on the runway-check topic. SubmitQueue publishes
// one Check per request to determine whether the request's changes can merge
// cleanly against the target branch. The check is read-only — it does not
// mutate the target branch or any external state.
type Check struct {
// Queue is the SubmitQueue queue name.
Queue string `json:"queue"`
// RequestID is the SubmitQueue request ID. Serves as the idempotency key.
RequestID string `json:"request_id"`
// Repo identifies the repository (e.g., "uber/submitqueue").
Repo string `json:"repo"`
// TargetBranch is the destination branch (e.g., "main").
TargetBranch string `json:"target_branch"`
// Changes is the set of code changes to check for mergeability.
Changes []change.Change `json:"changes"`
// Strategy is the landing strategy that would be used to land these changes.
Strategy mergestrategy.MergeStrategy `json:"strategy"`
}

// ToBytes serializes the Check to JSON bytes for queue message payload.
func (c Check) ToBytes() ([]byte, error) {
return json.Marshal(c)
}

// CheckFromBytes deserializes a Check from JSON bytes.
func CheckFromBytes(data []byte) (Check, error) {
var c Check
err := json.Unmarshal(data, &c)
return c, err
}

// MergeabilityResult describes whether a single change can be applied cleanly
// to the target branch.
type MergeabilityResult struct {
// Change is the input change this result corresponds to.
Change change.Change `json:"change"`
// Mergeable is true if the change can be applied cleanly.
Mergeable bool `json:"mergeable"`
// Reason is a human-readable explanation when Mergeable is false; empty when true.
Reason string `json:"reason,omitempty"`
}

// CheckResult is the outbound message published to the sq-check-result topic.
// It carries per-change mergeability detail back to SubmitQueue.
type CheckResult struct {
// Queue is the SubmitQueue queue name (partition key for the outbound topic).
Queue string `json:"queue"`
// RequestID correlates to Check.RequestID.
RequestID string `json:"request_id"`
// Results is one entry per change in the input Check.Changes.
Results []MergeabilityResult `json:"results"`
}

// ToBytes serializes the CheckResult to JSON bytes for queue message payload.
func (r CheckResult) ToBytes() ([]byte, error) {
return json.Marshal(r)
}

// CheckResultFromBytes deserializes a CheckResult from JSON bytes.
func CheckResultFromBytes(data []byte) (CheckResult, error) {
var r CheckResult
err := json.Unmarshal(data, &r)
return r, err
}
152 changes: 152 additions & 0 deletions runway/entity/check_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// 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 (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/uber/submitqueue/entity/change"
"github.com/uber/submitqueue/entity/mergestrategy"
)

func TestCheck_SerializationRoundTrip(t *testing.T) {
tests := []struct {
name string
check Check
}{
{
name: "single change single URI",
check: Check{
Queue: "go-code-main",
RequestID: "go-code-main/42",
Repo: "uber/submitqueue",
TargetBranch: "main",
Changes: []change.Change{{URIs: []string{"github://uber/submitqueue/pull/123/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}},
Strategy: mergestrategy.MergeStrategyRebase,
},
},
{
name: "multiple changes",
check: Check{
Queue: "queue1",
RequestID: "queue1/100",
Repo: "uber/repo-a",
TargetBranch: "main",
Changes: []change.Change{
{URIs: []string{"github://uber/repo-a/pull/101/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}},
{URIs: []string{"github://uber/repo-a/pull/102/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}},
},
Strategy: mergestrategy.MergeStrategySquashRebase,
},
},
{
name: "stacked diff with multiple URIs",
check: Check{
Queue: "queue2",
RequestID: "queue2/200",
Repo: "uber/submitqueue",
TargetBranch: "release",
Changes: []change.Change{{URIs: []string{
"github://uber/submitqueue/pull/10/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"github://uber/submitqueue/pull/11/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
}}},
Strategy: mergestrategy.MergeStrategyMerge,
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := tt.check.ToBytes()
require.NoError(t, err)

deserialized, err := CheckFromBytes(data)
require.NoError(t, err)

assert.Equal(t, tt.check, deserialized)
})
}
}

func TestCheckFromBytes_InvalidJSON(t *testing.T) {
_, err := CheckFromBytes([]byte(`{"invalid": json"}`))
assert.Error(t, err)
}

func TestCheckFromBytes_EmptyData(t *testing.T) {
c, err := CheckFromBytes([]byte(`{}`))
require.NoError(t, err)

assert.Empty(t, c.Queue)
assert.Empty(t, c.RequestID)
assert.Equal(t, mergestrategy.MergeStrategyUnknown, c.Strategy)
}

func TestCheckResult_SerializationRoundTrip(t *testing.T) {
tests := []struct {
name string
result CheckResult
}{
{
name: "all mergeable",
result: CheckResult{
Queue: "go-code-main",
RequestID: "go-code-main/42",
Results: []MergeabilityResult{
{Change: change.Change{URIs: []string{"github://uber/submitqueue/pull/1/aaaa"}}, Mergeable: true},
{Change: change.Change{URIs: []string{"github://uber/submitqueue/pull/2/bbbb"}}, Mergeable: true},
},
},
},
{
name: "some unmergeable",
result: CheckResult{
Queue: "queue1",
RequestID: "queue1/100",
Results: []MergeabilityResult{
{Change: change.Change{URIs: []string{"github://uber/repo/pull/1/aaaa"}}, Mergeable: true},
{Change: change.Change{URIs: []string{"github://uber/repo/pull/2/bbbb"}}, Mergeable: false, Reason: "conflicts with src/main.go"},
},
},
},
{
name: "empty results",
result: CheckResult{
Queue: "queue2",
RequestID: "queue2/200",
Results: []MergeabilityResult{},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := tt.result.ToBytes()
require.NoError(t, err)

deserialized, err := CheckResultFromBytes(data)
require.NoError(t, err)

assert.Equal(t, tt.result, deserialized)
})
}
}

func TestCheckResultFromBytes_InvalidJSON(t *testing.T) {
_, err := CheckResultFromBytes([]byte(`not json`))
assert.Error(t, err)
}
16 changes: 16 additions & 0 deletions runway/entity/entity.go
Original file line number Diff line number Diff line change
@@ -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 entity holds Runway-specific domain types (distinct from shared repo entity/).
package entity
Loading