Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ local-stop: ## Stop all services (keep data)

mocks: ## Generate mock files using mockgen
@echo "Generating mocks..."
@$(BAZEL) run @rules_go//go -- generate ./extension/storage/... ./extension/counter/... ./extension/queue/... ./extension/mergechecker/... ./extension/scorer/... ./core/consumer/...
@$(BAZEL) run @rules_go//go -- generate ./extension/storage/... ./extension/counter/... ./extension/queue/... ./extension/mergechecker/... ./extension/pusher/... ./extension/scorer/... ./core/consumer/...
@echo "Mocks generated successfully!"

proto: ## Generate protobuf files from .proto definitions
Expand Down
2 changes: 2 additions & 0 deletions example/server/orchestrator/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ go_library(
"//extension/counter/mysql",
"//extension/mergechecker",
"//extension/mergechecker/github",
"//extension/pusher",
"//extension/pusher/git",
"//extension/queue",
"//extension/queue/mysql",
"//extension/scorer/heuristic",
Expand Down
31 changes: 29 additions & 2 deletions example/server/orchestrator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import (
mysqlcounter "github.com/uber/submitqueue/extension/counter/mysql"
"github.com/uber/submitqueue/extension/mergechecker"
githubchecker "github.com/uber/submitqueue/extension/mergechecker/github"
"github.com/uber/submitqueue/extension/pusher"
gitpusher "github.com/uber/submitqueue/extension/pusher/git"
extqueue "github.com/uber/submitqueue/extension/queue"
queueMySQL "github.com/uber/submitqueue/extension/queue/mysql"
"github.com/uber/submitqueue/extension/scorer/heuristic"
Expand Down Expand Up @@ -203,8 +205,14 @@ func run() error {
return fmt.Errorf("failed to create change provider: %w", err)
}

// Create pusher
psh, err := newPusher(logger, scope)
if err != nil {
return fmt.Errorf("failed to create pusher: %w", err)
}

// Register controllers
if err := registerControllers(c, logger.Sugar(), scope, registry, mc, cp, cnt, store); err != nil {
if err := registerControllers(c, logger.Sugar(), scope, registry, mc, cp, psh, cnt, store); err != nil {
return err
}

Expand Down Expand Up @@ -389,7 +397,7 @@ func newTopicRegistry(q extqueue.Queue, subscriberName string) (consumer.TopicRe
// │ │ │
// └────────┴────────────────────────┘

func registerControllers(c consumer.Consumer, logger *zap.SugaredLogger, scope tally.Scope, registry consumer.TopicRegistry, mc mergechecker.MergeChecker, cp changeprovider.ChangeProvider, cnt counter.Counter, store storage.Storage) error {
func registerControllers(c consumer.Consumer, logger *zap.SugaredLogger, scope tally.Scope, registry consumer.TopicRegistry, mc mergechecker.MergeChecker, cp changeprovider.ChangeProvider, psh pusher.Pusher, cnt counter.Counter, store storage.Storage) error {
requestController := start.NewController(
logger,
scope,
Expand Down Expand Up @@ -495,6 +503,7 @@ func registerControllers(c consumer.Consumer, logger *zap.SugaredLogger, scope t
scope,
store,
registry,
psh,
consumer.TopicKeyMerge,
"orchestrator-merge",
)
Expand Down Expand Up @@ -595,3 +604,21 @@ func newChangeProvider(logger *zap.Logger, scope tally.Scope) (changeprovider.Ch
MetricsScope: scope.SubScope("changeprovider"),
}), nil
}

// newPusher creates a git-backed Pusher bound to the configured checkout
// path, remote, and target branch. Configured via PUSHER_CHECKOUT_PATH
// (required), PUSHER_REMOTE (default "origin"), and PUSHER_TARGET (default
// "main").
func newPusher(logger *zap.Logger, scope tally.Scope) (pusher.Pusher, error) {
checkout := os.Getenv("PUSHER_CHECKOUT_PATH")
if checkout == "" {
return nil, fmt.Errorf("PUSHER_CHECKOUT_PATH environment variable is required")
}
return gitpusher.NewPusher(gitpusher.Params{
CheckoutPath: checkout,
Remote: getEnv("PUSHER_REMOTE", "origin"),
Target: getEnv("PUSHER_TARGET", "main"),
Logger: logger.Sugar(),
MetricsScope: scope.SubScope("pusher"),
}), nil
}
9 changes: 9 additions & 0 deletions extension/pusher/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 = "pusher",
srcs = ["pusher.go"],
importpath = "github.com/uber/submitqueue/extension/pusher",
visibility = ["//visibility:public"],
deps = ["//entity"],
)
44 changes: 44 additions & 0 deletions extension/pusher/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Pusher

Pluggable abstraction for landing a list of `entity.Change` values onto a
target branch and pushing the result to a source-control remote.

## Interface

`Pusher` exposes a single `Push` method that accepts a list of changes.
Implementations are bound to a specific `(checkout, remote, target)` tuple
at construction time, so the interface itself stays vendor- and
configuration-agnostic.

The interface enforces an **all-or-nothing atomicity contract**: when `Push`
returns an error, no change has reached the remote — neither partially nor
fully. Callers can treat a non-nil error as "the remote is exactly as it was
before the call". The `ErrConflict` sentinel marks user-caused failures so
callers can route them to a non-retry path.

A successful `Push` returns one `ChangeOutcome` per input change in input
order. Each outcome reports either:

- `OutcomeStatusCommitted` with the list of `CommitSHAs` produced on the
target branch (one change can land as multiple commits, e.g. a stack of
PRs); or
- `OutcomeStatusAlreadyExisted` with no commits, when the change is already
present on the target branch (previously landed via another path, or
subsumed by an earlier change in the same push). Git surfaces this as
"rebased out" during a cherry-pick.

## Implementations

- [`git/`](git/) — applies changes against a local checkout via `git
cherry-pick`, then `git push`. Construction takes the path to the
checkout, the remote name, and the target branch; the implementation
owns that working tree and serializes concurrent invocations.

## Adding a new backend

1. Create `extension/pusher/{backend}/` with a `Pusher` implementation.
2. Bind the implementation to its checkout/remote/target at construction.
3. Map each `entity.Change` to the backend's commit/push primitives.
4. Honour the atomicity contract: never publish partial state. Return
`ErrConflict` (wrapped) for user-caused apply failures and a plain error
for transient infra failures.
30 changes: 30 additions & 0 deletions extension/pusher/git/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
load("@rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "git",
srcs = ["git_pusher.go"],
importpath = "github.com/uber/submitqueue/extension/pusher/git",
visibility = ["//visibility:public"],
deps = [
"//core/metrics",
"//entity",
"//entity/github",
"//extension/pusher",
"@com_github_uber_go_tally_v4//:tally",
"@org_uber_go_zap//:zap",
],
)

go_test(
name = "git_test",
srcs = ["git_pusher_test.go"],
embed = [":git"],
deps = [
"//entity",
"//extension/pusher",
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
"@com_github_uber_go_tally_v4//:tally",
"@org_uber_go_zap//zaptest",
],
)
Loading
Loading