Skip to content
Open
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
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"],
)
34 changes: 34 additions & 0 deletions runway/core/topickey/topickey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// 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 the Runway-owned queue identifiers. Runway owns the
// merge-conflict check queues; other services (e.g. SubmitQueue) import these
// keys to publish onto / consume from them.
package topickey

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

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

const (
// TopicKeyMergeConflictCheck is the runway-owned queue that carries
// merge-conflict check requests. A client publishes a full
// MergeConflictCheckRequest here; runway consumes it.
TopicKeyMergeConflictCheck TopicKey = "merge-conflict-checker"
// TopicKeyMergeConflictCheckSignal is the runway-owned queue that carries
// merge-conflict check results. Runway publishes a full
// MergeConflictCheckResult here; the requesting client consumes it.
TopicKeyMergeConflictCheckSignal TopicKey = "merge-conflict-checker-signal"
)
24 changes: 24 additions & 0 deletions runway/entity/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
load("@rules_go//go:def.bzl", "go_library", "go_test")

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

go_test(
name = "entity_test",
srcs = ["merge_conflict_check_test.go"],
embed = [":entity"],
deps = [
"//entity/change",
"//entity/mergestrategy",
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
],
)
109 changes: 109 additions & 0 deletions runway/entity/merge_conflict_check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// 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's domain entities, including the wire contract for
// the merge-conflict check queues that Runway owns. The contract crosses a
// service boundary (a calling service cannot read Runway's storage and vice
// versa), so these payloads carry the full data needed to perform a merge
// attempt rather than opaque entity IDs.
package entity

import (
"encoding/json"

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

// MergeStep is one step of an ordered merge-conflict check: a single set of
// change(s) applied with a strategy. Runway applies the steps of a request in
// order on top of the target branch; the ordering encodes the base-layering
// (earlier steps are the in-flight base, the last step is the candidate).
type MergeStep struct {
// StepID is an opaque, caller-assigned identifier for this step. Runway
// treats it as an attribution token only — it echoes it back per-step in
// StepConflict so a multi-step result is attributable — and never interprets
// its contents. (A caller might use, for example, its own request id here.)
StepID string `json:"step_id"`
// Changes are the code change(s) to apply for this step (provider URIs with
// head commit SHAs; see entity/change.Change).
Changes []change.Change `json:"changes"`
// Strategy is how this step's changes are integrated into the target branch.
Strategy mergestrategy.MergeStrategy `json:"strategy"`
}

// MergeConflictCheckRequest is the payload a client publishes to the
// TopicKeyMergeConflictCheck queue. The ID is owned by the client so it can
// record the in-flight check before publishing and correlate the asynchronous
// result; runway echoes it back unchanged.
type MergeConflictCheckRequest struct {
// ID is the client-owned correlation id for this check request (one per
// request). Runway echoes it back on the result unchanged.
ID string `json:"id"`
// QueueName is the caller-provided queue name the check belongs to. Runway
// resolves the target branch and provider config per-queue from this name;
// no target ref is passed.
QueueName string `json:"queue_name"`
// Steps is the ordered application sequence: in-flight steps first, the
// candidate last. A single-element slice expresses "candidate vs target
// branch".
Steps []MergeStep `json:"steps"`
}

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

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

// StepConflict identifies a single step that failed to apply cleanly during a
// merge-conflict check, so a multi-step result attributes the conflict.
type StepConflict struct {
// StepID echoes the StepID of the step that conflicted (see MergeStep.StepID).
StepID string `json:"step_id"`
// Reason is a human-readable explanation of the conflict.
Reason string `json:"reason"`
}

// MergeConflictCheckResult is the payload runway publishes to the
// TopicKeyMergeConflictCheckSignal queue once a check completes.
type MergeConflictCheckResult struct {
// ID echoes the client-owned correlation id from the request.
ID string `json:"id"`
// Mergeable is true if the whole ordered step sequence applied cleanly.
Mergeable bool `json:"mergeable"`
// Reason is a human-readable explanation when Mergeable is false. Empty when mergeable.
Reason string `json:"reason"`
// Conflicts optionally attributes the failure to specific steps. May be
// empty even when Mergeable is false.
Conflicts []StepConflict `json:"conflicts,omitempty"`
}

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

// MergeConflictCheckResultFromBytes deserializes a MergeConflictCheckResult from JSON bytes.
func MergeConflictCheckResultFromBytes(data []byte) (MergeConflictCheckResult, error) {
var res MergeConflictCheckResult
err := json.Unmarshal(data, &res)
return res, err
}
66 changes: 66 additions & 0 deletions runway/entity/merge_conflict_check_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// 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 TestMergeConflictCheckRequestRoundTrip(t *testing.T) {
req := MergeConflictCheckRequest{
ID: "queue-a/42/check",
QueueName: "queue-a",
Steps: []MergeStep{
{
StepID: "queue-a/1",
Changes: []change.Change{{URIs: []string{"github://uber/repo/pull/1/" + "0123456789abcdef0123456789abcdef01234567"}}},
Strategy: mergestrategy.MergeStrategyRebase,
},
{
StepID: "queue-a/2",
Changes: []change.Change{{URIs: []string{"github://uber/repo/pull/2/" + "89abcdef0123456789abcdef0123456789abcdef"}}},
Strategy: mergestrategy.MergeStrategyMerge,
},
},
}

data, err := req.ToBytes()
require.NoError(t, err)

got, err := MergeConflictCheckRequestFromBytes(data)
require.NoError(t, err)
assert.Equal(t, req, got)
}

func TestMergeConflictCheckResultRoundTrip(t *testing.T) {
res := MergeConflictCheckResult{
ID: "queue-a/42/check",
Mergeable: false,
Reason: "conflict in foo.go",
Conflicts: []StepConflict{{StepID: "queue-a/2", Reason: "conflict in foo.go"}},
}

data, err := res.ToBytes()
require.NoError(t, err)

got, err := MergeConflictCheckResultFromBytes(data)
require.NoError(t, err)
assert.Equal(t, res, got)
}
Loading