From 4690c6d40e92a49cce2044fc53a4432d55d5bef5 Mon Sep 17 00:00:00 2001 From: Preetam Dwivedi Date: Thu, 3 Sep 2026 22:31:00 -0400 Subject: [PATCH] refactor: migrate merge terminology to land MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary ### Why? SubmitQueue uses land as the domain verb for integrating changes, while merge is reserved for the MERGE land strategy, Git primitives, and external provider terminology. ### What? Rename merge-oriented API contracts, queue topics, controllers, extensions, entities, configuration, environment variables, wiring, tooling, and documentation to land terminology. Preserve the LandStrategy MERGE value and its Git merge-commit behavior. ## Test Plan - ✅ `make proto` - ✅ `make fmt` - ✅ `make build` - ✅ `make test` - ✅ `make lint-binary lint-license lint-message-id lint-queue-shard` - ✅ `make tidy` and `make gazelle` produced no changes # Conflicts: # service/submitqueue/gateway/server/mapper/land.go # service/submitqueue/gateway/server/mapper/land_test.go # Please enter the commit message for your changes. Lines starting # with '#' will be kept; you may remove them yourself if you want to. # An empty message aborts the commit. # # interactive rebase in progress; onto c9ee7a84 # Last command done (1 command done): # pick a7aac77e # refactor: migrate merge terminology to land # No commands remaining. # You are currently rebasing branch 'land-terminology' on 'c9ee7a84'. # # Changes to be committed: # modified: AGENTS.md # modified: Makefile # modified: README.md # modified: api/base/hook/README.md # modified: api/base/hook/hook_test.go # modified: api/base/hook/protopb/hook.pb.go # renamed: api/base/mergestrategy/proto/BUILD.bazel -> api/base/landstrategy/proto/BUILD.bazel # renamed: api/base/mergestrategy/proto/mergestrategy.proto -> api/base/landstrategy/proto/landstrategy.proto # renamed: api/base/mergestrategy/protopb/BUILD.bazel -> api/base/landstrategy/protopb/BUILD.bazel # renamed: api/base/mergestrategy/protopb/mergestrategy.pb.go -> api/base/landstrategy/protopb/landstrategy.pb.go # modified: api/base/messagequeue/proto/messagequeue.proto # modified: api/runway/messagequeue/BUILD.bazel # modified: api/runway/messagequeue/README.md # renamed: api/runway/messagequeue/merge.go -> api/runway/messagequeue/land.go # renamed: api/runway/messagequeue/merge_test.go -> api/runway/messagequeue/land_test.go # modified: api/runway/messagequeue/proto/BUILD.bazel # renamed: api/runway/messagequeue/proto/merge.proto -> api/runway/messagequeue/proto/land.proto # modified: api/runway/messagequeue/protopb/BUILD.bazel # renamed: api/runway/messagequeue/protopb/merge.pb.go -> api/runway/messagequeue/protopb/land.pb.go # modified: api/runway/messagequeue/topics.go # modified: api/submitqueue/gateway/proto/gateway.proto # modified: api/submitqueue/gateway/protopb/BUILD.bazel # modified: api/submitqueue/gateway/protopb/gateway.pb.go # modified: api/submitqueue/gateway/protopb/gateway.pb.yarpc.go # modified: api/submitqueue/gateway/protopb/gateway_grpc.pb.go # modified: doc/howto/QUICKSTART.md # modified: doc/howto/TESTING.md # modified: doc/rfc/change-uri.md # modified: doc/rfc/consumer-gate.md # modified: doc/rfc/hook-framework.md # modified: doc/rfc/index.md # modified: doc/rfc/messagequeue-contract.md # modified: doc/rfc/runway/workflow.md # modified: doc/rfc/sql-queue-rfc.md # modified: doc/rfc/stovepipe/steps/build.md # modified: doc/rfc/stovepipe/steps/process.md # modified: doc/rfc/stovepipe/workflow.md # modified: doc/rfc/submitqueue/extension-contract.md # modified: doc/rfc/submitqueue/modular-queue-wiring.md # modified: doc/rfc/submitqueue/speculation-generator-best-first.md # modified: doc/rfc/submitqueue/speculation.md # modified: doc/rfc/submitqueue/workflow.md # renamed: platform/base/mergestrategy/BUILD.bazel -> platform/base/landstrategy/BUILD.bazel # new file: platform/base/landstrategy/landstrategy.go # deleted: platform/base/mergestrategy/mergestrategy.go # modified: platform/errs/README.md # modified: platform/extension/consumergate/file/store_test.go # modified: platform/extension/messagequeue/README.md # modified: platform/extension/messagequeue/mysql/README.md # modified: platform/extension/messagequeue/mysql/ctl/README.md # modified: platform/extension/messagequeue/mysql/schema/queue_messages.sql # modified: platform/git/exec/gitexec.go # modified: platform/hook/README.md # modified: platform/hook/dlq.go # modified: platform/metrics/README.md # modified: platform/metrics/metrics.go # modified: platform/publish/publish_test.go # modified: runway/README.md # modified: runway/controller/dlq/dlq.go # modified: runway/controller/dlq/dlq_test.go # renamed: runway/controller/merge/BUILD.bazel -> runway/controller/land/BUILD.bazel # renamed: runway/controller/merge/merge.go -> runway/controller/land/land.go # renamed: runway/controller/merge/merge_test.go -> runway/controller/land/land_test.go # renamed: runway/controller/mergeconflictcheck/BUILD.bazel -> runway/controller/landconflictcheck/BUILD.bazel # renamed: runway/controller/mergeconflictcheck/mergeconflictcheck.go -> runway/controller/landconflictcheck/landconflictcheck.go # renamed: runway/controller/mergeconflictcheck/mergeconflictcheck_test.go -> runway/controller/landconflictcheck/landconflictcheck_test.go # renamed: runway/extension/merger/BUILD.bazel -> runway/extension/lander/BUILD.bazel # renamed: runway/extension/merger/fake/BUILD.bazel -> runway/extension/lander/fake/BUILD.bazel # renamed: runway/extension/merger/fake/fake.go -> runway/extension/lander/fake/fake.go # renamed: runway/extension/merger/fake/fake_test.go -> runway/extension/lander/fake/fake_test.go # renamed: runway/extension/merger/git/BUILD.bazel -> runway/extension/lander/git/BUILD.bazel # renamed: runway/extension/merger/git/README.md -> runway/extension/lander/git/README.md # renamed: runway/extension/merger/git/author.go -> runway/extension/lander/git/author.go # renamed: runway/extension/merger/git/changeref.go -> runway/extension/lander/git/changeref.go # renamed: runway/extension/merger/git/git_merger.go -> runway/extension/lander/git/git_lander.go # renamed: runway/extension/merger/git/git_merger_test.go -> runway/extension/lander/git/git_lander_test.go # renamed: runway/extension/merger/git/headbranch.go -> runway/extension/lander/git/headbranch.go # renamed: runway/extension/merger/git/headbranch_test.go -> runway/extension/lander/git/headbranch_test.go # renamed: runway/extension/merger/git/objects.go -> runway/extension/lander/git/objects.go # renamed: runway/extension/merger/merger.go -> runway/extension/lander/lander.go # renamed: runway/extension/merger/mock/BUILD.bazel -> runway/extension/lander/mock/BUILD.bazel # new file: runway/extension/lander/mock/lander_mock.go # renamed: runway/extension/merger/noop/BUILD.bazel -> runway/extension/lander/noop/BUILD.bazel # renamed: runway/extension/merger/noop/noop.go -> runway/extension/lander/noop/noop.go # renamed: runway/extension/merger/noop/noop_test.go -> runway/extension/lander/noop/noop_test.go # deleted: runway/extension/merger/mock/merger_mock.go # modified: service/README.md # modified: service/runway/README.md # modified: service/runway/server/BUILD.bazel # modified: service/runway/server/Dockerfile # modified: service/runway/server/checkout.go # modified: service/runway/server/checkout_test.go # modified: service/runway/server/config.go # modified: service/runway/server/config_test.go # modified: service/runway/server/docker-compose.yml # modified: service/runway/server/gitruntime.go # modified: service/runway/server/main.go # modified: service/submitqueue/README.md # modified: service/submitqueue/demo/provider/README.md # new file: service/submitqueue/demo/provider/fake/land.yaml # deleted: service/submitqueue/demo/provider/fake/merge.yaml # modified: service/submitqueue/demo/provider/git/BUILD.bazel # renamed: service/submitqueue/demo/provider/git/merge.yaml -> service/submitqueue/demo/provider/git/land.yaml # modified: service/submitqueue/demo/provider/git/profiles.yaml # renamed: service/submitqueue/demo/provider/github/merge.yaml -> service/submitqueue/demo/provider/github/land.yaml # modified: service/submitqueue/demo/requests/BUILD.bazel # modified: service/submitqueue/demo/requests/fake.go # modified: service/submitqueue/demo/requests/git.go # modified: service/submitqueue/demo/requests/main.go # modified: service/submitqueue/demo/requests/main_test.go # modified: service/submitqueue/docker-compose.fake.yml # modified: service/submitqueue/docker-compose.git.yml # modified: service/submitqueue/docker-compose.provider.yml # modified: service/submitqueue/docker-compose.yml # modified: service/submitqueue/gateway/client/main.go # modified: service/submitqueue/gateway/server/mapper/BUILD.bazel # modified: service/submitqueue/gateway/server/mapper/land.go # modified: service/submitqueue/gateway/server/mapper/land_test.go # modified: service/submitqueue/gateway/server/queues.yaml # modified: service/submitqueue/orchestrator/server/config.go # modified: service/submitqueue/orchestrator/server/config_test.go # modified: stovepipe/README.md # modified: submitqueue/client/BUILD.bazel # modified: submitqueue/client/land.go # modified: submitqueue/core/batch/transition_test.go # modified: submitqueue/core/changeset/README.md # modified: submitqueue/core/changeset/changeset.go # modified: submitqueue/core/request/request_test.go # modified: submitqueue/core/topickey/topickey.go # modified: submitqueue/entity/BUILD.bazel # modified: submitqueue/entity/batch.go # modified: submitqueue/entity/batch_test.go # modified: submitqueue/entity/land.go # renamed: submitqueue/entity/merge_result.go -> submitqueue/entity/land_check_result.go # modified: submitqueue/entity/land_test.go # modified: submitqueue/entity/push_result.go # modified: submitqueue/entity/request.go # modified: submitqueue/entity/request_log.go # modified: submitqueue/entity/request_test.go # modified: submitqueue/entity/speculation.go # modified: submitqueue/extension/changeprovider/git/README.md # renamed: submitqueue/extension/mergechecker/BUILD.bazel -> submitqueue/extension/landchecker/BUILD.bazel # renamed: submitqueue/extension/mergechecker/fake/BUILD.bazel -> submitqueue/extension/landchecker/fake/BUILD.bazel # renamed: submitqueue/extension/mergechecker/fake/fake.go -> submitqueue/extension/landchecker/fake/fake.go # renamed: submitqueue/extension/mergechecker/fake/fake_test.go -> submitqueue/extension/landchecker/fake/fake_test.go # renamed: submitqueue/extension/mergechecker/github/BUILD.bazel -> submitqueue/extension/landchecker/github/BUILD.bazel # renamed: submitqueue/extension/mergechecker/github/checker.go -> submitqueue/extension/landchecker/github/checker.go # renamed: submitqueue/extension/mergechecker/github/checker_test.go -> submitqueue/extension/landchecker/github/checker_test.go # renamed: submitqueue/extension/mergechecker/github/graphql.go -> submitqueue/extension/landchecker/github/graphql.go # renamed: submitqueue/extension/mergechecker/github/graphql_test.go -> submitqueue/extension/landchecker/github/graphql_test.go # renamed: submitqueue/extension/mergechecker/github/validate.go -> submitqueue/extension/landchecker/github/validate.go # renamed: submitqueue/extension/mergechecker/github/validate_test.go -> submitqueue/extension/landchecker/github/validate_test.go # renamed: submitqueue/extension/mergechecker/mergechecker.go -> submitqueue/extension/landchecker/landchecker.go # renamed: submitqueue/extension/mergechecker/mock/BUILD.bazel -> submitqueue/extension/landchecker/mock/BUILD.bazel # renamed: submitqueue/extension/mergechecker/mock/mergechecker_mock.go -> submitqueue/extension/landchecker/mock/landchecker_mock.go # modified: submitqueue/extension/speculation/allocator/sticky/sticky.go # modified: submitqueue/extension/speculation/generator/bestfirst/README.md # modified: submitqueue/extension/speculation/generator/bestfirst/bestfirst_test.go # modified: submitqueue/extension/speculation/scorer/scorer.go # modified: submitqueue/extension/speculation/speculator/README.md # modified: submitqueue/extension/speculation/speculator/speculator.go # modified: submitqueue/extension/speculation/speculator/standard/README.md # modified: submitqueue/extension/storage/mysql/BUILD.bazel # modified: submitqueue/extension/storage/mysql/batch_store_test.go # modified: submitqueue/extension/storage/mysql/request_store_test.go # modified: submitqueue/gateway/controller/BUILD.bazel # modified: submitqueue/gateway/controller/cancel.go # modified: submitqueue/gateway/controller/land_test.go # modified: submitqueue/orchestrator/BUILD.bazel # modified: submitqueue/orchestrator/README.md # modified: submitqueue/orchestrator/controller/README.md # modified: submitqueue/orchestrator/controller/batch/BUILD.bazel # modified: submitqueue/orchestrator/controller/batch/batch_test.go # modified: submitqueue/orchestrator/controller/cancel/cancel.go # modified: submitqueue/orchestrator/controller/cancel/cancel_test.go # modified: submitqueue/orchestrator/controller/conclude/conclude.go # modified: submitqueue/orchestrator/controller/dependencyanalysis/dependencyanalysis.go # modified: submitqueue/orchestrator/controller/dependencyanalysis/dependencyanalysis_test.go # modified: submitqueue/orchestrator/controller/dlq/BUILD.bazel # modified: submitqueue/orchestrator/controller/dlq/README.md # modified: submitqueue/orchestrator/controller/dlq/batch.go # modified: submitqueue/orchestrator/controller/dlq/batch_test.go # modified: submitqueue/orchestrator/controller/dlq/dlq.go # modified: submitqueue/orchestrator/controller/dlq/dlq_test.go # renamed: submitqueue/orchestrator/controller/dlq/mergeconflictsignal.go -> submitqueue/orchestrator/controller/dlq/landconflictsignal.go # renamed: submitqueue/orchestrator/controller/dlq/mergeconflictsignal_test.go -> submitqueue/orchestrator/controller/dlq/landconflictsignal_test.go # renamed: submitqueue/orchestrator/controller/dlq/mergesignal.go -> submitqueue/orchestrator/controller/dlq/landsignal.go # renamed: submitqueue/orchestrator/controller/dlq/mergesignal_test.go -> submitqueue/orchestrator/controller/dlq/landsignal_test.go # modified: submitqueue/orchestrator/controller/dlq/request_test.go # modified: submitqueue/orchestrator/controller/dlq/speculate.go # renamed: submitqueue/orchestrator/controller/merge/BUILD.bazel -> submitqueue/orchestrator/controller/land/BUILD.bazel # renamed: submitqueue/orchestrator/controller/merge/merge.go -> submitqueue/orchestrator/controller/land/land.go # renamed: submitqueue/orchestrator/controller/merge/merge_test.go -> submitqueue/orchestrator/controller/land/land_test.go # renamed: submitqueue/orchestrator/controller/mergeconflictsignal/BUILD.bazel -> submitqueue/orchestrator/controller/landconflictsignal/BUILD.bazel # renamed: submitqueue/orchestrator/controller/mergeconflictsignal/mergeconflictsignal.go -> submitqueue/orchestrator/controller/landconflictsignal/landconflictsignal.go # renamed: submitqueue/orchestrator/controller/mergeconflictsignal/mergeconflictsignal_test.go -> submitqueue/orchestrator/controller/landconflictsignal/landconflictsignal_test.go # renamed: submitqueue/orchestrator/controller/mergesignal/BUILD.bazel -> submitqueue/orchestrator/controller/landsignal/BUILD.bazel # renamed: submitqueue/orchestrator/controller/mergesignal/mergesignal.go -> submitqueue/orchestrator/controller/landsignal/landsignal.go # renamed: submitqueue/orchestrator/controller/mergesignal/mergesignal_test.go -> submitqueue/orchestrator/controller/landsignal/landsignal_test.go # modified: submitqueue/orchestrator/controller/speculate/check.go # modified: submitqueue/orchestrator/controller/speculate/check_test.go # modified: submitqueue/orchestrator/controller/speculate/dispatch.go # modified: submitqueue/orchestrator/controller/speculate/doc.go # modified: submitqueue/orchestrator/controller/speculate/finalize.go # modified: submitqueue/orchestrator/controller/speculate/outcome.go # modified: submitqueue/orchestrator/controller/speculate/outcome_test.go # modified: submitqueue/orchestrator/controller/speculate/run.go # modified: submitqueue/orchestrator/controller/speculate/run_test.go # modified: submitqueue/orchestrator/controller/speculate/snapshot.go # modified: submitqueue/orchestrator/controller/speculate/speculate.go # modified: submitqueue/orchestrator/controller/speculate/speculate_test.go # modified: submitqueue/orchestrator/controller/start/BUILD.bazel # modified: submitqueue/orchestrator/controller/start/start_test.go # modified: submitqueue/orchestrator/controller/validate/BUILD.bazel # modified: submitqueue/orchestrator/controller/validate/validate.go # modified: submitqueue/orchestrator/controller/validate/validate_test.go # modified: submitqueue/orchestrator/pipeline.go # modified: test/e2e/runway/BUILD.bazel # modified: test/e2e/runway/harness_test.go # modified: test/e2e/runway/suite_test.go # modified: test/e2e/submitqueue/BUILD.bazel # modified: test/e2e/submitqueue/git_suite_test.go # modified: test/e2e/submitqueue/harness_test.go # modified: test/e2e/submitqueue/suite_test.go # modified: test/integration/extension/messagequeue/mysql/queue_test.go # modified: test/integration/submitqueue/extension/storage/BUILD.bazel # modified: test/integration/submitqueue/extension/storage/suite.go # modified: test/integration/submitqueue/gateway/BUILD.bazel # modified: test/integration/submitqueue/gateway/suite_test.go # modified: tool/gitsandbox/BUILD.bazel # modified: tool/gitsandbox/main.go # modified: tool/gitsandbox/main_test.go # modified: tool/linter/messageid/main_test.go # modified: tool/proto/BUILD.bazel # --- AGENTS.md | 10 +- Makefile | 22 +- README.md | 2 +- api/base/hook/README.md | 2 +- api/base/hook/hook_test.go | 4 +- api/base/hook/protopb/hook.pb.go | 9 +- .../proto/BUILD.bazel | 2 +- .../proto/landstrategy.proto} | 12 +- .../protopb/BUILD.bazel | 4 +- .../protopb/landstrategy.pb.go} | 62 +-- .../messagequeue/proto/messagequeue.proto | 2 +- api/runway/messagequeue/BUILD.bazel | 6 +- api/runway/messagequeue/README.md | 18 +- api/runway/messagequeue/{merge.go => land.go} | 30 +- .../{merge_test.go => land_test.go} | 40 +- api/runway/messagequeue/proto/BUILD.bazel | 2 +- .../proto/{merge.proto => land.proto} | 60 +-- api/runway/messagequeue/protopb/BUILD.bazel | 4 +- .../protopb/{merge.pb.go => land.pb.go} | 243 ++++----- api/runway/messagequeue/topics.go | 28 +- api/submitqueue/gateway/proto/gateway.proto | 8 +- api/submitqueue/gateway/protopb/BUILD.bazel | 2 +- api/submitqueue/gateway/protopb/gateway.pb.go | 18 +- .../gateway/protopb/gateway.pb.yarpc.go | 168 +++--- .../gateway/protopb/gateway_grpc.pb.go | 4 +- doc/howto/QUICKSTART.md | 20 +- doc/howto/TESTING.md | 2 +- doc/rfc/change-uri.md | 2 +- doc/rfc/consumer-gate.md | 8 +- doc/rfc/hook-framework.md | 12 +- doc/rfc/index.md | 6 +- doc/rfc/messagequeue-contract.md | 6 +- doc/rfc/runway/workflow.md | 76 +-- doc/rfc/sql-queue-rfc.md | 8 +- doc/rfc/stovepipe/steps/build.md | 2 +- doc/rfc/stovepipe/steps/process.md | 2 +- doc/rfc/stovepipe/workflow.md | 4 +- doc/rfc/submitqueue/extension-contract.md | 14 +- doc/rfc/submitqueue/modular-queue-wiring.md | 6 +- .../speculation-generator-best-first.md | 2 +- doc/rfc/submitqueue/speculation.md | 34 +- doc/rfc/submitqueue/workflow.md | 68 +-- .../BUILD.bazel | 4 +- platform/base/landstrategy/landstrategy.go | 35 ++ platform/base/mergestrategy/mergestrategy.go | 35 -- platform/errs/README.md | 2 +- .../extension/consumergate/file/store_test.go | 6 +- platform/extension/messagequeue/README.md | 2 +- .../extension/messagequeue/mysql/README.md | 4 +- .../messagequeue/mysql/ctl/README.md | 40 +- .../mysql/schema/queue_messages.sql | 2 +- platform/git/exec/gitexec.go | 2 +- platform/hook/README.md | 2 +- platform/hook/dlq.go | 2 +- platform/metrics/README.md | 2 +- platform/metrics/metrics.go | 2 +- platform/publish/publish_test.go | 10 +- runway/README.md | 22 +- runway/controller/dlq/dlq.go | 34 +- runway/controller/dlq/dlq_test.go | 20 +- runway/controller/{merge => land}/BUILD.bazel | 12 +- .../{merge/merge.go => land/land.go} | 72 +-- .../merge_test.go => land/land_test.go} | 122 ++--- .../BUILD.bazel | 12 +- .../landconflictcheck.go} | 72 +-- .../landconflictcheck_test.go} | 122 ++--- .../extension/{merger => lander}/BUILD.bazel | 4 +- .../{merger => lander}/fake/BUILD.bazel | 8 +- .../extension/{merger => lander}/fake/fake.go | 60 +-- .../{merger => lander}/fake/fake_test.go | 50 +- .../{merger => lander}/git/BUILD.bazel | 14 +- .../{merger => lander}/git/README.md | 34 +- .../{merger => lander}/git/author.go | 10 +- .../{merger => lander}/git/changeref.go | 16 +- .../git/git_lander.go} | 250 ++++----- .../git/git_lander_test.go} | 480 +++++++++--------- .../{merger => lander}/git/headbranch.go | 22 +- .../{merger => lander}/git/headbranch_test.go | 102 ++-- .../{merger => lander}/git/objects.go | 22 +- .../{merger/merger.go => lander/lander.go} | 44 +- .../{merger => lander}/mock/BUILD.bazel | 6 +- runway/extension/lander/mock/lander_mock.go | 112 ++++ .../{merger => lander}/noop/BUILD.bazel | 8 +- .../extension/{merger => lander}/noop/noop.go | 38 +- .../{merger => lander}/noop/noop_test.go | 26 +- runway/extension/merger/mock/merger_mock.go | 112 ---- service/README.md | 6 +- service/runway/README.md | 52 +- service/runway/server/BUILD.bazel | 20 +- service/runway/server/Dockerfile | 6 +- service/runway/server/checkout.go | 30 +- service/runway/server/checkout_test.go | 30 +- service/runway/server/config.go | 144 +++--- service/runway/server/config_test.go | 146 +++--- service/runway/server/docker-compose.yml | 10 +- service/runway/server/gitruntime.go | 18 +- service/runway/server/main.go | 290 +++++------ service/submitqueue/README.md | 2 +- service/submitqueue/demo/provider/README.md | 14 +- .../submitqueue/demo/provider/fake/land.yaml | 13 + .../submitqueue/demo/provider/fake/merge.yaml | 13 - .../submitqueue/demo/provider/git/BUILD.bazel | 2 +- .../provider/git/{merge.yaml => land.yaml} | 18 +- .../demo/provider/git/profiles.yaml | 8 +- .../provider/github/{merge.yaml => land.yaml} | 10 +- service/submitqueue/demo/requests/BUILD.bazel | 8 +- service/submitqueue/demo/requests/fake.go | 2 +- service/submitqueue/demo/requests/git.go | 2 +- service/submitqueue/demo/requests/main.go | 18 +- .../submitqueue/demo/requests/main_test.go | 10 +- service/submitqueue/docker-compose.fake.yml | 10 +- service/submitqueue/docker-compose.git.yml | 28 +- .../submitqueue/docker-compose.provider.yml | 10 +- service/submitqueue/docker-compose.yml | 2 +- service/submitqueue/gateway/client/main.go | 2 +- .../gateway/server/mapper/BUILD.bazel | 8 +- .../submitqueue/gateway/server/mapper/land.go | 34 +- .../gateway/server/mapper/land_test.go | 32 +- .../submitqueue/gateway/server/queues.yaml | 2 +- .../submitqueue/orchestrator/server/config.go | 2 +- .../orchestrator/server/config_test.go | 2 +- stovepipe/README.md | 2 +- submitqueue/client/BUILD.bazel | 2 +- submitqueue/client/land.go | 18 +- submitqueue/core/batch/transition_test.go | 2 +- submitqueue/core/changeset/README.md | 2 +- submitqueue/core/changeset/changeset.go | 6 +- submitqueue/core/request/request_test.go | 4 +- submitqueue/core/topickey/topickey.go | 8 +- submitqueue/entity/BUILD.bazel | 6 +- submitqueue/entity/batch.go | 18 +- submitqueue/entity/batch_test.go | 4 +- submitqueue/entity/land.go | 4 +- .../{merge_result.go => land_check_result.go} | 12 +- submitqueue/entity/land_test.go | 14 +- submitqueue/entity/push_result.go | 2 +- submitqueue/entity/request.go | 10 +- submitqueue/entity/request_log.go | 6 +- submitqueue/entity/request_test.go | 14 +- submitqueue/entity/speculation.go | 2 +- .../extension/changeprovider/git/README.md | 2 +- .../{mergechecker => landchecker}/BUILD.bazel | 4 +- .../fake/BUILD.bazel | 6 +- .../fake/fake.go | 36 +- .../fake/fake_test.go | 36 +- .../github/BUILD.bazel | 6 +- .../github/checker.go | 40 +- .../github/checker_test.go | 46 +- .../github/graphql.go | 0 .../github/graphql_test.go | 0 .../github/validate.go | 0 .../github/validate_test.go | 2 +- .../landchecker.go} | 24 +- .../mock/BUILD.bazel | 6 +- .../mock/landchecker_mock.go} | 40 +- .../speculation/allocator/sticky/sticky.go | 2 +- .../speculation/generator/bestfirst/README.md | 2 +- .../generator/bestfirst/bestfirst_test.go | 12 +- .../extension/speculation/scorer/scorer.go | 2 +- .../speculation/speculator/README.md | 2 +- .../speculation/speculator/speculator.go | 2 +- .../speculation/speculator/standard/README.md | 2 +- .../extension/storage/mysql/BUILD.bazel | 2 +- .../storage/mysql/batch_store_test.go | 2 +- .../storage/mysql/request_store_test.go | 8 +- submitqueue/gateway/controller/BUILD.bazel | 2 +- submitqueue/gateway/controller/cancel.go | 4 +- submitqueue/gateway/controller/land_test.go | 8 +- submitqueue/orchestrator/BUILD.bazel | 6 +- submitqueue/orchestrator/README.md | 10 +- submitqueue/orchestrator/controller/README.md | 2 +- .../orchestrator/controller/batch/BUILD.bazel | 2 +- .../controller/batch/batch_test.go | 4 +- .../orchestrator/controller/cancel/cancel.go | 14 +- .../controller/cancel/cancel_test.go | 2 +- .../controller/conclude/conclude.go | 2 +- .../dependencyanalysis/dependencyanalysis.go | 4 +- .../dependencyanalysis_test.go | 2 +- .../orchestrator/controller/dlq/BUILD.bazel | 8 +- .../orchestrator/controller/dlq/README.md | 2 +- .../orchestrator/controller/dlq/batch.go | 4 +- .../orchestrator/controller/dlq/batch_test.go | 16 +- .../orchestrator/controller/dlq/dlq.go | 2 +- .../orchestrator/controller/dlq/dlq_test.go | 2 +- ...onflictsignal.go => landconflictsignal.go} | 34 +- ...nal_test.go => landconflictsignal_test.go} | 20 +- .../dlq/{mergesignal.go => landsignal.go} | 30 +- ...mergesignal_test.go => landsignal_test.go} | 22 +- .../controller/dlq/request_test.go | 2 +- .../orchestrator/controller/dlq/speculate.go | 2 +- .../controller/{merge => land}/BUILD.bazel | 14 +- .../{merge/merge.go => land/land.go} | 92 ++-- .../merge_test.go => land/land_test.go} | 62 +-- .../BUILD.bazel | 6 +- .../landconflictsignal.go} | 36 +- .../landconflictsignal_test.go} | 24 +- .../{mergesignal => landsignal}/BUILD.bazel | 6 +- .../landsignal.go} | 50 +- .../landsignal_test.go} | 42 +- .../controller/speculate/check.go | 10 +- .../controller/speculate/check_test.go | 4 +- .../controller/speculate/dispatch.go | 2 +- .../orchestrator/controller/speculate/doc.go | 14 +- .../controller/speculate/finalize.go | 46 +- .../controller/speculate/outcome.go | 30 +- .../controller/speculate/outcome_test.go | 46 +- .../orchestrator/controller/speculate/run.go | 6 +- .../controller/speculate/run_test.go | 126 ++--- .../controller/speculate/snapshot.go | 2 +- .../controller/speculate/speculate.go | 14 +- .../controller/speculate/speculate_test.go | 12 +- .../orchestrator/controller/start/BUILD.bazel | 2 +- .../controller/start/start_test.go | 20 +- .../controller/validate/BUILD.bazel | 8 +- .../controller/validate/validate.go | 50 +- .../controller/validate/validate_test.go | 54 +- submitqueue/orchestrator/pipeline.go | 42 +- test/e2e/runway/BUILD.bazel | 2 +- test/e2e/runway/harness_test.go | 50 +- test/e2e/runway/suite_test.go | 68 +-- test/e2e/submitqueue/BUILD.bazel | 4 +- test/e2e/submitqueue/git_suite_test.go | 92 ++-- test/e2e/submitqueue/harness_test.go | 4 +- test/e2e/submitqueue/suite_test.go | 146 ++++-- .../messagequeue/mysql/queue_test.go | 4 +- .../submitqueue/extension/storage/BUILD.bazel | 2 +- .../submitqueue/extension/storage/suite.go | 28 +- .../submitqueue/gateway/BUILD.bazel | 2 +- .../submitqueue/gateway/suite_test.go | 4 +- tool/gitsandbox/BUILD.bazel | 4 +- tool/gitsandbox/main.go | 12 +- tool/gitsandbox/main_test.go | 2 +- tool/linter/messageid/main_test.go | 4 +- tool/proto/BUILD.bazel | 14 +- 234 files changed, 3033 insertions(+), 2949 deletions(-) rename api/base/{mergestrategy => landstrategy}/proto/BUILD.bazel (67%) rename api/base/{mergestrategy/proto/mergestrategy.proto => landstrategy/proto/landstrategy.proto} (85%) rename api/base/{mergestrategy => landstrategy}/protopb/BUILD.bazel (73%) rename api/base/{mergestrategy/protopb/mergestrategy.pb.go => landstrategy/protopb/landstrategy.pb.go} (70%) rename api/runway/messagequeue/{merge.go => land.go} (78%) rename api/runway/messagequeue/{merge_test.go => land_test.go} (78%) rename api/runway/messagequeue/proto/{merge.proto => land.proto} (70%) rename api/runway/messagequeue/protopb/{merge.pb.go => land.pb.go} (63%) rename platform/base/{mergestrategy => landstrategy}/BUILD.bazel (55%) create mode 100644 platform/base/landstrategy/landstrategy.go delete mode 100644 platform/base/mergestrategy/mergestrategy.go rename runway/controller/{merge => land}/BUILD.bazel (85%) rename runway/controller/{merge/merge.go => land/land.go} (65%) rename runway/controller/{merge/merge_test.go => land/land_test.go} (70%) rename runway/controller/{mergeconflictcheck => landconflictcheck}/BUILD.bazel (83%) rename runway/controller/{mergeconflictcheck/mergeconflictcheck.go => landconflictcheck/landconflictcheck.go} (63%) rename runway/controller/{mergeconflictcheck/mergeconflictcheck_test.go => landconflictcheck/landconflictcheck_test.go} (67%) rename runway/extension/{merger => lander}/BUILD.bazel (66%) rename runway/extension/{merger => lander}/fake/BUILD.bazel (76%) rename runway/extension/{merger => lander}/fake/fake.go (64%) rename runway/extension/{merger => lander}/fake/fake_test.go (71%) rename runway/extension/{merger => lander}/git/BUILD.bazel (82%) rename runway/extension/{merger => lander}/git/README.md (73%) rename runway/extension/{merger => lander}/git/author.go (92%) rename runway/extension/{merger => lander}/git/changeref.go (89%) rename runway/extension/{merger/git/git_merger.go => lander/git/git_lander.go} (82%) rename runway/extension/{merger/git/git_merger_test.go => lander/git/git_lander_test.go} (76%) rename runway/extension/{merger => lander}/git/headbranch.go (93%) rename runway/extension/{merger => lander}/git/headbranch_test.go (73%) rename runway/extension/{merger => lander}/git/objects.go (85%) rename runway/extension/{merger/merger.go => lander/lander.go} (55%) rename runway/extension/{merger => lander}/mock/BUILD.bazel (61%) create mode 100644 runway/extension/lander/mock/lander_mock.go rename runway/extension/{merger => lander}/noop/BUILD.bazel (74%) rename runway/extension/{merger => lander}/noop/noop.go (61%) rename runway/extension/{merger => lander}/noop/noop_test.go (81%) delete mode 100644 runway/extension/merger/mock/merger_mock.go create mode 100644 service/submitqueue/demo/provider/fake/land.yaml delete mode 100644 service/submitqueue/demo/provider/fake/merge.yaml rename service/submitqueue/demo/provider/git/{merge.yaml => land.yaml} (80%) rename service/submitqueue/demo/provider/github/{merge.yaml => land.yaml} (90%) rename submitqueue/entity/{merge_result.go => land_check_result.go} (69%) rename submitqueue/extension/{mergechecker => landchecker}/BUILD.bazel (83%) rename submitqueue/extension/{mergechecker => landchecker}/fake/BUILD.bazel (82%) rename submitqueue/extension/{mergechecker => landchecker}/fake/fake.go (59%) rename submitqueue/extension/{mergechecker => landchecker}/fake/fake_test.go (72%) rename submitqueue/extension/{mergechecker => landchecker}/github/BUILD.bazel (88%) rename submitqueue/extension/{mergechecker => landchecker}/github/checker.go (76%) rename submitqueue/extension/{mergechecker => landchecker}/github/checker_test.go (78%) rename submitqueue/extension/{mergechecker => landchecker}/github/graphql.go (100%) rename submitqueue/extension/{mergechecker => landchecker}/github/graphql_test.go (100%) rename submitqueue/extension/{mergechecker => landchecker}/github/validate.go (100%) rename submitqueue/extension/{mergechecker => landchecker}/github/validate_test.go (99%) rename submitqueue/extension/{mergechecker/mergechecker.go => landchecker/landchecker.go} (59%) rename submitqueue/extension/{mergechecker => landchecker}/mock/BUILD.bazel (71%) rename submitqueue/extension/{mergechecker/mock/mergechecker_mock.go => landchecker/mock/landchecker_mock.go} (60%) rename submitqueue/orchestrator/controller/dlq/{mergeconflictsignal.go => landconflictsignal.go} (73%) rename submitqueue/orchestrator/controller/dlq/{mergeconflictsignal_test.go => landconflictsignal_test.go} (64%) rename submitqueue/orchestrator/controller/dlq/{mergesignal.go => landsignal.go} (76%) rename submitqueue/orchestrator/controller/dlq/{mergesignal_test.go => landsignal_test.go} (70%) rename submitqueue/orchestrator/controller/{merge => land}/BUILD.bazel (84%) rename submitqueue/orchestrator/controller/{merge/merge.go => land/land.go} (71%) rename submitqueue/orchestrator/controller/{merge/merge_test.go => land/land_test.go} (87%) rename submitqueue/orchestrator/controller/{mergeconflictsignal => landconflictsignal}/BUILD.bazel (93%) rename submitqueue/orchestrator/controller/{mergeconflictsignal/mergeconflictsignal.go => landconflictsignal/landconflictsignal.go} (87%) rename submitqueue/orchestrator/controller/{mergeconflictsignal/mergeconflictsignal_test.go => landconflictsignal/landconflictsignal_test.go} (90%) rename submitqueue/orchestrator/controller/{mergesignal => landsignal}/BUILD.bazel (94%) rename submitqueue/orchestrator/controller/{mergesignal/mergesignal.go => landsignal/landsignal.go} (83%) rename submitqueue/orchestrator/controller/{mergesignal/mergesignal_test.go => landsignal/landsignal_test.go} (90%) diff --git a/AGENTS.md b/AGENTS.md index f95bf9bbc..da7a8f2ca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,7 +58,7 @@ submitqueue/ # repo root (Go module github.com/uber/submi │ ├── extension/ # Stovepipe-specific extension contracts and implementations │ └── core/ # Stovepipe-internal queue contracts and shared infrastructure ├── runway/ # Runway domain (single service — the domain *is* the service) -│ └── controller/ # Runway service controllers (consumes the merge queues; no gateway/orchestrator split) +│ └── controller/ # Runway service controllers (consumes the land queues; no gateway/orchestrator split) ├── tool/ # Development and CI tooling ├── service/ # Runnable server/client wiring (entry points + Docker Compose) │ ├── submitqueue/ # Runnable SubmitQueue servers/clients + Docker Compose @@ -113,7 +113,7 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er Controllers receive `consumer.Delivery` (a subset interface without Ack/Nack) to enforce separation of business logic from queue mechanics. `delivery.Hold(delayMs)` requests delayed redelivery without consuming retry budget; the controller must then return `nil`. -**Queue payloads: IDs within a boundary, full payloads across one.** When producer and consumer share a store (for example SubmitQueue's `build`→`buildsignal` flow), put only the entity **ID** on the queue and reload from storage (the store is the source of truth, messages stay small, redelivery is idempotent). Reloading from storage is what makes the publish ordering load-bearing — see "Persist before you publish" above. When a queue **crosses a service boundary** (for example SubmitQueue validation or merge handing work to Runway), publish the **full payload** the consumer needs, and have the **client own the correlation ID** so it can match the asynchronous result back to the work it is tracking. The queue's **owner defines the wire contract and topic keys** (in its own domain package); the other side imports them. +**Queue payloads: IDs within a boundary, full payloads across one.** When producer and consumer share a store (same service — e.g. `build`→`buildsignal`, `validate`→`landconflict`), put only the entity **ID** on the queue and reload from storage (the store is the source of truth, messages stay small, redelivery is idempotent). Reloading from storage is what makes the publish ordering load-bearing — see "Persist before you publish" above. When a queue **crosses a service boundary** (the consumer cannot read the producer's store — e.g. orchestrator→runway), publish the **full payload** the consumer needs, and have the **client own the correlation ID** so it can match the async result back to the work it is tracking. The queue's **owner defines the wire contract and topic keys** (in its own domain package); the other side imports them. ### Entities @@ -163,7 +163,7 @@ When in doubt, ask: *"If the next implementation were DynamoDB / Kafka / Bigtabl Paths follow the directory layout: shared packages live under `platform/` at the repo root; domain code nests under `submitqueue/`, `stovepipe/`, and other domain folders. - RPC Controllers: `github.com/uber/submitqueue/{domain}/{service}/controller` (e.g. `.../submitqueue/gateway/controller`; single-service domains drop the `{service}` segment, e.g. `.../runway/controller`) -- Queue Controllers: `github.com/uber/submitqueue/{domain}/{service}/controller/{step}` (single-service: `.../runway/controller/{step}`, e.g. `.../runway/controller/merge`) +- Queue Controllers: `github.com/uber/submitqueue/{domain}/{service}/controller/{step}` (single-service: `.../runway/controller/{step}`, e.g. `.../runway/controller/land`) - Proto (generated): `github.com/uber/submitqueue/api/{domain}/{service}/protopb` (single-service: `.../api/{domain}/protopb`, e.g. `.../api/runway/protopb`) - Queue contracts: external `github.com/uber/submitqueue/api/{domain}/messagequeue`; internal `github.com/uber/submitqueue/{domain}/core/messagequeue` - Domain entities: `github.com/uber/submitqueue/{domain}/entity` (e.g. `.../submitqueue/entity`) @@ -198,7 +198,7 @@ To add a new `.proto` to a service, drop it in the service's `api/{domain}/{serv New queue contracts are defined in **proto3** (`.proto` under `proto/`, generated Go in `protopb/` as the binding) and serialized as **protobuf JSON** (protojson) so the queue keeps storing self-describing JSON. Location follows audience: external/cross-domain contracts go under `api/{domain}/messagequeue/`; internal contracts (used only within the owning domain) go under `{domain}/core/messagequeue/`. Bazel `visibility` enforces the split — internal targets are domain-scoped, `api/` targets are public. -For proto-backed contracts, the message types are generated and the contract package adds generic `protojson` glue — `Marshal(m)` / `Unmarshal[T](b, m)` — owning the wire conventions: `UseProtoNames` (snake_case fields), UPPER_SNAKE enum values, int64-as-string, and unknown fields discarded on read (additive evolution). The topic key(s) carrying a message are declared on the message via the `topic_keys` proto option — a `google.protobuf.MessageOptions` extension defined in `api/base/messagequeue`. A topic key is a stable logical name, not a concrete wire topic; each implementer maps it to its backend's topic name, and a `TopicKeys(msg)` reflection helper reads the option back. It is contract metadata, not the hot path — publish/consume still routes on `consumer.TopicKey` + `TopicRegistry`. The contract package owns both halves: the proto payload and the `TopicKey` constants for its topic keys. A contract test round-trips the payloads and asserts every topic key is bound to exactly one message. Shared field types (`Change`, `Strategy`) are shared protos under `api/base/{change,mergestrategy}`. `api/runway/messagequeue/` and `stovepipe/core/messagequeue/` are current examples. +The message types are generated; the contract package adds only generic `protojson` glue — `Marshal(m)` / `Unmarshal[T](b, m)` — owning the wire conventions: `UseProtoNames` (snake_case fields), UPPER_SNAKE enum values, int64-as-string, unknown fields discarded on read (additive evolution). The topic key(s) carrying a message are declared on the message via the `topic_keys` proto option — a `google.protobuf.MessageOptions` extension defined in `api/base/messagequeue`. A topic key is a stable logical name, not a concrete wire topic; each implementer maps it to its backend's topic name, and a `TopicKeys(msg)` reflection helper reads the option back. It is contract metadata, not the hot path — publish/consume still routes on `consumer.TopicKey` + `TopicRegistry`. The contract package owns both halves: the proto payload and the `TopicKey` constants for its topic keys. A contract test round-trips the payloads and asserts every topic key is bound to exactly one message. Shared field types (`Change`, `Strategy`) are shared protos under `api/base/{change,landstrategy}`. `api/runway/messagequeue/` is the reference example. SubmitQueue's internal pipeline predates the proto-backed convention. It continues to serialize domain entities with `encoding/json` and declares its logical keys in `submitqueue/core/topickey/`. Do not convert or mix these wire formats incidentally; treat migration as an explicit compatibility change. @@ -376,6 +376,6 @@ Errors are classified by origin (user vs infra) and retryability. The framework **Key rules:** 1. **Non-retryable by default** — a plain `fmt.Errorf(...)` is non-retryable. Retryability is opted into explicitly, but that decision is almost always made by a classifier, not a controller (see rule 4). 2. **Infra by default** — any error not wrapped with `NewUserError` is infra. There is no `NewInfraError`. -3. **Extensions return plain errors** — extension interfaces (`MergeChecker`, `Storage`, `Publisher`) return standard `error` values with their own domain sentinels (e.g. `storage.ErrNotFound`). They do NOT classify errors as user or infra. +3. **Extensions return plain errors** — extension interfaces (`LandChecker`, `Storage`, `Publisher`) return standard `error` values with their own domain sentinels (e.g. `storage.ErrNotFound`). They do NOT classify errors as user or infra. 4. **Classifiers do the bulk of classification; controllers override only with knowledge a classifier lacks** — primary pipeline consumers compose per-backend classifiers into `errs.NewClassifierProcessor(...)`; the processor runs once per chain in the consumer and decides retryability from the raw error. So the common case is a controller returning the raw error (`fmt.Errorf("...: %w", err)`) and letting the classifier verdict stand. Reserve an explicit `errs.New*Error` wrap for the rare case where the controller knows something the classifier cannot infer from the error value alone (e.g. `storage.ErrNotFound` meaning "user asked for a missing resource" *in this call site*). Do **not** wrap a failure as retryable just because replaying it is convenient (e.g. a failed queue publish) — that turns permanent failures into infinite retries instead of dead-lettering. DLQ reconciliation consumers use `errs.AlwaysRetryableProcessor` instead. See [platform/errs/README.md](platform/errs/README.md). 5. **Error chain works end-to-end** — extensions wrap custom errors, controllers wrap with `errs.New*Error`, and `errors.Is`/`errors.As` walks the full chain. diff --git a/Makefile b/Makefile index 25e5097ed..2403b8a11 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ GOIMPORTS_VERSION ?= v0.33.0 # (the out_dir convention in tool/proto/BUILD.bazel) and copied back here. A # package may hold multiple .proto files (e.g. an RPC contract plus messagequeue # contracts); all generated stubs land in the same protopb/ dir. -PROTO_PACKAGES = api/base/change api/base/hook api/base/mergestrategy api/base/messagequeue api/runway/messagequeue api/runway api/submitqueue/gateway api/submitqueue/orchestrator api/stovepipe stovepipe/core/messagequeue +PROTO_PACKAGES = api/base/change api/base/hook api/base/landstrategy api/base/messagequeue api/runway/messagequeue api/runway api/submitqueue/gateway api/submitqueue/orchestrator api/stovepipe stovepipe/core/messagequeue # Set REPO_ROOT for docker-compose export REPO_ROOT := $(shell pwd) @@ -46,7 +46,7 @@ export REPO_ROOT := $(shell pwd) # path, so adding a provider is mostly adding a directory — see # service/submitqueue/demo/provider/README.md. # -# fake a change is a URI; nothing merges anywhere. Needs nothing. +# fake a change is a URI; nothing lands anywhere. Needs nothing. # git branches in a bare repository on disk; real fetch, cherry-pick, push. # github real pull requests. Needs a repository and GITHUB_TOKEN. PROVIDER ?= fake @@ -60,7 +60,7 @@ PROVIDER_COMPOSE_FILE_git = service/submitqueue/docker-compose.git.yml PROVIDER_COMPOSE_FILE_github = service/submitqueue/docker-compose.provider.yml PROVIDER_COMPOSE_FILE = $(PROVIDER_COMPOSE_FILE_$(PROVIDER)) -# Where PROVIDER=git keeps the bare repository it merges into. Outside the +# Where PROVIDER=git keeps the bare repository it lands into. Outside the # repository, so a demo leaves nothing in a checkout, and bind-mounted rather # than kept in a volume so `git log` on the host can show what landed. # @@ -118,7 +118,7 @@ endef # # The two have to agree. A change minted for one provider is meaningless to a # stack wired to another: fake changes point at no repository, so a stack -# running the git merger rejects every one of them as a commit it cannot find, +# running the git lander rejects every one of them as a commit it cannot find, # and fifty requests fail identically for a reason that is nowhere in the error. # The stack knows which provider it has — it is mounted at /etc/submitqueue — # so a run that was not told otherwise asks it rather than guessing. @@ -263,10 +263,10 @@ demo-requests: ## Create N changes, enqueue each as it is created, and watch (PR deps: tidy-go ## Download and tidy Go dependencies @echo "Dependencies installed!" -e2e-git-test: ## Run the hermetic git E2E (real merger against a bare repo; no credentials) +e2e-git-test: ## Run the hermetic git E2E (real lander against a bare repo; no credentials) @echo "Running hermetic git end-to-end tests..." @$(BAZEL) test //test/e2e/submitqueue:go_default_test --test_output=errors \ - --test_filter='TestGitMergeE2E' + --test_filter='TestGitLandE2E' e2e-test: ## Run end-to-end tests (hermetic; Bazel builds all inputs; runs in parallel) @echo "Running end-to-end tests (parallel)..." @@ -403,7 +403,7 @@ local-init-submitqueue-schemas: ## Manually apply all database schemas @echo "✅ All schemas applied successfully" local-init-runway-queue-schema: ## Apply queue schema only (mysql-queue) for Runway compose stacks - @echo "Applying queue schema to mysql-queue (Runway; consumes the merge queues)..." + @echo "Applying queue schema to mysql-queue (Runway; consumes the land queues)..." @for file in platform/extension/messagequeue/mysql/schema/*.sql; do \ echo " - Applying $$(basename $$file)..."; \ docker exec -i $(RUNWAY_LOCAL_PROJECT)-mysql-queue-1 mysql -uroot -proot submitqueue < $$file 2>&1 | grep -v "Using a password" || true; \ @@ -494,8 +494,8 @@ local-submitqueue-restart: build-all-linux ## Restart all services (rebuild and local-submitqueue-start: build-all-linux ## Start full stack (PROVIDER=fake|git|github; github needs GITHUB_TOKEN) @echo "Starting full stack against provider '$(PROVIDER)' ($(SQ_PROVIDER_CONFIG_DIR))..." - @test -f "$(SQ_PROVIDER_CONFIG_DIR)/merge.yaml" \ - || { echo "No such provider '$(PROVIDER)': $(SQ_PROVIDER_CONFIG_DIR)/merge.yaml not found"; exit 2; } + @test -f "$(SQ_PROVIDER_CONFIG_DIR)/land.yaml" \ + || { echo "No such provider '$(PROVIDER)': $(SQ_PROVIDER_CONFIG_DIR)/land.yaml not found"; exit 2; } @test -n "$(PROVIDER_COMPOSE_FILE)" \ || { echo "Provider '$(PROVIDER)' has no compose overlay; add PROVIDER_COMPOSE_FILE_$(PROVIDER) to the Makefile"; exit 2; } @if [ "$(PROVIDER)" = "git" ]; then \ @@ -519,7 +519,7 @@ local-submitqueue-start: build-all-linux ## Start full stack (PROVIDER=fake|git| @echo "" @echo "Gateway gRPC port: $$(docker port $(SUBMITQUEUE_LOCAL_PROJECT)-gateway-service-1 8080 2>/dev/null | cut -d: -f2 || echo 'unknown')" @if [ "$(PROVIDER)" = "git" ]; then \ - echo "Merge target: $(SQ_GIT_SANDBOX_DIR)/sandbox.git"; \ + echo "Land target: $(SQ_GIT_SANDBOX_DIR)/sandbox.git"; \ fi @echo "" @echo "Generate traffic with:" @@ -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/landchecker/... ./runway/extension/lander/... ./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/README.md b/README.md index 6c7fb86a7..130407284 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE) [![Slack](https://img.shields.io/badge/Slack-join%20the%20community-4A154B?logo=slack&logoColor=white)](https://join.slack.com/t/submitqueue/shared_invite/zt-46gkqj682-7zcQphxm2pYqkjDo9lbmYA) -SubmitQueue is a high-performance speculative merge queue that keeps your trunk consistently green at scale. Rather than validating changes one at a time, SubmitQueue speculatively rebases and validates multiple changes in parallel against predicted future states of HEAD. When validations pass, changes land automatically. When they fail, SubmitQueue isolates the offending change and retries the rest — all without human intervention. +SubmitQueue is a high-performance speculative submission queue that keeps your trunk consistently green at scale. Rather than validating changes one at a time, SubmitQueue speculatively rebases and validates multiple changes in parallel against predicted future states of HEAD. When validations pass, changes land automatically. When they fail, SubmitQueue isolates the offending change and retries the rest — all without human intervention. Designed for large monorepos and fast-moving teams where concurrent changes can introduce subtle conflicts and destabilize builds. diff --git a/api/base/hook/README.md b/api/base/hook/README.md index 461042eff..ad60b5a70 100644 --- a/api/base/hook/README.md +++ b/api/base/hook/README.md @@ -30,7 +30,7 @@ A domain with no versioned entities (Runway holds no durable state of its own) p ## Payload -Shaped per `type` by the domain that publishes it, add-only, and documented by that domain. It must carry the subject's id, and it must carry any fact recorded nowhere else — merge step outcomes, build failure detail — because for those the event is the only durable record. +Shaped per `type` by the domain that publishes it, add-only, and documented by that domain. It must carry the subject's id, and it must carry any fact recorded nowhere else — land step outcomes, build failure detail — because for those the event is the only durable record. It must **not** be an entity snapshot. A snapshot is stale the moment it is redelivered, it competes with the store as a source of truth, and it drags a domain's schema into a contract shared by every domain. Hooks resolve entities from their stores. diff --git a/api/base/hook/hook_test.go b/api/base/hook/hook_test.go index f3a3b58c3..1f8abce6d 100644 --- a/api/base/hook/hook_test.go +++ b/api/base/hook/hook_test.go @@ -47,9 +47,9 @@ func TestHookEventRoundTrip(t *testing.T) { }), }, "unversioned": { - Id: "runway/merge.completed/queue-a-42/msg-9/0", + Id: "runway/land.completed/queue-a-42/msg-9/0", Source: "runway", - Type: "merge.completed", + Type: "land.completed", TimestampMs: 1722800012345, Payload: mustStruct(t, map[string]any{"request_id": "queue-a/42"}), }, diff --git a/api/base/hook/protopb/hook.pb.go b/api/base/hook/protopb/hook.pb.go index cc0046c49..d84f9b0fc 100644 --- a/api/base/hook/protopb/hook.pb.go +++ b/api/base/hook/protopb/hook.pb.go @@ -40,19 +40,18 @@ const ( // HookEvent is one fire-and-forget lifecycle event. Every domain publishes this // same shape to its own hook topic, so a sink that consumes several domains -// reads one schema rather than one per producer. See api/base/hook/README.md. +// reads one schema rather than one per producer. type HookEvent struct { state protoimpl.MessageState `protogen:"open.v1"` // id is the opaque identity of this occurrence, derived from the transition // it describes so that replaying the transition mints the same id. It is // the queue's dedupe key and a hook's idempotency key, and is never parsed. Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - // source is the domain that produced the event: "submitqueue", - // "stovepipe", ... An open string rather than an enum so a new producer - // does not break existing consumers. + // source is the domain that produced the event. An open string rather than + // an enum so a new producer does not break existing consumers. Source string `protobuf:"bytes,2,opt,name=source,proto3" json:"source,omitempty"` // type is what happened, as one dotted open string: "request.landed", - // "batch.failed", ... It is the only dimension a consumer filters on, and + // "batch.failed", etc. It is the only dimension a consumer filters on, and // open for the same reason as source. Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` // timestamp_ms is when the occurrence happened, in milliseconds since the diff --git a/api/base/mergestrategy/proto/BUILD.bazel b/api/base/landstrategy/proto/BUILD.bazel similarity index 67% rename from api/base/mergestrategy/proto/BUILD.bazel rename to api/base/landstrategy/proto/BUILD.bazel index cca226aff..3c1aeb03d 100644 --- a/api/base/mergestrategy/proto/BUILD.bazel +++ b/api/base/landstrategy/proto/BUILD.bazel @@ -1,4 +1,4 @@ exports_files( - ["mergestrategy.proto"], + ["landstrategy.proto"], visibility = ["//tool/proto:__pkg__"], ) diff --git a/api/base/mergestrategy/proto/mergestrategy.proto b/api/base/landstrategy/proto/landstrategy.proto similarity index 85% rename from api/base/mergestrategy/proto/mergestrategy.proto rename to api/base/landstrategy/proto/landstrategy.proto index 0037ffaa1..5e4a2a01f 100644 --- a/api/base/mergestrategy/proto/mergestrategy.proto +++ b/api/base/landstrategy/proto/landstrategy.proto @@ -14,19 +14,19 @@ syntax = "proto3"; -package uber.base.mergestrategy; +package uber.base.landstrategy; -option go_package = "github.com/uber/submitqueue/api/base/mergestrategy/protopb"; +option go_package = "github.com/uber/submitqueue/api/base/landstrategy/protopb"; option java_multiple_files = true; -option java_outer_classname = "MergeStrategyProto"; -option java_package = "com.uber.submitqueue.base.mergestrategy"; +option java_outer_classname = "LandStrategyProto"; +option java_package = "com.uber.submitqueue.base.landstrategy"; // Strategy defines the possible source control integration methods — how a change // is integrated into the target branch. // -// This is the shared wire contract for merge strategy, reused across SubmitQueue +// This is the shared wire contract for land strategy, reused across SubmitQueue // and other repo-local domains — the proto-level analog of the -// platform/base/mergestrategy Go entity. Domains import it rather than redefining their own. +// platform/base/landstrategy Go entity. Domains import it rather than redefining their own. // // A strategy is chosen once for a change and applies to every URI that change // carries, the same way to each. Since one URI is one unit of change (see diff --git a/api/base/mergestrategy/protopb/BUILD.bazel b/api/base/landstrategy/protopb/BUILD.bazel similarity index 73% rename from api/base/mergestrategy/protopb/BUILD.bazel rename to api/base/landstrategy/protopb/BUILD.bazel index c3d0a5c30..343896628 100644 --- a/api/base/mergestrategy/protopb/BUILD.bazel +++ b/api/base/landstrategy/protopb/BUILD.bazel @@ -2,8 +2,8 @@ load("@rules_go//go:def.bzl", "go_library") go_library( name = "go_default_library", - srcs = ["mergestrategy.pb.go"], - importpath = "github.com/uber/submitqueue/api/base/mergestrategy/protopb", + srcs = ["landstrategy.pb.go"], + importpath = "github.com/uber/submitqueue/api/base/landstrategy/protopb", visibility = ["//visibility:public"], deps = [ "@org_golang_google_protobuf//reflect/protoreflect:go_default_library", diff --git a/api/base/mergestrategy/protopb/mergestrategy.pb.go b/api/base/landstrategy/protopb/landstrategy.pb.go similarity index 70% rename from api/base/mergestrategy/protopb/mergestrategy.pb.go rename to api/base/landstrategy/protopb/landstrategy.pb.go index 026067481..9427b23be 100644 --- a/api/base/mergestrategy/protopb/mergestrategy.pb.go +++ b/api/base/landstrategy/protopb/landstrategy.pb.go @@ -16,7 +16,7 @@ // versions: // protoc-gen-go v1.36.10 // protoc v5.29.3 -// source: mergestrategy.proto +// source: landstrategy.proto package protopb @@ -39,9 +39,9 @@ const ( // Strategy defines the possible source control integration methods — how a change // is integrated into the target branch. // -// This is the shared wire contract for merge strategy, reused across SubmitQueue +// This is the shared wire contract for land strategy, reused across SubmitQueue // and other repo-local domains — the proto-level analog of the -// platform/base/mergestrategy Go entity. Domains import it rather than redefining their own. +// platform/base/landstrategy Go entity. Domains import it rather than redefining their own. // // A strategy is chosen once for a change and applies to every URI that change // carries, the same way to each. Since one URI is one unit of change (see @@ -101,11 +101,11 @@ func (x Strategy) String() string { } func (Strategy) Descriptor() protoreflect.EnumDescriptor { - return file_mergestrategy_proto_enumTypes[0].Descriptor() + return file_landstrategy_proto_enumTypes[0].Descriptor() } func (Strategy) Type() protoreflect.EnumType { - return &file_mergestrategy_proto_enumTypes[0] + return &file_landstrategy_proto_enumTypes[0] } func (x Strategy) Number() protoreflect.EnumNumber { @@ -114,40 +114,40 @@ func (x Strategy) Number() protoreflect.EnumNumber { // Deprecated: Use Strategy.Descriptor instead. func (Strategy) EnumDescriptor() ([]byte, []int) { - return file_mergestrategy_proto_rawDescGZIP(), []int{0} + return file_landstrategy_proto_rawDescGZIP(), []int{0} } -var File_mergestrategy_proto protoreflect.FileDescriptor +var File_landstrategy_proto protoreflect.FileDescriptor -const file_mergestrategy_proto_rawDesc = "" + +const file_landstrategy_proto_rawDesc = "" + "\n" + - "\x13mergestrategy.proto\x12\x17uber.base.mergestrategy*N\n" + + "\x12landstrategy.proto\x12\x16uber.base.landstrategy*N\n" + "\bStrategy\x12\v\n" + "\aDEFAULT\x10\x00\x12\n" + "\n" + "\x06REBASE\x10\x01\x12\x11\n" + "\rSQUASH_REBASE\x10\x02\x12\t\n" + "\x05MERGE\x10\x03\x12\v\n" + - "\aPROMOTE\x10\x04B{\n" + - "'com.uber.submitqueue.base.mergestrategyB\x12MergeStrategyProtoP\x01Z:github.com/uber/submitqueue/api/base/mergestrategy/protopbb\x06proto3" + "\aPROMOTE\x10\x04Bx\n" + + "&com.uber.submitqueue.base.landstrategyB\x11LandStrategyProtoP\x01Z9github.com/uber/submitqueue/api/base/landstrategy/protopbb\x06proto3" var ( - file_mergestrategy_proto_rawDescOnce sync.Once - file_mergestrategy_proto_rawDescData []byte + file_landstrategy_proto_rawDescOnce sync.Once + file_landstrategy_proto_rawDescData []byte ) -func file_mergestrategy_proto_rawDescGZIP() []byte { - file_mergestrategy_proto_rawDescOnce.Do(func() { - file_mergestrategy_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_mergestrategy_proto_rawDesc), len(file_mergestrategy_proto_rawDesc))) +func file_landstrategy_proto_rawDescGZIP() []byte { + file_landstrategy_proto_rawDescOnce.Do(func() { + file_landstrategy_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_landstrategy_proto_rawDesc), len(file_landstrategy_proto_rawDesc))) }) - return file_mergestrategy_proto_rawDescData + return file_landstrategy_proto_rawDescData } -var file_mergestrategy_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_mergestrategy_proto_goTypes = []any{ - (Strategy)(0), // 0: uber.base.mergestrategy.Strategy +var file_landstrategy_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_landstrategy_proto_goTypes = []any{ + (Strategy)(0), // 0: uber.base.landstrategy.Strategy } -var file_mergestrategy_proto_depIdxs = []int32{ +var file_landstrategy_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for method output_type 0, // [0:0] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name @@ -155,26 +155,26 @@ var file_mergestrategy_proto_depIdxs = []int32{ 0, // [0:0] is the sub-list for field type_name } -func init() { file_mergestrategy_proto_init() } -func file_mergestrategy_proto_init() { - if File_mergestrategy_proto != nil { +func init() { file_landstrategy_proto_init() } +func file_landstrategy_proto_init() { + if File_landstrategy_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_mergestrategy_proto_rawDesc), len(file_mergestrategy_proto_rawDesc)), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_landstrategy_proto_rawDesc), len(file_landstrategy_proto_rawDesc)), NumEnums: 1, NumMessages: 0, NumExtensions: 0, NumServices: 0, }, - GoTypes: file_mergestrategy_proto_goTypes, - DependencyIndexes: file_mergestrategy_proto_depIdxs, - EnumInfos: file_mergestrategy_proto_enumTypes, + GoTypes: file_landstrategy_proto_goTypes, + DependencyIndexes: file_landstrategy_proto_depIdxs, + EnumInfos: file_landstrategy_proto_enumTypes, }.Build() - File_mergestrategy_proto = out.File - file_mergestrategy_proto_goTypes = nil - file_mergestrategy_proto_depIdxs = nil + File_landstrategy_proto = out.File + file_landstrategy_proto_goTypes = nil + file_landstrategy_proto_depIdxs = nil } diff --git a/api/base/messagequeue/proto/messagequeue.proto b/api/base/messagequeue/proto/messagequeue.proto index 3dbabb705..bd18807d1 100644 --- a/api/base/messagequeue/proto/messagequeue.proto +++ b/api/base/messagequeue/proto/messagequeue.proto @@ -28,7 +28,7 @@ option java_package = "com.uber.submitqueue.base.messagequeue"; // in any domain — annotates itself with stable logical topic key(s), making the // key-to-payload binding part of the language-neutral proto contract rather than // out-of-band Go wiring. A single payload may list several keys (one shape can -// serve a queue pair, e.g. a dry-run check and a committing merge). Domains +// serve a queue pair, e.g. a dry-run check and a committing operation). Domains // import this rather than redefining their own. extend google.protobuf.MessageOptions { // topic_keys are the stable logical topic keys that carry this message — not diff --git a/api/runway/messagequeue/BUILD.bazel b/api/runway/messagequeue/BUILD.bazel index d0c6ac55b..6a7687b06 100644 --- a/api/runway/messagequeue/BUILD.bazel +++ b/api/runway/messagequeue/BUILD.bazel @@ -3,7 +3,7 @@ load("@rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", srcs = [ - "merge.go", + "land.go", "topics.go", ], importpath = "github.com/uber/submitqueue/api/runway/messagequeue", @@ -19,11 +19,11 @@ go_library( go_test( name = "go_default_test", - srcs = ["merge_test.go"], + srcs = ["land_test.go"], embed = [":go_default_library"], deps = [ "//api/base/change/protopb:go_default_library", - "//api/base/mergestrategy/protopb:go_default_library", + "//api/base/landstrategy/protopb:go_default_library", "//api/runway/messagequeue/protopb:go_default_library", "@com_github_stretchr_testify//assert:go_default_library", "@com_github_stretchr_testify//require:go_default_library", diff --git a/api/runway/messagequeue/README.md b/api/runway/messagequeue/README.md index d9aaf161b..d9a39682f 100644 --- a/api/runway/messagequeue/README.md +++ b/api/runway/messagequeue/README.md @@ -1,14 +1,14 @@ # Runway message queue contract -The published, language-neutral contract for the merge queues Runway owns. A client — in any language — publishes a merge request and consumes the result without access to Runway's Go types or storage. See [the message queue contract RFC](../../../doc/rfc/messagequeue-contract.md) for the design. +The published, language-neutral contract for the land queues Runway owns. A client — in any language — publishes a land request and consumes the result without access to Runway's Go types or storage. See [the message queue contract RFC](../../../doc/rfc/messagequeue-contract.md) for the design. -Payloads are defined as proto3 messages in [`proto/merge.proto`](proto/merge.proto) and generated into [`protopb/`](protopb); the proto is the authority and a non-Go client compiles against it directly. On the wire, payloads are serialized as protobuf JSON (`protojson`), so the queue keeps storing self-describing JSON. The message types are generated, so the Go helpers in this package are just generic `protojson` glue — `Marshal(m)` and `Unmarshal[T](b, m)` — for Go callers; field names stay snake_case (`UseProtoNames`) and enums serialize as their UPPER_SNAKE value name. +Payloads are defined as proto3 messages in [`proto/land.proto`](proto/land.proto) and generated into [`protopb/`](protopb); the proto is the authority and a non-Go client compiles against it directly. On the wire, payloads are serialized as protobuf JSON (`protojson`), so the queue keeps storing self-describing JSON. The message types are generated, so the Go helpers in this package are just generic `protojson` glue — `Marshal(m)` and `Unmarshal[T](b, m)` — for Go callers; field names stay snake_case (`UseProtoNames`) and enums serialize as their UPPER_SNAKE value name. -The shared field types `Change` and `MergeStrategy` come from `api/base/change` and `api/base/mergestrategy`, imported by the contract. +The shared field types `Change` and `Strategy` come from `api/base/change` and `api/base/landstrategy`, imported by the contract. -## Merge strategy +## Land strategy -Each `MergeStep` carries a `Strategy` (from `api/base/mergestrategy`) naming how that step is integrated into the merge target. `REBASE`, `SQUASH_REBASE`, and `MERGE` *transform* the change onto the branch tip and produce new revisions. `PROMOTE` is different: it integrates the exact revision **as-is**, advancing the merge target to an already-existing commit with no content transform and no new revision. Each backend realizes `PROMOTE` natively — git fast-forward, Mercurial bookmark advance, Subversion/Perforce copy — so for `PROMOTE` a step's `StepOutput.id` is the same revision the request named rather than a freshly created one. It is the strategy a post-merge verifier (e.g. Stovepipe) uses to advance a verified branch like `verified/main` to an already-landed, already-verified commit. `DEFAULT` lets the server pick per queue configuration. +Each `LandStep` carries a `Strategy` (from `api/base/landstrategy`) naming how that step is integrated into the land target. `REBASE`, `SQUASH_REBASE`, and `MERGE` *transform* the change onto the branch tip and produce new revisions. `PROMOTE` is different: it integrates the exact revision **as-is**, advancing the land target to an already-existing commit with no content transform and no new revision. Each backend realizes `PROMOTE` natively — git fast-forward, Mercurial bookmark advance, Subversion/Perforce copy — so for `PROMOTE` a step's `StepOutput.id` is the same revision the request named rather than a freshly created one. It is the strategy a post-land verifier (e.g. Stovepipe) uses to advance a verified branch like `verified/main` to an already-landed, already-verified commit. `DEFAULT` lets the server pick per queue configuration. ## Topic keys @@ -16,14 +16,14 @@ The binding between a topic key and its payload lives in each message's `topic_k | Message | Direction | Topic keys | |---|---|---| -| `MergeRequest` | client → Runway | `merge-conflict-check`, `merge` | -| `MergeResult` | Runway → client | `merge-conflict-check-signal`, `merge-signal` | +| `LandRequest` | client → Runway | `land-conflict-check`, `runway-land` | +| `LandResult` | Runway → client | `land-conflict-check-signal`, `land-signal` | -One message serves a queue pair because a merge-conflict check is a dry run of a merge: Runway applies the same ordered steps onto the same merge target, and the topic key the request arrives on decides whether it commits the result and reports the produced revisions. A request on `merge-conflict-check` is a dry run; a request on `merge` commits. +One message serves a queue pair because a land-conflict check is a dry run of a land: Runway applies the same ordered steps onto the same land target, and the topic key the request arrives on decides whether it commits the result and reports the produced revisions. A request on `land-conflict-check` is a dry run; a request on `runway-land` commits. ## Result shape -`MergeResult.outcome` is an `Outcome` enum (`OUTCOME_UNSPECIFIED`/`SUCCEEDED`/`FAILED`): `SUCCEEDED` means mergeable (check) or merged (commit), `FAILED` a conflict or a failed apply; `reason` carries the explanation when `FAILED`. Per-step detail is in `steps` (request order): each `StepResult.outputs` is the list of `StepOutput`s the step produced on the merge target, **in application order** (the order they were created). A committing merge populates `outputs`; a dry-run check, an already-present change, or a failed step leaves them empty. `StepOutput.id` is the VCS-neutral revision identifier (git SHA, Mercurial hash, Subversion revision, Perforce changelist, …), with room to grow (author, timestamp, …). +`LandResult.outcome` is an `Outcome` enum (`OUTCOME_UNSPECIFIED`/`SUCCEEDED`/`FAILED`): `SUCCEEDED` means landable (check) or landed (commit), `FAILED` a conflict or a failed apply; `reason` carries the explanation when `FAILED`. Per-step detail is in `steps` (request order): each `StepResult.outputs` is the list of `StepOutput`s the step produced on the land target, **in application order** (the order they were created). A committing land populates `outputs`; a dry-run check, an already-present change, or a failed step leaves them empty. `StepOutput.id` is the VCS-neutral revision identifier (git SHA, Mercurial hash, Subversion revision, Perforce changelist, …), with room to grow (author, timestamp, …). ## Evolution diff --git a/api/runway/messagequeue/merge.go b/api/runway/messagequeue/land.go similarity index 78% rename from api/runway/messagequeue/merge.go rename to api/runway/messagequeue/land.go index 69184a991..f1919c57c 100644 --- a/api/runway/messagequeue/merge.go +++ b/api/runway/messagequeue/land.go @@ -13,7 +13,7 @@ // limitations under the License. // Package messagequeue holds Runway's external message-queue contract: the wire -// payloads for the merge queues Runway owns, defined by the proto files in +// payloads for the land queues Runway owns, defined by the proto files in // proto/ and generated into protopb/. The proto is the language-neutral // authority; the generated Go types in protopb are the binding for Go callers. // @@ -25,12 +25,12 @@ // declared on the message itself via the topic_keys proto option (see // api/base/messagequeue). // -// One contract serves two queue pairs because a merge-conflict check is a dry -// run of a merge: Runway applies the same ordered steps onto the same target +// One contract serves two queue pairs because a land-conflict check is a dry +// run of a land: Runway applies the same ordered steps onto the same target // branch, and the only difference is whether it commits the result and reports // the revisions it produced. The topic key a request arrives on encodes that choice -// — the merge-conflict-check pair for a dry run, the merge pair for a -// committing merge — so MergeRequest and MergeResult are identical on both. +// — the land-conflict-check pair for a dry run, the land pair for a +// committing land — so LandRequest and LandResult are identical on both. package messagequeue import ( @@ -44,19 +44,19 @@ import ( // Wire payload types. These alias the generated protobuf bindings so callers // reference the contract through this curated package rather than protopb. type ( - // MergeRequest is the payload a client publishes to one of Runway's merge - // queues: the merge-conflict-check topic for a dry-run check, the merge - // topic for a committing merge. - MergeRequest = protopb.MergeRequest - // MergeStep is one step of an ordered merge: a single set of change(s) + // LandRequest is the payload a client publishes to one of Runway's land + // queues: the land-conflict-check topic for a dry-run check, the land + // topic for a committing land. + LandRequest = protopb.LandRequest + // LandStep is one step of an ordered land: a single set of change(s) // applied with a strategy. - MergeStep = protopb.MergeStep - // MergeResult is the payload Runway publishes to the corresponding signal + LandStep = protopb.LandStep + // LandResult is the payload Runway publishes to the corresponding signal // queue once a request completes. - MergeResult = protopb.MergeResult - // StepResult reports what happened to a single MergeStep. + LandResult = protopb.LandResult + // StepResult reports what happened to a single LandStep. StepResult = protopb.StepResult - // StepOutput is a single revision a merge step produced on the merge target. + // StepOutput is a single revision a land step produced on the land target. StepOutput = protopb.StepOutput ) diff --git a/api/runway/messagequeue/merge_test.go b/api/runway/messagequeue/land_test.go similarity index 78% rename from api/runway/messagequeue/merge_test.go rename to api/runway/messagequeue/land_test.go index 754489db1..7be336d46 100644 --- a/api/runway/messagequeue/merge_test.go +++ b/api/runway/messagequeue/land_test.go @@ -22,15 +22,15 @@ import ( "google.golang.org/protobuf/proto" changepb "github.com/uber/submitqueue/api/base/change/protopb" - strategypb "github.com/uber/submitqueue/api/base/mergestrategy/protopb" + strategypb "github.com/uber/submitqueue/api/base/landstrategy/protopb" "github.com/uber/submitqueue/api/runway/messagequeue/protopb" ) -func TestMergeRequestRoundTrip(t *testing.T) { - req := &MergeRequest{ +func TestLandRequestRoundTrip(t *testing.T) { + req := &LandRequest{ Id: "queue-a/42", QueueName: "queue-a", - Steps: []*MergeStep{ + Steps: []*LandStep{ { StepId: "queue-a/1", Change: &changepb.Change{Uris: []string{"github://github.example.com/uber/repo/pull/1/0123456789abcdef0123456789abcdef01234567"}}, @@ -47,17 +47,17 @@ func TestMergeRequestRoundTrip(t *testing.T) { data, err := Marshal(req) require.NoError(t, err) - got := &MergeRequest{} + got := &LandRequest{} require.NoError(t, Unmarshal(data, got)) - assert.True(t, proto.Equal(req, got), "round-tripped MergeRequest should equal the original") + assert.True(t, proto.Equal(req, got), "round-tripped LandRequest should equal the original") } -func TestMergeResultRoundTrip(t *testing.T) { - // A committing merge reports the revisions each step produced on the target; +func TestLandResultRoundTrip(t *testing.T) { + // A committing land reports the revisions each step produced on the target; // a dry-run check leaves outputs empty and reports a per-step reason on - // failure. Both shapes share the one MergeResult contract. - cases := map[string]*MergeResult{ - "merged with produced revisions": { + // failure. Both shapes share the one LandResult contract. + cases := map[string]*LandResult{ + "landed with produced revisions": { Id: "queue-a/42", Outcome: protopb.Outcome_SUCCEEDED, Steps: []*StepResult{ @@ -81,9 +81,9 @@ func TestMergeResultRoundTrip(t *testing.T) { data, err := Marshal(res) require.NoError(t, err) - got := &MergeResult{} + got := &LandResult{} require.NoError(t, Unmarshal(data, got)) - assert.True(t, proto.Equal(res, got), "round-tripped MergeResult should equal the original") + assert.True(t, proto.Equal(res, got), "round-tripped LandResult should equal the original") }) } } @@ -92,10 +92,10 @@ func TestMergeResultRoundTrip(t *testing.T) { // on: snake_case field names (UseProtoNames) and proto-conventional UPPER_SNAKE // enum values on the wire. func TestWireFormat(t *testing.T) { - data, err := Marshal(&MergeRequest{ + data, err := Marshal(&LandRequest{ Id: "queue-a/42", QueueName: "queue-a", - Steps: []*MergeStep{{StepId: "queue-a/1", Strategy: strategypb.Strategy_SQUASH_REBASE}}, + Steps: []*LandStep{{StepId: "queue-a/1", Strategy: strategypb.Strategy_SQUASH_REBASE}}, }) require.NoError(t, err) @@ -108,7 +108,7 @@ func TestWireFormat(t *testing.T) { // topic_keys option names an unknown key. func TestTopicKeysBindEveryTopicKey(t *testing.T) { bound := map[string]int{} - for _, m := range []proto.Message{&MergeRequest{}, &MergeResult{}} { + for _, m := range []proto.Message{&LandRequest{}, &LandResult{}} { keys := TopicKeys(m) require.NotEmpty(t, keys, "message must declare a non-empty topic_keys option") for _, key := range keys { @@ -117,10 +117,10 @@ func TestTopicKeysBindEveryTopicKey(t *testing.T) { } keys := []TopicKey{ - TopicKeyMergeConflictCheck, - TopicKeyMergeConflictCheckSignal, - TopicKeyMerge, - TopicKeyMergeSignal, + TopicKeyLandConflictCheck, + TopicKeyLandConflictCheckSignal, + TopicKeyLand, + TopicKeyLandSignal, } valid := map[string]bool{} diff --git a/api/runway/messagequeue/proto/BUILD.bazel b/api/runway/messagequeue/proto/BUILD.bazel index 6a0c6c855..4e397e2f7 100644 --- a/api/runway/messagequeue/proto/BUILD.bazel +++ b/api/runway/messagequeue/proto/BUILD.bazel @@ -1,4 +1,4 @@ exports_files( - ["merge.proto"], + ["land.proto"], visibility = ["//tool/proto:__pkg__"], ) diff --git a/api/runway/messagequeue/proto/merge.proto b/api/runway/messagequeue/proto/land.proto similarity index 70% rename from api/runway/messagequeue/proto/merge.proto rename to api/runway/messagequeue/proto/land.proto index 5ab44e55e..77bf75e46 100644 --- a/api/runway/messagequeue/proto/merge.proto +++ b/api/runway/messagequeue/proto/land.proto @@ -17,20 +17,20 @@ syntax = "proto3"; package uber.runway.messagequeue; import "api/base/change/proto/change.proto"; -import "api/base/mergestrategy/proto/mergestrategy.proto"; +import "api/base/landstrategy/proto/landstrategy.proto"; import "api/base/messagequeue/proto/messagequeue.proto"; option go_package = "github.com/uber/submitqueue/api/runway/messagequeue/protopb"; option java_multiple_files = true; -option java_outer_classname = "MergeProto"; +option java_outer_classname = "LandProto"; option java_package = "com.uber.submitqueue.runway.messagequeue"; -// MergeStep is one step of an ordered merge: a single change applied with a -// strategy. Runway applies the steps of a request in order on top of the merge +// LandStep is one step of an ordered land: a single change applied with a +// strategy. Runway applies the steps of a request in order on top of the land // target; the ordering encodes the base-layering (earlier steps are the // in-flight base, the last step is the candidate). A change may contain // multiple URIs (e.g. stacked PRs that land together). -message MergeStep { +message LandStep { // step_id is an opaque, caller-assigned identifier for this step. Runway // treats it as an attribution token only -- it echoes it back per-step in // StepResult so a multi-step result is attributable -- and never interprets @@ -40,36 +40,36 @@ message MergeStep { // URIs, which are a stack: each URI is its own unit of change, applied in // the order given, each on top of the last. uber.base.change.Change change = 2; - // strategy is how this step's change is integrated into the merge target. + // strategy is how this step's change is integrated into the land target. // It applies to every URI the change carries, the same way to each, so a // step is never a mix of strategies. PROMOTE is the exception that admits // only a single URI. - uber.base.mergestrategy.Strategy strategy = 3; + uber.base.landstrategy.Strategy strategy = 3; } -// MergeRequest is the payload a client publishes to one of Runway's merge -// queues: the merge-conflict-check topic for a dry-run check, the merge topic -// for a committing merge. The id is owned by the client so it can record the +// LandRequest is the payload a client publishes to one of Runway's land +// queues: the land-conflict-check topic for a dry-run check, the land topic +// for a committing land. The id is owned by the client so it can record the // in-flight work before publishing and correlate the asynchronous result; // Runway echoes it back unchanged. -message MergeRequest { - option (uber.base.messagequeue.topic_keys) = "merge-conflict-check"; - option (uber.base.messagequeue.topic_keys) = "runway-merge"; +message LandRequest { + option (uber.base.messagequeue.topic_keys) = "land-conflict-check"; + option (uber.base.messagequeue.topic_keys) = "runway-land"; // id is the client-owned correlation id for this request (one per request). // Runway echoes it back on the result unchanged. string id = 1; // queue_name is the caller-provided queue name the request belongs to. - // Runway resolves the merge target and provider config per-queue from this + // Runway resolves the land target and provider config per-queue from this // name; no target ref is passed. string queue_name = 2; // steps is the ordered application sequence: in-flight steps first, the - // candidate last. A single-element list expresses "candidate vs merge + // candidate last. A single-element list expresses "candidate vs land // target". - repeated MergeStep steps = 3; + repeated LandStep steps = 3; } -// StepOutput is a single output a merge step produced on the merge target. +// StepOutput is a single output a land step produced on the land target. message StepOutput { // id is the VCS-neutral revision identifier -- a git commit SHA, a Mercurial // changeset hash, a Subversion revision number, a Perforce changelist, and so @@ -78,12 +78,12 @@ message StepOutput { // (room to grow: author, committed_at, parent, ...) } -// StepResult reports what happened to a single MergeStep, so a multi-step result +// StepResult reports what happened to a single LandStep, so a multi-step result // is attributable to the step that produced (or failed to produce) it. message StepResult { // step_id echoes the step_id of the step this result is for. string step_id = 1; - // outputs are the revisions this step produced on the merge target, in + // outputs are the revisions this step produced on the land target, in // application order (the order the step created them on the target). Empty // for a dry-run check, for a change already present on the target, or for a // step that failed to apply. @@ -93,37 +93,37 @@ message StepResult { string reason = 3; } -// Outcome is the overall result of applying a MergeRequest's ordered step +// Outcome is the overall result of applying a LandRequest's ordered step // sequence. enum Outcome { // OUTCOME_UNSPECIFIED is the zero value; a well-formed result never uses it. OUTCOME_UNSPECIFIED = 0; - // SUCCEEDED means the whole sequence applied cleanly: mergeable for a - // dry-run check, merged for a committing merge. + // SUCCEEDED means the whole sequence applied cleanly: landable for a + // dry-run check, landed for a committing land. SUCCEEDED = 1; // FAILED means the sequence could not be applied: a conflict for a dry-run - // check, a failed apply for a committing merge. + // check, a failed apply for a committing land. FAILED = 2; } -// MergeResult is the payload Runway publishes to the corresponding signal queue -// (merge-conflict-check-signal for a check, merge-signal for a merge) once a +// LandResult is the payload Runway publishes to the corresponding signal queue +// (land-conflict-check-signal for a check, land-signal for a land) once a // request completes. -message MergeResult { - option (uber.base.messagequeue.topic_keys) = "merge-conflict-check-signal"; - option (uber.base.messagequeue.topic_keys) = "merge-signal"; +message LandResult { + option (uber.base.messagequeue.topic_keys) = "land-conflict-check-signal"; + option (uber.base.messagequeue.topic_keys) = "land-signal"; // id echoes the client-owned correlation id from the request. string id = 1; // outcome is SUCCEEDED if the whole ordered step sequence applied cleanly - // (mergeable for a dry-run check, merged for a committing merge), FAILED + // (landable for a dry-run check, landed for a committing land), FAILED // otherwise. Outcome outcome = 2; // reason is a human-readable explanation when outcome is FAILED. Empty on // success. string reason = 3; // steps optionally reports per-step outcomes, in request order. A committing - // merge populates each step's outputs with the revisions it produced; a + // land populates each step's outputs with the revisions it produced; a // dry-run check leaves them empty. repeated StepResult steps = 4; // queue_name echoes the caller-provided queue name from the request, so the diff --git a/api/runway/messagequeue/protopb/BUILD.bazel b/api/runway/messagequeue/protopb/BUILD.bazel index 69c1e067b..f66abe5bf 100644 --- a/api/runway/messagequeue/protopb/BUILD.bazel +++ b/api/runway/messagequeue/protopb/BUILD.bazel @@ -2,12 +2,12 @@ load("@rules_go//go:def.bzl", "go_library") go_library( name = "go_default_library", - srcs = ["merge.pb.go"], + srcs = ["land.pb.go"], importpath = "github.com/uber/submitqueue/api/runway/messagequeue/protopb", visibility = ["//visibility:public"], deps = [ "//api/base/change/protopb:go_default_library", - "//api/base/mergestrategy/protopb:go_default_library", + "//api/base/landstrategy/protopb:go_default_library", "//api/base/messagequeue/protopb:go_default_library", "@org_golang_google_protobuf//reflect/protoreflect:go_default_library", "@org_golang_google_protobuf//runtime/protoimpl:go_default_library", diff --git a/api/runway/messagequeue/protopb/merge.pb.go b/api/runway/messagequeue/protopb/land.pb.go similarity index 63% rename from api/runway/messagequeue/protopb/merge.pb.go rename to api/runway/messagequeue/protopb/land.pb.go index e1bbd6aef..31eae5b89 100644 --- a/api/runway/messagequeue/protopb/merge.pb.go +++ b/api/runway/messagequeue/protopb/land.pb.go @@ -16,7 +16,7 @@ // versions: // protoc-gen-go v1.36.10 // protoc v5.29.3 -// source: merge.proto +// source: land.proto package protopb @@ -26,7 +26,7 @@ import ( unsafe "unsafe" protopb "github.com/uber/submitqueue/api/base/change/protopb" - protopb1 "github.com/uber/submitqueue/api/base/mergestrategy/protopb" + protopb1 "github.com/uber/submitqueue/api/base/landstrategy/protopb" _ "github.com/uber/submitqueue/api/base/messagequeue/protopb" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" @@ -39,18 +39,18 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -// Outcome is the overall result of applying a MergeRequest's ordered step +// Outcome is the overall result of applying a LandRequest's ordered step // sequence. type Outcome int32 const ( // OUTCOME_UNSPECIFIED is the zero value; a well-formed result never uses it. Outcome_OUTCOME_UNSPECIFIED Outcome = 0 - // SUCCEEDED means the whole sequence applied cleanly: mergeable for a - // dry-run check, merged for a committing merge. + // SUCCEEDED means the whole sequence applied cleanly: landable for a + // dry-run check, landed for a committing land. Outcome_SUCCEEDED Outcome = 1 // FAILED means the sequence could not be applied: a conflict for a dry-run - // check, a failed apply for a committing merge. + // check, a failed apply for a committing land. Outcome_FAILED Outcome = 2 ) @@ -79,11 +79,11 @@ func (x Outcome) String() string { } func (Outcome) Descriptor() protoreflect.EnumDescriptor { - return file_merge_proto_enumTypes[0].Descriptor() + return file_land_proto_enumTypes[0].Descriptor() } func (Outcome) Type() protoreflect.EnumType { - return &file_merge_proto_enumTypes[0] + return &file_land_proto_enumTypes[0] } func (x Outcome) Number() protoreflect.EnumNumber { @@ -92,15 +92,15 @@ func (x Outcome) Number() protoreflect.EnumNumber { // Deprecated: Use Outcome.Descriptor instead. func (Outcome) EnumDescriptor() ([]byte, []int) { - return file_merge_proto_rawDescGZIP(), []int{0} + return file_land_proto_rawDescGZIP(), []int{0} } -// MergeStep is one step of an ordered merge: a single change applied with a -// strategy. Runway applies the steps of a request in order on top of the merge +// LandStep is one step of an ordered land: a single change applied with a +// strategy. Runway applies the steps of a request in order on top of the land // target; the ordering encodes the base-layering (earlier steps are the // in-flight base, the last step is the candidate). A change may contain // multiple URIs (e.g. stacked PRs that land together). -type MergeStep struct { +type LandStep struct { state protoimpl.MessageState `protogen:"open.v1"` // step_id is an opaque, caller-assigned identifier for this step. Runway // treats it as an attribution token only -- it echoes it back per-step in @@ -111,30 +111,30 @@ type MergeStep struct { // URIs, which are a stack: each URI is its own unit of change, applied in // the order given, each on top of the last. Change *protopb.Change `protobuf:"bytes,2,opt,name=change,proto3" json:"change,omitempty"` - // strategy is how this step's change is integrated into the merge target. + // strategy is how this step's change is integrated into the land target. // It applies to every URI the change carries, the same way to each, so a // step is never a mix of strategies. PROMOTE is the exception that admits // only a single URI. - Strategy protopb1.Strategy `protobuf:"varint,3,opt,name=strategy,proto3,enum=uber.base.mergestrategy.Strategy" json:"strategy,omitempty"` + Strategy protopb1.Strategy `protobuf:"varint,3,opt,name=strategy,proto3,enum=uber.base.landstrategy.Strategy" json:"strategy,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *MergeStep) Reset() { - *x = MergeStep{} - mi := &file_merge_proto_msgTypes[0] +func (x *LandStep) Reset() { + *x = LandStep{} + mi := &file_land_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *MergeStep) String() string { +func (x *LandStep) String() string { return protoimpl.X.MessageStringOf(x) } -func (*MergeStep) ProtoMessage() {} +func (*LandStep) ProtoMessage() {} -func (x *MergeStep) ProtoReflect() protoreflect.Message { - mi := &file_merge_proto_msgTypes[0] +func (x *LandStep) ProtoReflect() protoreflect.Message { + mi := &file_land_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -145,69 +145,69 @@ func (x *MergeStep) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use MergeStep.ProtoReflect.Descriptor instead. -func (*MergeStep) Descriptor() ([]byte, []int) { - return file_merge_proto_rawDescGZIP(), []int{0} +// Deprecated: Use LandStep.ProtoReflect.Descriptor instead. +func (*LandStep) Descriptor() ([]byte, []int) { + return file_land_proto_rawDescGZIP(), []int{0} } -func (x *MergeStep) GetStepId() string { +func (x *LandStep) GetStepId() string { if x != nil { return x.StepId } return "" } -func (x *MergeStep) GetChange() *protopb.Change { +func (x *LandStep) GetChange() *protopb.Change { if x != nil { return x.Change } return nil } -func (x *MergeStep) GetStrategy() protopb1.Strategy { +func (x *LandStep) GetStrategy() protopb1.Strategy { if x != nil { return x.Strategy } return protopb1.Strategy(0) } -// MergeRequest is the payload a client publishes to one of Runway's merge -// queues: the merge-conflict-check topic for a dry-run check, the merge topic -// for a committing merge. The id is owned by the client so it can record the +// LandRequest is the payload a client publishes to one of Runway's land +// queues: the land-conflict-check topic for a dry-run check, the land topic +// for a committing land. The id is owned by the client so it can record the // in-flight work before publishing and correlate the asynchronous result; // Runway echoes it back unchanged. -type MergeRequest struct { +type LandRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // id is the client-owned correlation id for this request (one per request). // Runway echoes it back on the result unchanged. Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // queue_name is the caller-provided queue name the request belongs to. - // Runway resolves the merge target and provider config per-queue from this + // Runway resolves the land target and provider config per-queue from this // name; no target ref is passed. QueueName string `protobuf:"bytes,2,opt,name=queue_name,json=queueName,proto3" json:"queue_name,omitempty"` // steps is the ordered application sequence: in-flight steps first, the - // candidate last. A single-element list expresses "candidate vs merge + // candidate last. A single-element list expresses "candidate vs land // target". - Steps []*MergeStep `protobuf:"bytes,3,rep,name=steps,proto3" json:"steps,omitempty"` + Steps []*LandStep `protobuf:"bytes,3,rep,name=steps,proto3" json:"steps,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *MergeRequest) Reset() { - *x = MergeRequest{} - mi := &file_merge_proto_msgTypes[1] +func (x *LandRequest) Reset() { + *x = LandRequest{} + mi := &file_land_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *MergeRequest) String() string { +func (x *LandRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*MergeRequest) ProtoMessage() {} +func (*LandRequest) ProtoMessage() {} -func (x *MergeRequest) ProtoReflect() protoreflect.Message { - mi := &file_merge_proto_msgTypes[1] +func (x *LandRequest) ProtoReflect() protoreflect.Message { + mi := &file_land_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -218,33 +218,33 @@ func (x *MergeRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use MergeRequest.ProtoReflect.Descriptor instead. -func (*MergeRequest) Descriptor() ([]byte, []int) { - return file_merge_proto_rawDescGZIP(), []int{1} +// Deprecated: Use LandRequest.ProtoReflect.Descriptor instead. +func (*LandRequest) Descriptor() ([]byte, []int) { + return file_land_proto_rawDescGZIP(), []int{1} } -func (x *MergeRequest) GetId() string { +func (x *LandRequest) GetId() string { if x != nil { return x.Id } return "" } -func (x *MergeRequest) GetQueueName() string { +func (x *LandRequest) GetQueueName() string { if x != nil { return x.QueueName } return "" } -func (x *MergeRequest) GetSteps() []*MergeStep { +func (x *LandRequest) GetSteps() []*LandStep { if x != nil { return x.Steps } return nil } -// StepOutput is a single output a merge step produced on the merge target. +// StepOutput is a single output a land step produced on the land target. type StepOutput struct { state protoimpl.MessageState `protogen:"open.v1"` // id is the VCS-neutral revision identifier -- a git commit SHA, a Mercurial @@ -257,7 +257,7 @@ type StepOutput struct { func (x *StepOutput) Reset() { *x = StepOutput{} - mi := &file_merge_proto_msgTypes[2] + mi := &file_land_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -269,7 +269,7 @@ func (x *StepOutput) String() string { func (*StepOutput) ProtoMessage() {} func (x *StepOutput) ProtoReflect() protoreflect.Message { - mi := &file_merge_proto_msgTypes[2] + mi := &file_land_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -282,7 +282,7 @@ func (x *StepOutput) ProtoReflect() protoreflect.Message { // Deprecated: Use StepOutput.ProtoReflect.Descriptor instead. func (*StepOutput) Descriptor() ([]byte, []int) { - return file_merge_proto_rawDescGZIP(), []int{2} + return file_land_proto_rawDescGZIP(), []int{2} } func (x *StepOutput) GetId() string { @@ -292,13 +292,13 @@ func (x *StepOutput) GetId() string { return "" } -// StepResult reports what happened to a single MergeStep, so a multi-step result +// StepResult reports what happened to a single LandStep, so a multi-step result // is attributable to the step that produced (or failed to produce) it. type StepResult struct { state protoimpl.MessageState `protogen:"open.v1"` // step_id echoes the step_id of the step this result is for. StepId string `protobuf:"bytes,1,opt,name=step_id,json=stepId,proto3" json:"step_id,omitempty"` - // outputs are the revisions this step produced on the merge target, in + // outputs are the revisions this step produced on the land target, in // application order (the order the step created them on the target). Empty // for a dry-run check, for a change already present on the target, or for a // step that failed to apply. @@ -312,7 +312,7 @@ type StepResult struct { func (x *StepResult) Reset() { *x = StepResult{} - mi := &file_merge_proto_msgTypes[3] + mi := &file_land_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -324,7 +324,7 @@ func (x *StepResult) String() string { func (*StepResult) ProtoMessage() {} func (x *StepResult) ProtoReflect() protoreflect.Message { - mi := &file_merge_proto_msgTypes[3] + mi := &file_land_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -337,7 +337,7 @@ func (x *StepResult) ProtoReflect() protoreflect.Message { // Deprecated: Use StepResult.ProtoReflect.Descriptor instead. func (*StepResult) Descriptor() ([]byte, []int) { - return file_merge_proto_rawDescGZIP(), []int{3} + return file_land_proto_rawDescGZIP(), []int{3} } func (x *StepResult) GetStepId() string { @@ -361,22 +361,22 @@ func (x *StepResult) GetReason() string { return "" } -// MergeResult is the payload Runway publishes to the corresponding signal queue -// (merge-conflict-check-signal for a check, merge-signal for a merge) once a +// LandResult is the payload Runway publishes to the corresponding signal queue +// (land-conflict-check-signal for a check, land-signal for a land) once a // request completes. -type MergeResult struct { +type LandResult struct { state protoimpl.MessageState `protogen:"open.v1"` // id echoes the client-owned correlation id from the request. Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // outcome is SUCCEEDED if the whole ordered step sequence applied cleanly - // (mergeable for a dry-run check, merged for a committing merge), FAILED + // (landable for a dry-run check, landed for a committing land), FAILED // otherwise. Outcome Outcome `protobuf:"varint,2,opt,name=outcome,proto3,enum=uber.runway.messagequeue.Outcome" json:"outcome,omitempty"` // reason is a human-readable explanation when outcome is FAILED. Empty on // success. Reason string `protobuf:"bytes,3,opt,name=reason,proto3" json:"reason,omitempty"` // steps optionally reports per-step outcomes, in request order. A committing - // merge populates each step's outputs with the revisions it produced; a + // land populates each step's outputs with the revisions it produced; a // dry-run check leaves them empty. Steps []*StepResult `protobuf:"bytes,4,rep,name=steps,proto3" json:"steps,omitempty"` // queue_name echoes the caller-provided queue name from the request, so the @@ -386,21 +386,21 @@ type MergeResult struct { sizeCache protoimpl.SizeCache } -func (x *MergeResult) Reset() { - *x = MergeResult{} - mi := &file_merge_proto_msgTypes[4] +func (x *LandResult) Reset() { + *x = LandResult{} + mi := &file_land_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *MergeResult) String() string { +func (x *LandResult) String() string { return protoimpl.X.MessageStringOf(x) } -func (*MergeResult) ProtoMessage() {} +func (*LandResult) ProtoMessage() {} -func (x *MergeResult) ProtoReflect() protoreflect.Message { - mi := &file_merge_proto_msgTypes[4] +func (x *LandResult) ProtoReflect() protoreflect.Message { + mi := &file_land_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -411,60 +411,61 @@ func (x *MergeResult) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use MergeResult.ProtoReflect.Descriptor instead. -func (*MergeResult) Descriptor() ([]byte, []int) { - return file_merge_proto_rawDescGZIP(), []int{4} +// Deprecated: Use LandResult.ProtoReflect.Descriptor instead. +func (*LandResult) Descriptor() ([]byte, []int) { + return file_land_proto_rawDescGZIP(), []int{4} } -func (x *MergeResult) GetId() string { +func (x *LandResult) GetId() string { if x != nil { return x.Id } return "" } -func (x *MergeResult) GetOutcome() Outcome { +func (x *LandResult) GetOutcome() Outcome { if x != nil { return x.Outcome } return Outcome_OUTCOME_UNSPECIFIED } -func (x *MergeResult) GetReason() string { +func (x *LandResult) GetReason() string { if x != nil { return x.Reason } return "" } -func (x *MergeResult) GetSteps() []*StepResult { +func (x *LandResult) GetSteps() []*StepResult { if x != nil { return x.Steps } return nil } -func (x *MergeResult) GetQueueName() string { +func (x *LandResult) GetQueueName() string { if x != nil { return x.QueueName } return "" } -var File_merge_proto protoreflect.FileDescriptor +var File_land_proto protoreflect.FileDescriptor -const file_merge_proto_rawDesc = "" + +const file_land_proto_rawDesc = "" + "\n" + - "\vmerge.proto\x12\x18uber.runway.messagequeue\x1a\"api/base/change/proto/change.proto\x1a0api/base/mergestrategy/proto/mergestrategy.proto\x1a.api/base/messagequeue/proto/messagequeue.proto\"\x95\x01\n" + - "\tMergeStep\x12\x17\n" + + "\n" + + "land.proto\x12\x18uber.runway.messagequeue\x1a\"api/base/change/proto/change.proto\x1a.api/base/landstrategy/proto/landstrategy.proto\x1a.api/base/messagequeue/proto/messagequeue.proto\"\x93\x01\n" + + "\bLandStep\x12\x17\n" + "\astep_id\x18\x01 \x01(\tR\x06stepId\x120\n" + - "\x06change\x18\x02 \x01(\v2\x18.uber.base.change.ChangeR\x06change\x12=\n" + - "\bstrategy\x18\x03 \x01(\x0e2!.uber.base.mergestrategy.StrategyR\bstrategy\"\xa2\x01\n" + - "\fMergeRequest\x12\x0e\n" + + "\x06change\x18\x02 \x01(\v2\x18.uber.base.change.ChangeR\x06change\x12<\n" + + "\bstrategy\x18\x03 \x01(\x0e2 .uber.base.landstrategy.StrategyR\bstrategy\"\x9e\x01\n" + + "\vLandRequest\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x1d\n" + "\n" + - "queue_name\x18\x02 \x01(\tR\tqueueName\x129\n" + - "\x05steps\x18\x03 \x03(\v2#.uber.runway.messagequeue.MergeStepR\x05steps:(\x8a\xb5\x18\x14merge-conflict-check\x8a\xb5\x18\frunway-merge\"\x1c\n" + + "queue_name\x18\x02 \x01(\tR\tqueueName\x128\n" + + "\x05steps\x18\x03 \x03(\v2\".uber.runway.messagequeue.LandStepR\x05steps:&\x8a\xb5\x18\x13land-conflict-check\x8a\xb5\x18\vrunway-land\"\x1c\n" + "\n" + "StepOutput\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\"}\n" + @@ -472,53 +473,53 @@ const file_merge_proto_rawDesc = "" + "StepResult\x12\x17\n" + "\astep_id\x18\x01 \x01(\tR\x06stepId\x12>\n" + "\aoutputs\x18\x02 \x03(\v2$.uber.runway.messagequeue.StepOutputR\aoutputs\x12\x16\n" + - "\x06reason\x18\x03 \x01(\tR\x06reason\"\xfe\x01\n" + - "\vMergeResult\x12\x0e\n" + + "\x06reason\x18\x03 \x01(\tR\x06reason\"\xfb\x01\n" + + "\n" + + "LandResult\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12;\n" + "\aoutcome\x18\x02 \x01(\x0e2!.uber.runway.messagequeue.OutcomeR\aoutcome\x12\x16\n" + "\x06reason\x18\x03 \x01(\tR\x06reason\x12:\n" + "\x05steps\x18\x04 \x03(\v2$.uber.runway.messagequeue.StepResultR\x05steps\x12\x1d\n" + "\n" + - "queue_name\x18\x05 \x01(\tR\tqueueName:/\x8a\xb5\x18\x1bmerge-conflict-check-signal\x8a\xb5\x18\fmerge-signal*=\n" + + "queue_name\x18\x05 \x01(\tR\tqueueName:-\x8a\xb5\x18\x1aland-conflict-check-signal\x8a\xb5\x18\vland-signal*=\n" + "\aOutcome\x12\x17\n" + "\x13OUTCOME_UNSPECIFIED\x10\x00\x12\r\n" + "\tSUCCEEDED\x10\x01\x12\n" + "\n" + - "\x06FAILED\x10\x02Bu\n" + - "(com.uber.submitqueue.runway.messagequeueB\n" + - "MergeProtoP\x01Z;github.com/uber/submitqueue/api/runway/messagequeue/protopbb\x06proto3" + "\x06FAILED\x10\x02Bt\n" + + "(com.uber.submitqueue.runway.messagequeueB\tLandProtoP\x01Z;github.com/uber/submitqueue/api/runway/messagequeue/protopbb\x06proto3" var ( - file_merge_proto_rawDescOnce sync.Once - file_merge_proto_rawDescData []byte + file_land_proto_rawDescOnce sync.Once + file_land_proto_rawDescData []byte ) -func file_merge_proto_rawDescGZIP() []byte { - file_merge_proto_rawDescOnce.Do(func() { - file_merge_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_merge_proto_rawDesc), len(file_merge_proto_rawDesc))) +func file_land_proto_rawDescGZIP() []byte { + file_land_proto_rawDescOnce.Do(func() { + file_land_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_land_proto_rawDesc), len(file_land_proto_rawDesc))) }) - return file_merge_proto_rawDescData + return file_land_proto_rawDescData } -var file_merge_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_merge_proto_msgTypes = make([]protoimpl.MessageInfo, 5) -var file_merge_proto_goTypes = []any{ +var file_land_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_land_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_land_proto_goTypes = []any{ (Outcome)(0), // 0: uber.runway.messagequeue.Outcome - (*MergeStep)(nil), // 1: uber.runway.messagequeue.MergeStep - (*MergeRequest)(nil), // 2: uber.runway.messagequeue.MergeRequest + (*LandStep)(nil), // 1: uber.runway.messagequeue.LandStep + (*LandRequest)(nil), // 2: uber.runway.messagequeue.LandRequest (*StepOutput)(nil), // 3: uber.runway.messagequeue.StepOutput (*StepResult)(nil), // 4: uber.runway.messagequeue.StepResult - (*MergeResult)(nil), // 5: uber.runway.messagequeue.MergeResult + (*LandResult)(nil), // 5: uber.runway.messagequeue.LandResult (*protopb.Change)(nil), // 6: uber.base.change.Change - (protopb1.Strategy)(0), // 7: uber.base.mergestrategy.Strategy + (protopb1.Strategy)(0), // 7: uber.base.landstrategy.Strategy } -var file_merge_proto_depIdxs = []int32{ - 6, // 0: uber.runway.messagequeue.MergeStep.change:type_name -> uber.base.change.Change - 7, // 1: uber.runway.messagequeue.MergeStep.strategy:type_name -> uber.base.mergestrategy.Strategy - 1, // 2: uber.runway.messagequeue.MergeRequest.steps:type_name -> uber.runway.messagequeue.MergeStep +var file_land_proto_depIdxs = []int32{ + 6, // 0: uber.runway.messagequeue.LandStep.change:type_name -> uber.base.change.Change + 7, // 1: uber.runway.messagequeue.LandStep.strategy:type_name -> uber.base.landstrategy.Strategy + 1, // 2: uber.runway.messagequeue.LandRequest.steps:type_name -> uber.runway.messagequeue.LandStep 3, // 3: uber.runway.messagequeue.StepResult.outputs:type_name -> uber.runway.messagequeue.StepOutput - 0, // 4: uber.runway.messagequeue.MergeResult.outcome:type_name -> uber.runway.messagequeue.Outcome - 4, // 5: uber.runway.messagequeue.MergeResult.steps:type_name -> uber.runway.messagequeue.StepResult + 0, // 4: uber.runway.messagequeue.LandResult.outcome:type_name -> uber.runway.messagequeue.Outcome + 4, // 5: uber.runway.messagequeue.LandResult.steps:type_name -> uber.runway.messagequeue.StepResult 6, // [6:6] is the sub-list for method output_type 6, // [6:6] is the sub-list for method input_type 6, // [6:6] is the sub-list for extension type_name @@ -526,27 +527,27 @@ var file_merge_proto_depIdxs = []int32{ 0, // [0:6] is the sub-list for field type_name } -func init() { file_merge_proto_init() } -func file_merge_proto_init() { - if File_merge_proto != nil { +func init() { file_land_proto_init() } +func file_land_proto_init() { + if File_land_proto != nil { return } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: unsafe.Slice(unsafe.StringData(file_merge_proto_rawDesc), len(file_merge_proto_rawDesc)), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_land_proto_rawDesc), len(file_land_proto_rawDesc)), NumEnums: 1, NumMessages: 5, NumExtensions: 0, NumServices: 0, }, - GoTypes: file_merge_proto_goTypes, - DependencyIndexes: file_merge_proto_depIdxs, - EnumInfos: file_merge_proto_enumTypes, - MessageInfos: file_merge_proto_msgTypes, + GoTypes: file_land_proto_goTypes, + DependencyIndexes: file_land_proto_depIdxs, + EnumInfos: file_land_proto_enumTypes, + MessageInfos: file_land_proto_msgTypes, }.Build() - File_merge_proto = out.File - file_merge_proto_goTypes = nil - file_merge_proto_depIdxs = nil + File_land_proto = out.File + file_land_proto_goTypes = nil + file_land_proto_depIdxs = nil } diff --git a/api/runway/messagequeue/topics.go b/api/runway/messagequeue/topics.go index e80e7f9b9..92bd342ec 100644 --- a/api/runway/messagequeue/topics.go +++ b/api/runway/messagequeue/topics.go @@ -18,25 +18,25 @@ import "github.com/uber/submitqueue/platform/consumer" // TopicKey is the typed identifier used to look up a queue backend, topic name, // and subscription config in a consumer.TopicRegistry. The constants below are -// the wire topic names a client uses to publish to / consume from Runway's merge +// the wire topic names a client uses to publish to / consume from Runway's land // queues; they are the same strings each message lists in its topics option. type TopicKey = consumer.TopicKey const ( - // TopicKeyMergeConflictCheck carries dry-run merge-conflict check requests. - // A client publishes a MergeRequest here; Runway attempts the merge without - // committing and reports only whether it was mergeable. - TopicKeyMergeConflictCheck TopicKey = "merge-conflict-check" - // TopicKeyMergeConflictCheckSignal carries merge-conflict check results. - // Runway publishes a MergeResult here (with no produced revisions); the + // TopicKeyLandConflictCheck carries dry-run land-conflict check requests. + // A client publishes a LandRequest here; Runway attempts the land without + // committing and reports only whether it was landable. + TopicKeyLandConflictCheck TopicKey = "land-conflict-check" + // TopicKeyLandConflictCheckSignal carries land-conflict check results. + // Runway publishes a LandResult here (with no produced revisions); the // requesting client consumes it. - TopicKeyMergeConflictCheckSignal TopicKey = "merge-conflict-check-signal" - // TopicKeyMerge carries committing merge requests. A client publishes a - // MergeRequest here; Runway applies the steps, commits the result, and + TopicKeyLandConflictCheckSignal TopicKey = "land-conflict-check-signal" + // TopicKeyLand carries committing land requests. A client publishes a + // LandRequest here; Runway applies the steps, commits the result, and // reports the revisions it produced. - TopicKeyMerge TopicKey = "runway-merge" - // TopicKeyMergeSignal carries committing merge results. Runway publishes a - // MergeResult here (with the produced revisions populated); the requesting + TopicKeyLand TopicKey = "runway-land" + // TopicKeyLandSignal carries committing land results. Runway publishes a + // LandResult here (with the produced revisions populated); the requesting // client consumes it. - TopicKeyMergeSignal TopicKey = "merge-signal" + TopicKeyLandSignal TopicKey = "land-signal" ) diff --git a/api/submitqueue/gateway/proto/gateway.proto b/api/submitqueue/gateway/proto/gateway.proto index 272c7d013..c1ac2ed92 100644 --- a/api/submitqueue/gateway/proto/gateway.proto +++ b/api/submitqueue/gateway/proto/gateway.proto @@ -22,7 +22,7 @@ option java_outer_classname = "GatewayProto"; option java_package = "com.uber.submitqueue.gateway"; import "api/base/change/proto/change.proto"; -import "api/base/mergestrategy/proto/mergestrategy.proto"; +import "api/base/landstrategy/proto/landstrategy.proto"; // *************** // API domain definitions. @@ -46,7 +46,7 @@ message PingResponse { string hostname = 4; } -// LandRequest defines a request to land (merge into target branch of the source control repository) a set of code changes. +// LandRequest defines a request to land a set of code changes on the source control repository's target branch. // // SubmitQueue guarantees changes are landed in order with no other changes in between. // SubmitQueue does not guarantee each change is individually valid, but produces a validity marker on such changes. @@ -59,7 +59,7 @@ message LandRequest { // Source control integration strategy to use for this land operation. If not specified, the default queue strategy is used. // It applies to every URI the change carries, the same way to each — a land // request cannot pick a different strategy per URI. - uber.base.mergestrategy.Strategy strategy = 4; + uber.base.landstrategy.Strategy strategy = 4; } // LandResponse defines the response to a land request. @@ -261,7 +261,7 @@ service SubmitQueueGateway { // state transition is performed in the background by the orchestrator and may not have completed by the time the // caller receives a response. // - // Cancellation is NOT GUARANTEED: a request that has already merged, or that races to completion before the cancel + // Cancellation is NOT GUARANTEED: a request that has already landed, or that races to completion before the cancel // signal propagates through the pipeline, may still land (or end in an error). Callers must NOT assume that a // successful Cancel response means the request was cancelled — the actual terminal outcome (cancelled, landed, or // error) must be checked through the request-summary or request-history APIs. diff --git a/api/submitqueue/gateway/protopb/BUILD.bazel b/api/submitqueue/gateway/protopb/BUILD.bazel index 874e958ac..ef4aa4bb0 100644 --- a/api/submitqueue/gateway/protopb/BUILD.bazel +++ b/api/submitqueue/gateway/protopb/BUILD.bazel @@ -11,7 +11,7 @@ go_library( visibility = ["//visibility:public"], deps = [ "//api/base/change/protopb:go_default_library", - "//api/base/mergestrategy/protopb:go_default_library", + "//api/base/landstrategy/protopb:go_default_library", "@org_golang_google_grpc//:go_default_library", "@org_golang_google_grpc//codes:go_default_library", "@org_golang_google_grpc//status:go_default_library", diff --git a/api/submitqueue/gateway/protopb/gateway.pb.go b/api/submitqueue/gateway/protopb/gateway.pb.go index 301a53930..cc5bc9671 100644 --- a/api/submitqueue/gateway/protopb/gateway.pb.go +++ b/api/submitqueue/gateway/protopb/gateway.pb.go @@ -26,7 +26,7 @@ import ( unsafe "unsafe" protopb "github.com/uber/submitqueue/api/base/change/protopb" - protopb1 "github.com/uber/submitqueue/api/base/mergestrategy/protopb" + protopb1 "github.com/uber/submitqueue/api/base/landstrategy/protopb" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) @@ -157,7 +157,7 @@ func (x *PingResponse) GetHostname() string { return "" } -// LandRequest defines a request to land (merge into target branch of the source control repository) a set of code changes. +// LandRequest defines a request to land a set of code changes on the source control repository's target branch. // // SubmitQueue guarantees changes are landed in order with no other changes in between. // SubmitQueue does not guarantee each change is individually valid, but produces a validity marker on such changes. @@ -171,7 +171,7 @@ type LandRequest struct { // Source control integration strategy to use for this land operation. If not specified, the default queue strategy is used. // It applies to every URI the change carries, the same way to each — a land // request cannot pick a different strategy per URI. - Strategy protopb1.Strategy `protobuf:"varint,4,opt,name=strategy,proto3,enum=uber.base.mergestrategy.Strategy" json:"strategy,omitempty"` + Strategy protopb1.Strategy `protobuf:"varint,4,opt,name=strategy,proto3,enum=uber.base.landstrategy.Strategy" json:"strategy,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1341,18 +1341,18 @@ var File_gateway_proto protoreflect.FileDescriptor const file_gateway_proto_rawDesc = "" + "\n" + - "\rgateway.proto\x12\x18uber.submitqueue.gateway\x1a\"api/base/change/proto/change.proto\x1a0api/base/mergestrategy/proto/mergestrategy.proto\"'\n" + + "\rgateway.proto\x12\x18uber.submitqueue.gateway\x1a\"api/base/change/proto/change.proto\x1a.api/base/landstrategy/proto/landstrategy.proto\"'\n" + "\vPingRequest\x12\x18\n" + "\amessage\x18\x01 \x01(\tR\amessage\"\x85\x01\n" + "\fPingResponse\x12\x18\n" + "\amessage\x18\x01 \x01(\tR\amessage\x12!\n" + "\fservice_name\x18\x02 \x01(\tR\vserviceName\x12\x1c\n" + "\ttimestamp\x18\x03 \x01(\x03R\ttimestamp\x12\x1a\n" + - "\bhostname\x18\x04 \x01(\tR\bhostname\"\x94\x01\n" + + "\bhostname\x18\x04 \x01(\tR\bhostname\"\x93\x01\n" + "\vLandRequest\x12\x14\n" + "\x05queue\x18\x01 \x01(\tR\x05queue\x120\n" + - "\x06change\x18\x02 \x01(\v2\x18.uber.base.change.ChangeR\x06change\x12=\n" + - "\bstrategy\x18\x04 \x01(\x0e2!.uber.base.mergestrategy.StrategyR\bstrategy\"\"\n" + + "\x06change\x18\x02 \x01(\v2\x18.uber.base.change.ChangeR\x06change\x12<\n" + + "\bstrategy\x18\x04 \x01(\x0e2 .uber.base.landstrategy.StrategyR\bstrategy\"\"\n" + "\fLandResponse\x12\x12\n" + "\x04sqid\x18\x01 \x01(\tR\x04sqid\"Q\n" + "\rCancelRequest\x12\x12\n" + @@ -1479,11 +1479,11 @@ var file_gateway_proto_goTypes = []any{ nil, // 22: uber.submitqueue.gateway.RequestSummary.MetadataEntry nil, // 23: uber.submitqueue.gateway.HistoryEvent.MetadataEntry (*protopb.Change)(nil), // 24: uber.base.change.Change - (protopb1.Strategy)(0), // 25: uber.base.mergestrategy.Strategy + (protopb1.Strategy)(0), // 25: uber.base.landstrategy.Strategy } var file_gateway_proto_depIdxs = []int32{ 24, // 0: uber.submitqueue.gateway.LandRequest.change:type_name -> uber.base.change.Change - 25, // 1: uber.submitqueue.gateway.LandRequest.strategy:type_name -> uber.base.mergestrategy.Strategy + 25, // 1: uber.submitqueue.gateway.LandRequest.strategy:type_name -> uber.base.landstrategy.Strategy 22, // 2: uber.submitqueue.gateway.RequestSummary.metadata:type_name -> uber.submitqueue.gateway.RequestSummary.MetadataEntry 6, // 3: uber.submitqueue.gateway.GetRequestSummaryByIDResponse.request:type_name -> uber.submitqueue.gateway.RequestSummary 6, // 4: uber.submitqueue.gateway.GetRequestSummaryByChangeURIResponse.requests:type_name -> uber.submitqueue.gateway.RequestSummary diff --git a/api/submitqueue/gateway/protopb/gateway.pb.yarpc.go b/api/submitqueue/gateway/protopb/gateway.pb.yarpc.go index 889e83b75..82db386ab 100644 --- a/api/submitqueue/gateway/protopb/gateway.pb.yarpc.go +++ b/api/submitqueue/gateway/protopb/gateway.pb.yarpc.go @@ -576,76 +576,76 @@ var ( var yarpcFileDescriptorClosuref1a937782ebbded5 = [][]byte{ // gateway.proto []byte{ - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x57, 0x4b, 0x6f, 0xdb, 0x46, - 0x10, 0x0e, 0x45, 0x5b, 0xb6, 0x46, 0xb2, 0x6b, 0x6c, 0xd3, 0x94, 0x50, 0x6d, 0xc4, 0x66, 0x53, - 0x47, 0x87, 0x42, 0x0a, 0xdc, 0xf4, 0x81, 0x06, 0x09, 0x10, 0x27, 0xce, 0x03, 0xa8, 0x53, 0x85, - 0xae, 0x51, 0x20, 0x40, 0x21, 0xac, 0xa4, 0x89, 0x4c, 0xd8, 0x24, 0xe5, 0xdd, 0xa5, 0x5b, 0x19, - 0x3d, 0xf4, 0xd2, 0x4b, 0xcf, 0xbd, 0x17, 0xfd, 0x2d, 0x3d, 0xf5, 0x3f, 0xf4, 0xc7, 0x14, 0xfb, - 0x20, 0x45, 0xda, 0x22, 0xa3, 0x38, 0x39, 0x89, 0xbb, 0x3b, 0xcf, 0xef, 0x9b, 0x9d, 0x1d, 0xc1, - 0xca, 0x88, 0x0a, 0xfc, 0x99, 0x4e, 0xda, 0x63, 0x16, 0x89, 0x88, 0x38, 0x71, 0x1f, 0x59, 0x9b, - 0xc7, 0xfd, 0xc0, 0x17, 0xa7, 0x31, 0xc6, 0xd8, 0x36, 0xe7, 0x4d, 0x97, 0x8e, 0xfd, 0x4e, 0x9f, - 0x72, 0xec, 0x0c, 0x8e, 0x68, 0x38, 0xc2, 0x8e, 0x52, 0x30, 0x0b, 0xad, 0xdd, 0xbc, 0x93, 0xca, - 0x04, 0xc8, 0x46, 0xc8, 0x05, 0xa3, 0x02, 0x47, 0x13, 0x23, 0x9a, 0xdb, 0xd3, 0x1a, 0xee, 0x6d, - 0xa8, 0x77, 0xfd, 0x70, 0xe4, 0xe1, 0x69, 0x8c, 0x5c, 0x10, 0x07, 0x96, 0x02, 0xe4, 0x9c, 0x8e, - 0xd0, 0xb1, 0x36, 0xad, 0x56, 0xcd, 0x4b, 0x96, 0xee, 0xef, 0x16, 0x34, 0xb4, 0x24, 0x1f, 0x47, - 0x21, 0xc7, 0x62, 0x51, 0xb2, 0x05, 0x0d, 0x8e, 0xec, 0xcc, 0x1f, 0x60, 0x2f, 0xa4, 0x01, 0x3a, - 0x15, 0x75, 0x5c, 0x37, 0x7b, 0x2f, 0x68, 0x80, 0x64, 0x1d, 0x6a, 0xc2, 0x0f, 0x90, 0x0b, 0x1a, - 0x8c, 0x1d, 0x7b, 0xd3, 0x6a, 0xd9, 0xde, 0x74, 0x83, 0x34, 0x61, 0xf9, 0x28, 0xe2, 0x42, 0x29, - 0x2f, 0x28, 0xe5, 0x74, 0xed, 0xfe, 0x69, 0x41, 0xfd, 0x3b, 0x1a, 0x0e, 0x93, 0x88, 0xaf, 0xc3, - 0xa2, 0xc2, 0xc9, 0x04, 0xa1, 0x17, 0xe4, 0x0e, 0x54, 0x35, 0x30, 0xca, 0x79, 0x7d, 0xc7, 0x69, - 0x2b, 0x5c, 0x25, 0x34, 0x6d, 0x83, 0xd8, 0x23, 0xf5, 0xe3, 0x19, 0x39, 0x72, 0x1f, 0x96, 0x13, - 0x68, 0x94, 0xcf, 0xd5, 0x9d, 0xad, 0x8c, 0x4e, 0x1e, 0xba, 0x03, 0xf3, 0xe1, 0xa5, 0x2a, 0xae, - 0x0b, 0x0d, 0x1d, 0x95, 0x41, 0x87, 0xc0, 0x02, 0x3f, 0xf5, 0x87, 0x26, 0x2a, 0xf5, 0xed, 0xbe, - 0x84, 0x95, 0x47, 0x34, 0x1c, 0xe0, 0x49, 0x12, 0xfb, 0x0c, 0x21, 0x72, 0x03, 0xaa, 0x0c, 0x29, - 0x8f, 0x42, 0x03, 0x9b, 0x59, 0x4d, 0xf3, 0xb4, 0x33, 0x79, 0xba, 0x6b, 0xb0, 0x9a, 0x98, 0xd4, - 0x8e, 0xdd, 0x7f, 0x2b, 0xb0, 0x6a, 0xec, 0x1f, 0xc4, 0x41, 0x40, 0xd9, 0x64, 0xa6, 0x9b, 0xd4, - 0x5c, 0x25, 0x0b, 0xdb, 0x4d, 0xa8, 0x6b, 0x38, 0x7a, 0x31, 0xf3, 0xb9, 0x63, 0x6f, 0xda, 0xad, - 0x9a, 0x07, 0x7a, 0xeb, 0x90, 0xf9, 0x9c, 0xdc, 0x82, 0x55, 0x86, 0x03, 0xf4, 0xcf, 0x70, 0xd8, - 0xa3, 0xa2, 0x17, 0x70, 0x85, 0x95, 0xed, 0x35, 0x92, 0xdd, 0x87, 0x62, 0x9f, 0xcb, 0x1c, 0xb8, - 0xa0, 0x22, 0xe6, 0xce, 0xa2, 0xce, 0x41, 0xaf, 0xc8, 0x06, 0xc0, 0x09, 0xe5, 0xa2, 0x87, 0x8c, - 0x45, 0xcc, 0xa9, 0xaa, 0xb3, 0x9a, 0xdc, 0xd9, 0x93, 0x1b, 0xc4, 0x83, 0xe5, 0x00, 0x05, 0x1d, - 0x52, 0x41, 0x9d, 0xa5, 0x4d, 0xbb, 0x55, 0xdf, 0xf9, 0xaa, 0x5d, 0x74, 0x1d, 0xda, 0xf9, 0x1c, - 0xdb, 0xfb, 0x46, 0x71, 0x2f, 0x14, 0x6c, 0xe2, 0xa5, 0x76, 0x9a, 0xf7, 0x60, 0x25, 0x77, 0x44, - 0xd6, 0xc0, 0x3e, 0xc6, 0x89, 0xc1, 0x42, 0x7e, 0x4a, 0x28, 0xce, 0xe8, 0xc9, 0x14, 0x0a, 0xb5, - 0xf8, 0xb6, 0xf2, 0x8d, 0xe5, 0x3e, 0x83, 0xf5, 0xa7, 0x28, 0xf2, 0x9e, 0x76, 0x27, 0xcf, 0x1f, - 0x97, 0xf1, 0x37, 0x13, 0x58, 0x77, 0x00, 0x1b, 0x05, 0x96, 0x4c, 0xbd, 0xec, 0xc2, 0x12, 0xd3, - 0xa7, 0xca, 0x5a, 0x7d, 0xa7, 0x35, 0x6f, 0xea, 0x5e, 0xa2, 0xe8, 0xbe, 0x82, 0x4f, 0x67, 0x38, - 0xd1, 0x75, 0x7e, 0xe8, 0x3d, 0x4f, 0xa2, 0xde, 0x00, 0x98, 0x92, 0x6c, 0x62, 0xaf, 0xa5, 0x1c, - 0x17, 0x24, 0x70, 0x02, 0xb7, 0xca, 0x6d, 0x9b, 0x3c, 0x1e, 0xc3, 0xb2, 0x09, 0x87, 0x3b, 0x96, - 0xe2, 0x70, 0xfe, 0x44, 0x52, 0x4d, 0xf7, 0x1f, 0x79, 0xc9, 0x7d, 0x2e, 0xca, 0x2f, 0xf9, 0x5d, - 0xf8, 0x38, 0x5b, 0x8c, 0x11, 0xeb, 0xd1, 0xd7, 0x02, 0x99, 0xac, 0xca, 0x8a, 0xaa, 0xca, 0x0f, - 0xa7, 0x55, 0xf9, 0x3d, 0x7b, 0x28, 0xcf, 0xf6, 0x39, 0xf9, 0x1c, 0x48, 0xaa, 0xd5, 0xc7, 0xd7, - 0x11, 0x43, 0xa9, 0xa0, 0x7b, 0xd0, 0x5a, 0x72, 0xb2, 0xab, 0x0e, 0xf6, 0x39, 0xf9, 0x04, 0x6a, - 0x63, 0x3a, 0xc2, 0x1e, 0xf7, 0xcf, 0x75, 0x2f, 0x5a, 0xf4, 0x96, 0xe5, 0xc6, 0x81, 0x7f, 0x8e, - 0x12, 0x49, 0x75, 0x28, 0xa2, 0x63, 0x0c, 0x4d, 0xad, 0x2b, 0xf1, 0x1f, 0xe4, 0x86, 0xfb, 0x2b, - 0x34, 0x74, 0x12, 0xef, 0x13, 0x1b, 0xb2, 0x0d, 0x1f, 0x84, 0xf8, 0x8b, 0xe8, 0x65, 0x3c, 0x6b, - 0xa6, 0x56, 0xe4, 0x76, 0x37, 0xf5, 0x9e, 0x2b, 0xde, 0x67, 0x3e, 0x17, 0xd1, 0x55, 0x8b, 0xf7, - 0xef, 0x0a, 0x34, 0x8c, 0x81, 0xbd, 0x33, 0x0c, 0x85, 0x6c, 0xf0, 0x69, 0xb3, 0x96, 0xe0, 0x59, - 0x0a, 0xbc, 0x7a, 0xba, 0x97, 0x6b, 0x01, 0x95, 0x92, 0x16, 0x60, 0x5f, 0x6c, 0x01, 0xdd, 0x4c, - 0x0b, 0x58, 0x50, 0x10, 0xdd, 0x2d, 0x86, 0x28, 0x1b, 0x53, 0x51, 0x03, 0x90, 0x69, 0x8a, 0xc9, - 0x18, 0x0d, 0x3b, 0xea, 0x5b, 0xa6, 0x89, 0x52, 0xc9, 0xb4, 0x20, 0xbd, 0x78, 0xb7, 0x56, 0xd1, - 0xcb, 0x5e, 0xf0, 0x1c, 0xda, 0x86, 0xfc, 0x07, 0x50, 0x55, 0x6e, 0x12, 0xea, 0xb7, 0xe7, 0xcb, - 0xcb, 0x33, 0x5a, 0xf9, 0xcb, 0x9d, 0x3a, 0x78, 0x3f, 0x97, 0x7b, 0x98, 0x3e, 0x19, 0xc6, 0xf0, - 0xcc, 0xe2, 0x98, 0x66, 0x50, 0xb9, 0x52, 0x06, 0x61, 0xb6, 0x85, 0xcc, 0xca, 0xc0, 0x20, 0xf5, - 0x04, 0x6a, 0x47, 0xea, 0xd4, 0xc7, 0xf9, 0xef, 0x89, 0xb1, 0xe7, 0x4d, 0x55, 0xdd, 0x2d, 0x58, - 0xd4, 0x45, 0x55, 0x3c, 0xd4, 0x20, 0xdc, 0x38, 0x0c, 0x19, 0x0e, 0xa2, 0x51, 0xe8, 0x9f, 0xe3, - 0xf0, 0xa5, 0xb4, 0xac, 0x75, 0xbe, 0x84, 0x45, 0x5d, 0xa2, 0xba, 0x1b, 0xdf, 0x2c, 0x0e, 0x40, - 0xc9, 0x7b, 0x5a, 0xba, 0x00, 0xdf, 0xdf, 0x2c, 0xb8, 0x6e, 0xe2, 0x7c, 0x11, 0x89, 0x27, 0x51, - 0x1c, 0x0e, 0xdf, 0xc9, 0x4b, 0xc2, 0x4e, 0x25, 0xc3, 0x4e, 0x9e, 0x78, 0xfb, 0x02, 0xf1, 0x3b, - 0xff, 0x2d, 0x01, 0x39, 0x50, 0x76, 0x55, 0x92, 0x4f, 0xb5, 0x59, 0xf2, 0x23, 0x2c, 0xc8, 0xa1, - 0x8e, 0x7c, 0x56, 0xec, 0x39, 0x33, 0x1e, 0x36, 0xb7, 0xdf, 0x24, 0x66, 0x86, 0x90, 0x6b, 0xd2, - 0xb0, 0x9c, 0x87, 0xca, 0x0c, 0x67, 0xa6, 0xb8, 0x32, 0xc3, 0xd9, 0xb1, 0xca, 0xbd, 0x46, 0x7e, - 0x82, 0xaa, 0x9e, 0x78, 0xc8, 0xed, 0x62, 0x9d, 0xdc, 0x98, 0xd5, 0x6c, 0xbd, 0x59, 0x30, 0x35, - 0xff, 0x87, 0x05, 0x1f, 0xcd, 0x7c, 0xa9, 0x49, 0xc9, 0x2c, 0x52, 0x36, 0x24, 0x34, 0xbf, 0x7e, - 0x6b, 0xbd, 0x34, 0x98, 0xbf, 0xac, 0x99, 0x03, 0x48, 0x7a, 0x65, 0xc8, 0xfd, 0xb7, 0xb2, 0x7d, - 0xb1, 0x59, 0x34, 0x1f, 0x5c, 0x55, 0x3d, 0x47, 0xb3, 0xcf, 0x45, 0x29, 0xcd, 0xd3, 0x77, 0xbc, - 0x94, 0xe6, 0xcc, 0x4b, 0x79, 0x89, 0x87, 0x4c, 0x43, 0x9d, 0x8f, 0x87, 0xcb, 0xef, 0xdd, 0x7c, - 0x3c, 0xcc, 0xe8, 0xdc, 0x97, 0x78, 0xb8, 0xdc, 0xba, 0xe6, 0xe3, 0xa1, 0xb0, 0x69, 0xcf, 0xc7, - 0x43, 0x71, 0xc7, 0x74, 0xaf, 0xed, 0x1e, 0xc3, 0xfa, 0x20, 0x0a, 0x0a, 0xcd, 0xec, 0x36, 0xcc, - 0x85, 0xef, 0xca, 0x3f, 0x7d, 0x5d, 0xeb, 0xd5, 0xbd, 0x91, 0x2f, 0x8e, 0xe2, 0x7e, 0x7b, 0x10, - 0x05, 0x1d, 0xa9, 0xd4, 0xc9, 0x28, 0x75, 0xe4, 0x9f, 0xc8, 0xec, 0xda, 0x18, 0xd1, 0xff, 0x22, - 0xc7, 0xfd, 0x7e, 0x55, 0x7d, 0x7c, 0xf1, 0x7f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x9c, 0x51, 0x24, - 0x34, 0xbb, 0x0e, 0x00, 0x00, + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x57, 0xcd, 0x6e, 0xdb, 0x46, + 0x10, 0x36, 0x45, 0x5b, 0xb6, 0x46, 0xb2, 0x6b, 0x6c, 0xd3, 0x94, 0x50, 0x6d, 0xc4, 0x61, 0x53, + 0x47, 0x87, 0x82, 0x2a, 0xdc, 0xf4, 0x07, 0x4d, 0x1b, 0x20, 0x4a, 0x9c, 0x1f, 0xa0, 0x4e, 0x15, + 0xba, 0x46, 0x81, 0x00, 0x85, 0xb0, 0x92, 0x26, 0x32, 0x61, 0x91, 0x94, 0xb9, 0x4b, 0xb7, 0x32, + 0x7a, 0xe8, 0xa5, 0x97, 0x5e, 0xfb, 0x00, 0x45, 0x9f, 0xa5, 0xa7, 0xbe, 0x43, 0x1f, 0x26, 0xd8, + 0x1f, 0x52, 0xa4, 0x2d, 0x32, 0x8a, 0x93, 0x93, 0xb8, 0xbb, 0xf3, 0xfb, 0x7d, 0xb3, 0xb3, 0x23, + 0x58, 0x1f, 0x51, 0x8e, 0xbf, 0xd0, 0xa9, 0x33, 0x89, 0x42, 0x1e, 0x12, 0x2b, 0xee, 0x63, 0xe4, + 0xb0, 0xb8, 0xef, 0x7b, 0xfc, 0x34, 0xc6, 0x18, 0x1d, 0x7d, 0xde, 0xb4, 0xe9, 0xc4, 0x6b, 0xf7, + 0x29, 0xc3, 0xf6, 0xe0, 0x98, 0x06, 0x23, 0x6c, 0x4b, 0x05, 0xbd, 0x50, 0xda, 0x4d, 0x27, 0x95, + 0x19, 0xd3, 0x60, 0xc8, 0x78, 0x44, 0x39, 0x8e, 0xa6, 0x5a, 0x32, 0xbb, 0xa5, 0xe4, 0xed, 0xdb, + 0x50, 0xef, 0x7a, 0xc1, 0xc8, 0xc5, 0xd3, 0x18, 0x19, 0x27, 0x16, 0xac, 0xfa, 0xc8, 0x18, 0x1d, + 0xa1, 0x65, 0xec, 0x18, 0xad, 0x9a, 0x9b, 0x2c, 0xed, 0x3f, 0x0c, 0x68, 0x28, 0x49, 0x36, 0x09, + 0x03, 0x86, 0xc5, 0xa2, 0xe4, 0x26, 0x34, 0x18, 0x46, 0x67, 0xde, 0x00, 0x7b, 0x01, 0xf5, 0xd1, + 0xaa, 0xc8, 0xe3, 0xba, 0xde, 0x7b, 0x46, 0x7d, 0x24, 0x5b, 0x50, 0xe3, 0x9e, 0x8f, 0x8c, 0x53, + 0x7f, 0x62, 0x99, 0x3b, 0x46, 0xcb, 0x74, 0x67, 0x1b, 0xa4, 0x09, 0x6b, 0xc7, 0x21, 0xe3, 0x52, + 0x79, 0x59, 0x2a, 0xa7, 0x6b, 0xfb, 0x2f, 0x03, 0xea, 0xdf, 0xd3, 0x60, 0x98, 0x44, 0x7c, 0x0d, + 0x56, 0x24, 0x4a, 0x3a, 0x08, 0xb5, 0x20, 0x9f, 0x41, 0x55, 0xc1, 0x22, 0x9d, 0xd7, 0xf7, 0x2c, + 0x47, 0xa2, 0x2a, 0x80, 0x71, 0x34, 0x5e, 0x0f, 0xe4, 0x8f, 0xab, 0xe5, 0xc8, 0xb7, 0xb0, 0x96, + 0x40, 0x23, 0x7d, 0x6e, 0xec, 0xed, 0x64, 0x74, 0x72, 0xc8, 0x1d, 0xea, 0x0f, 0x37, 0xd5, 0xb0, + 0x6d, 0x68, 0xa8, 0xa0, 0x34, 0x38, 0x04, 0x96, 0xd9, 0xa9, 0x37, 0xd4, 0x41, 0xc9, 0x6f, 0xfb, + 0x39, 0xac, 0x3f, 0xa0, 0xc1, 0x00, 0xc7, 0x49, 0xe8, 0x73, 0x84, 0xc8, 0x75, 0xa8, 0x46, 0x48, + 0x59, 0x18, 0x68, 0xd4, 0xf4, 0x6a, 0x96, 0xa6, 0x99, 0x49, 0xd3, 0xde, 0x84, 0x8d, 0xc4, 0xa4, + 0x72, 0x6c, 0xff, 0x57, 0x81, 0x0d, 0x6d, 0xff, 0x30, 0xf6, 0x7d, 0x1a, 0x4d, 0xe7, 0xba, 0x49, + 0xcd, 0x55, 0xb2, 0xa8, 0xdd, 0x80, 0xba, 0x42, 0xa3, 0x17, 0x47, 0x1e, 0xb3, 0xcc, 0x1d, 0xb3, + 0x55, 0x73, 0x41, 0x6d, 0x1d, 0x45, 0x1e, 0x23, 0xb7, 0x60, 0x23, 0xc2, 0x01, 0x7a, 0x67, 0x38, + 0xec, 0x51, 0xde, 0xf3, 0x99, 0x84, 0xca, 0x74, 0x1b, 0xc9, 0xee, 0x7d, 0x7e, 0xc0, 0x44, 0x0e, + 0x8c, 0x53, 0x1e, 0x33, 0x6b, 0x45, 0xe5, 0xa0, 0x56, 0x64, 0x1b, 0x60, 0x4c, 0x19, 0xef, 0x61, + 0x14, 0x85, 0x91, 0x55, 0x95, 0x67, 0x35, 0xb1, 0xb3, 0x2f, 0x36, 0x88, 0x0b, 0x6b, 0x3e, 0x72, + 0x3a, 0xa4, 0x9c, 0x5a, 0xab, 0x3b, 0x66, 0xab, 0xbe, 0xf7, 0xa5, 0x53, 0x74, 0x17, 0x9c, 0x7c, + 0x8e, 0xce, 0x81, 0x56, 0xdc, 0x0f, 0x78, 0x34, 0x75, 0x53, 0x3b, 0xcd, 0xbb, 0xb0, 0x9e, 0x3b, + 0x22, 0x9b, 0x60, 0x9e, 0xe0, 0x54, 0x63, 0x21, 0x3e, 0x05, 0x14, 0x67, 0x74, 0x3c, 0x83, 0x42, + 0x2e, 0xbe, 0xa9, 0x7c, 0x6d, 0xd8, 0x4f, 0x60, 0xeb, 0x31, 0xf2, 0xbc, 0xa7, 0xce, 0xf4, 0xe9, + 0xc3, 0x32, 0xfe, 0xe6, 0x02, 0x6b, 0x0f, 0x60, 0xbb, 0xc0, 0x92, 0xae, 0x97, 0x0e, 0xac, 0x46, + 0xea, 0x54, 0x5a, 0xab, 0xef, 0xb5, 0x16, 0x4d, 0xdd, 0x4d, 0x14, 0xed, 0x17, 0xf0, 0xf1, 0x1c, + 0x27, 0xaa, 0xcc, 0x8f, 0xdc, 0xa7, 0x49, 0xd4, 0xdb, 0x00, 0x33, 0x92, 0x75, 0xec, 0xb5, 0x94, + 0xe3, 0x82, 0x04, 0xc6, 0x70, 0xab, 0xdc, 0xb6, 0xce, 0xe3, 0x21, 0xac, 0xe9, 0x70, 0x98, 0x65, + 0x48, 0x0e, 0x17, 0x4f, 0x24, 0xd5, 0xb4, 0xff, 0x15, 0x77, 0xdc, 0x63, 0xbc, 0xfc, 0x8e, 0xdf, + 0x81, 0x0f, 0xb3, 0xc5, 0x18, 0x46, 0x3d, 0xfa, 0x92, 0x63, 0x24, 0xaa, 0xb2, 0x22, 0xab, 0xf2, + 0xfd, 0x59, 0x55, 0xfe, 0x10, 0xdd, 0x17, 0x67, 0x07, 0x8c, 0x7c, 0x0a, 0x24, 0xd5, 0xea, 0xe3, + 0xcb, 0x30, 0x42, 0xa1, 0xa0, 0x5a, 0xd0, 0x66, 0x72, 0xd2, 0x91, 0x07, 0x07, 0x8c, 0x7c, 0x04, + 0xb5, 0x09, 0x1d, 0x61, 0x8f, 0x79, 0xe7, 0xaa, 0x15, 0xad, 0xb8, 0x6b, 0x62, 0xe3, 0xd0, 0x3b, + 0x47, 0x81, 0xa4, 0x3c, 0xe4, 0xe1, 0x09, 0x06, 0xba, 0xd6, 0xa5, 0xf8, 0x8f, 0x62, 0xc3, 0xfe, + 0x0d, 0x1a, 0x2a, 0x89, 0x77, 0x89, 0x0d, 0xd9, 0x85, 0xf7, 0x02, 0xfc, 0x95, 0xf7, 0x32, 0x9e, + 0x15, 0x53, 0xeb, 0x62, 0xbb, 0x9b, 0x7a, 0xcf, 0x15, 0xef, 0x13, 0x8f, 0xf1, 0xf0, 0xaa, 0xc5, + 0xfb, 0x4f, 0x05, 0x1a, 0xda, 0xc0, 0xfe, 0x19, 0x06, 0x5c, 0xf4, 0xf7, 0xb4, 0x57, 0x0b, 0xf0, + 0x0c, 0x09, 0x5e, 0x3d, 0xdd, 0xcb, 0xb5, 0x80, 0x4a, 0x49, 0x0b, 0x30, 0x2f, 0xb6, 0x80, 0x6e, + 0xa6, 0x05, 0x2c, 0x4b, 0x88, 0xee, 0x14, 0x43, 0x94, 0x8d, 0xa9, 0xa8, 0x01, 0x88, 0x34, 0xf9, + 0x74, 0x82, 0x9a, 0x1d, 0xf9, 0x2d, 0xd2, 0x44, 0xa1, 0xa4, 0x5b, 0x90, 0x5a, 0xbc, 0x5d, 0xab, + 0xe8, 0x65, 0x2f, 0x78, 0x0e, 0x6d, 0x4d, 0xfe, 0x3d, 0xa8, 0x4a, 0x37, 0x09, 0xf5, 0xbb, 0x8b, + 0xe5, 0xe5, 0x6a, 0xad, 0xfc, 0xe5, 0x4e, 0x1d, 0xbc, 0x9b, 0xcb, 0x3d, 0x4c, 0x9f, 0x0c, 0x6d, + 0x78, 0x6e, 0x71, 0xcc, 0x32, 0xa8, 0x5c, 0x29, 0x83, 0x20, 0xdb, 0x42, 0xe6, 0x65, 0xa0, 0x91, + 0x7a, 0x04, 0xb5, 0x63, 0x79, 0xea, 0xe1, 0xe2, 0xf7, 0x44, 0xdb, 0x73, 0x67, 0xaa, 0xf6, 0x4d, + 0x58, 0x51, 0x45, 0x55, 0x3c, 0xd3, 0x20, 0x5c, 0x3f, 0x0a, 0x22, 0x1c, 0x84, 0xa3, 0xc0, 0x3b, + 0xc7, 0xe1, 0x73, 0x61, 0x59, 0xe9, 0x7c, 0x01, 0x2b, 0xaa, 0x44, 0x55, 0x37, 0xbe, 0x51, 0x1c, + 0x80, 0x94, 0x77, 0x95, 0x74, 0x01, 0xbe, 0xbf, 0x1b, 0x70, 0x4d, 0xc7, 0xf9, 0x2c, 0xe4, 0x8f, + 0xc2, 0x38, 0x18, 0xbe, 0x95, 0x97, 0x84, 0x9d, 0x4a, 0x86, 0x9d, 0x3c, 0xf1, 0xe6, 0x05, 0xe2, + 0xf7, 0xfe, 0x5f, 0x05, 0x72, 0x28, 0xed, 0xca, 0x24, 0x1f, 0x2b, 0xb3, 0xe4, 0x27, 0x58, 0x16, + 0x33, 0x1d, 0xf9, 0xa4, 0xd8, 0x73, 0x66, 0x3a, 0x6c, 0xee, 0xbe, 0x4e, 0x4c, 0x0f, 0x21, 0x4b, + 0xc2, 0xb0, 0x98, 0x87, 0xca, 0x0c, 0x67, 0x86, 0xb8, 0x32, 0xc3, 0xd9, 0xb1, 0xca, 0x5e, 0x22, + 0x3f, 0x43, 0x55, 0x4d, 0x3c, 0xe4, 0x76, 0xb1, 0x4e, 0x6e, 0xcc, 0x6a, 0xb6, 0x5e, 0x2f, 0x98, + 0x9a, 0xff, 0xd3, 0x80, 0x0f, 0xe6, 0xbe, 0xd4, 0xa4, 0x64, 0x16, 0x29, 0x1b, 0x12, 0x9a, 0x5f, + 0xbd, 0xb1, 0x5e, 0x1a, 0xcc, 0xdf, 0xc6, 0xdc, 0x01, 0x24, 0xbd, 0x32, 0xe4, 0xbb, 0x37, 0xb2, + 0x7d, 0xb1, 0x59, 0x34, 0xef, 0x5d, 0x55, 0x3d, 0x47, 0xb3, 0xc7, 0x78, 0x29, 0xcd, 0xb3, 0x77, + 0xbc, 0x94, 0xe6, 0xcc, 0x4b, 0x79, 0x89, 0x87, 0x4c, 0x43, 0x5d, 0x8c, 0x87, 0xcb, 0xef, 0xdd, + 0x62, 0x3c, 0xcc, 0xe9, 0xdc, 0x97, 0x78, 0xb8, 0xdc, 0xba, 0x16, 0xe3, 0xa1, 0xb0, 0x69, 0x2f, + 0xc6, 0x43, 0x71, 0xc7, 0xb4, 0x97, 0x3a, 0x27, 0xb0, 0x35, 0x08, 0xfd, 0x42, 0x33, 0x9d, 0x86, + 0xbe, 0xf0, 0x5d, 0xf1, 0x9f, 0xaf, 0x6b, 0xbc, 0xb8, 0x3b, 0xf2, 0xf8, 0x71, 0xdc, 0x77, 0x06, + 0xa1, 0xdf, 0x16, 0x4a, 0xed, 0x8c, 0x52, 0x5b, 0xfc, 0x83, 0xcc, 0xae, 0xb5, 0x11, 0xf5, 0x1f, + 0x72, 0xd2, 0xef, 0x57, 0xe5, 0xc7, 0xe7, 0xaf, 0x02, 0x00, 0x00, 0xff, 0xff, 0xa6, 0x8e, 0x5a, + 0x97, 0xb8, 0x0e, 0x00, 0x00, }, // api/base/change/proto/change.proto []byte{ @@ -660,21 +660,21 @@ var yarpcFileDescriptorClosuref1a937782ebbded5 = [][]byte{ 0x24, 0x25, 0xb1, 0x81, 0x19, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0xbf, 0x91, 0x62, 0xef, 0xc4, 0x00, 0x00, 0x00, }, - // api/base/mergestrategy/proto/mergestrategy.proto + // api/base/landstrategy/proto/landstrategy.proto []byte{ - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x32, 0x48, 0x2c, 0xc8, 0xd4, - 0x4f, 0x4a, 0x2c, 0x4e, 0xd5, 0xcf, 0x4d, 0x2d, 0x4a, 0x4f, 0x2d, 0x2e, 0x29, 0x4a, 0x2c, 0x49, - 0x4d, 0xaf, 0xd4, 0x2f, 0x28, 0xca, 0x2f, 0xc9, 0x47, 0x15, 0xd3, 0x03, 0x8b, 0x09, 0x89, 0x97, - 0x26, 0xa5, 0x16, 0xe9, 0x81, 0xb4, 0xe8, 0xa1, 0x48, 0x6b, 0xf9, 0x71, 0x71, 0x04, 0x43, 0xd9, - 0x42, 0xdc, 0x5c, 0xec, 0x2e, 0xae, 0x6e, 0x8e, 0xa1, 0x3e, 0x21, 0x02, 0x0c, 0x42, 0x5c, 0x5c, - 0x6c, 0x41, 0xae, 0x4e, 0x8e, 0xc1, 0xae, 0x02, 0x8c, 0x42, 0x82, 0x5c, 0xbc, 0xc1, 0x81, 0xa1, - 0x8e, 0xc1, 0x1e, 0xf1, 0x50, 0x21, 0x26, 0x21, 0x4e, 0x2e, 0x56, 0x5f, 0xd7, 0x20, 0x77, 0x57, - 0x01, 0x66, 0x90, 0xb6, 0x80, 0x20, 0x7f, 0x5f, 0xff, 0x10, 0x57, 0x01, 0x16, 0xa7, 0x6a, 0x2e, - 0xf5, 0xe4, 0xfc, 0x5c, 0x3d, 0xb0, 0x75, 0xc5, 0xa5, 0x49, 0xb9, 0x99, 0x25, 0x85, 0xa5, 0xa9, - 0xa5, 0xa9, 0x58, 0xac, 0x76, 0x12, 0xf2, 0x05, 0x71, 0x61, 0xb6, 0x07, 0x80, 0xdc, 0x19, 0xc0, - 0x18, 0x65, 0x95, 0x9e, 0x59, 0x92, 0x51, 0x9a, 0xa4, 0x97, 0x9c, 0x9f, 0xab, 0x0f, 0x32, 0x45, - 0x1f, 0xc9, 0x14, 0x7d, 0x7c, 0xfe, 0x2e, 0x48, 0x4a, 0x62, 0x03, 0x33, 0x8c, 0x01, 0x01, 0x00, - 0x00, 0xff, 0xff, 0x7a, 0x05, 0x07, 0xe6, 0x20, 0x01, 0x00, 0x00, + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xd2, 0x4b, 0x2c, 0xc8, 0xd4, + 0x4f, 0x4a, 0x2c, 0x4e, 0xd5, 0xcf, 0x49, 0xcc, 0x4b, 0x29, 0x2e, 0x29, 0x4a, 0x2c, 0x49, 0x4d, + 0xaf, 0xd4, 0x2f, 0x28, 0xca, 0x2f, 0xc9, 0x47, 0x11, 0xd2, 0x03, 0x0b, 0x09, 0x89, 0x95, 0x26, + 0xa5, 0x16, 0xe9, 0x81, 0x34, 0xe8, 0x21, 0xcb, 0x6a, 0xf9, 0x71, 0x71, 0x04, 0x43, 0xd9, 0x42, + 0xdc, 0x5c, 0xec, 0x2e, 0xae, 0x6e, 0x8e, 0xa1, 0x3e, 0x21, 0x02, 0x0c, 0x42, 0x5c, 0x5c, 0x6c, + 0x41, 0xae, 0x4e, 0x8e, 0xc1, 0xae, 0x02, 0x8c, 0x42, 0x82, 0x5c, 0xbc, 0xc1, 0x81, 0xa1, 0x8e, + 0xc1, 0x1e, 0xf1, 0x50, 0x21, 0x26, 0x21, 0x4e, 0x2e, 0x56, 0x5f, 0xd7, 0x20, 0x77, 0x57, 0x01, + 0x66, 0x90, 0xb6, 0x80, 0x20, 0x7f, 0x5f, 0xff, 0x10, 0x57, 0x01, 0x16, 0xa7, 0x0a, 0x2e, 0xb5, + 0xe4, 0xfc, 0x5c, 0x3d, 0xb0, 0x6d, 0xc5, 0xa5, 0x49, 0xb9, 0x99, 0x25, 0x85, 0xa5, 0xa9, 0xa5, + 0xa9, 0x98, 0x36, 0x3b, 0x09, 0xfa, 0x24, 0xe6, 0xa5, 0xc0, 0xec, 0x0e, 0x00, 0x39, 0x32, 0x80, + 0x31, 0xca, 0x32, 0x3d, 0xb3, 0x24, 0xa3, 0x34, 0x49, 0x2f, 0x39, 0x3f, 0x57, 0x1f, 0x64, 0x86, + 0x3e, 0x92, 0x19, 0xfa, 0x78, 0xbc, 0x5c, 0x90, 0x94, 0xc4, 0x06, 0x66, 0x18, 0x03, 0x02, 0x00, + 0x00, 0xff, 0xff, 0x12, 0x08, 0xd3, 0xbf, 0x1a, 0x01, 0x00, 0x00, }, } diff --git a/api/submitqueue/gateway/protopb/gateway_grpc.pb.go b/api/submitqueue/gateway/protopb/gateway_grpc.pb.go index ac9a4a2fa..79f70f8e2 100644 --- a/api/submitqueue/gateway/protopb/gateway_grpc.pb.go +++ b/api/submitqueue/gateway/protopb/gateway_grpc.pb.go @@ -62,7 +62,7 @@ type SubmitQueueGatewayClient interface { // state transition is performed in the background by the orchestrator and may not have completed by the time the // caller receives a response. // - // Cancellation is NOT GUARANTEED: a request that has already merged, or that races to completion before the cancel + // Cancellation is NOT GUARANTEED: a request that has already landed, or that races to completion before the cancel // signal propagates through the pipeline, may still land (or end in an error). Callers must NOT assume that a // successful Cancel response means the request was cancelled — the actual terminal outcome (cancelled, landed, or // error) must be checked through the request-summary or request-history APIs. @@ -185,7 +185,7 @@ type SubmitQueueGatewayServer interface { // state transition is performed in the background by the orchestrator and may not have completed by the time the // caller receives a response. // - // Cancellation is NOT GUARANTEED: a request that has already merged, or that races to completion before the cancel + // Cancellation is NOT GUARANTEED: a request that has already landed, or that races to completion before the cancel // signal propagates through the pipeline, may still land (or end in an error). Callers must NOT assume that a // successful Cancel response means the request was cancelled — the actual terminal outcome (cancelled, landed, or // error) must be checked through the request-summary or request-history APIs. diff --git a/doc/howto/QUICKSTART.md b/doc/howto/QUICKSTART.md index da49f4896..9130d39cf 100644 --- a/doc/howto/QUICKSTART.md +++ b/doc/howto/QUICKSTART.md @@ -183,7 +183,7 @@ QUEUE_LOG_LEVEL=debug make local-submitqueue-start The queue's own logic is real in every mode: validation, batching, conflict analysis, speculation, the request log, and the whole trail from `accepted` to `landed`. -In `fake` mode, everything at the edges is not. There is no repository, so a change is a URI that points at nothing; the build runner passes instantly; and Runway uses the **noop merger**, which reports success without touching anything. `landed` here means the pipeline ran to completion — not that a commit exists anywhere. +In `fake` mode, everything at the edges is not. There is no repository, so a change is a URI that points at nothing; the build runner passes instantly; and Runway uses the **noop lander**, which reports success without touching anything. `landed` here means the pipeline ran to completion — not that a commit exists anywhere. For that, take the next rung. @@ -200,7 +200,7 @@ Still no credential. `PROVIDER=git` provisions a bare repository at `/tmp/sq-san ✅ Stack is running against provider 'git'. Gateway gRPC port: 55295 -Merge target: /tmp/sq-sandbox/sandbox.git +Land target: /tmp/sq-sandbox/sandbox.git ``` Then the same command as before, unchanged: @@ -209,7 +209,7 @@ Then the same command as before, unchanged: make demo-requests ``` -`demo-requests` creates changes for whichever provider the running stack was started with, so there is nothing to repeat and nothing to keep in sync. The two must agree — a fake change points at no repository, so a stack running the git merger rejects every one of them as a commit it cannot find — and rather than leaving that to memory, a run with no `PROVIDER` of its own asks the stack which one it has. Passing one that disagrees still works, and says so before it starts. +`demo-requests` creates changes for whichever provider the running stack was started with, so there is nothing to repeat and nothing to keep in sync. The two must agree — a fake change points at no repository, so a stack running the git lander rejects every one of them as a commit it cannot find — and rather than leaving that to memory, a run with no `PROVIDER` of its own asks the stack which one it has. Passing one that disagrees still works, and says so before it starts. Now `demo-requests` pushes real branches with real commits, and landing them is a real cherry-pick and push. Look at the repository itself: @@ -244,7 +244,7 @@ The last rung is the only one that needs credentials, and the only one where a c ### What you need -A **scratch repository** you are willing to have commits pushed to and branches force-moved on. Do not point this at anything you care about — the merger pushes to the target branch and rewrites the head branch of every change it lands. +A **scratch repository** you are willing to have commits pushed to and branches force-moved on. Do not point this at anything you care about — the lander pushes to the target branch and rewrites the head branch of every change it lands. A **token** for it, scoped to that one repository. @@ -253,7 +253,7 @@ For a **fine-grained** token, grant these repository permissions. Each is here b | Permission | Access | Needed by | |---|---|---| | Metadata | Read | mandatory on every fine-grained token; GitHub adds it for you | -| Contents | Read and write | the git merger — clone, fetch, push to the target branch, and force-move each landed change's head branch | +| Contents | Read and write | the git lander — clone, fetch, push to the target branch, and force-move each landed change's head branch | | Pull requests | Read | the change provider reads pull request metadata, and `land -pr` reads the head commit | | Pull requests | Read **and write** | only for `make demo-requests`, which opens pull requests | | Actions | Read and write | the build runner — dispatch a run per batch, poll it, cancel it | @@ -264,7 +264,7 @@ Two things people get caught by. Fine-grained tokens must have the repository ex ### Configure and run -Everything provider-specific is one directory: [`service/submitqueue/demo/provider/github/`](../../service/submitqueue/demo/provider/github). Edit the three marked lines in `merge.yaml`: +Everything provider-specific is one directory: [`service/submitqueue/demo/provider/github/`](../../service/submitqueue/demo/provider/github). Edit the three marked lines in `land.yaml`: ```yaml remoteUrl: https://github.com//.git @@ -364,9 +364,9 @@ make land QUEUE=demo-queue \ URI='git://demo.example.com/demo/refs%2Fheads%2Fbad/2222222222222222222222222222222222222222?sq-fake=build-fail' ``` -That request walks the same path as far as `speculating`, records `building`, and then goes terminal at `error` instead of landing. Other tokens follow the same `sq-fake=` convention and are documented on the fake they drive — `provider-error` on the change provider, `unmergeable` and `mergecheck-error` on the merge checker, `trigger-error` and `build-error` on the build runner. +That request walks the same path as far as `speculating`, records `building`, and then goes terminal at `error` instead of landing. Other tokens follow the same `sq-fake=` convention and are documented on the fake they drive — `provider-error` on the change provider, `unlandable` and `landcheck-error` on the land checker, `trigger-error` and `build-error` on the build runner. -A hand-written URI like the one above belongs to the `fake` rung alone. On `git` it names a commit the merger cannot fetch, and on `github` the change provider tries to resolve it as a pull request — both fail, but for reasons that have nothing to do with the marker. +A hand-written URI like the one above belongs to the `fake` rung alone. On `git` it names a commit the lander cannot fetch, and on `github` the change provider tries to resolve it as a pull request — both fail, but for reasons that have nothing to do with the marker. Submit a good change into the **same folder** as a failing one and you can watch what makes a queue worth having: the two are batched in order, and the second speculates on the first landing. When the first fails, that guess is contradicted, the second re-plans, and it lands anyway. @@ -390,11 +390,11 @@ Both MySQL services mount **anonymous** volumes, so a stop/start cycle orphans a **A land is rejected before it returns an sqid.** The URI failed validation: 40 hex characters of SHA, and a percent-encoded `refs/…` ref. -**Everything reports `error` immediately.** Check the queue name exists in [`queues.yaml`](../../service/submitqueue/gateway/server/queues.yaml) and is configured in the provider directory you are running. A queue with no entry in `merge.yaml` gets the noop merger by design, so it will appear to land without pushing anything. +**Everything reports `error` immediately.** Check the queue name exists in [`queues.yaml`](../../service/submitqueue/gateway/server/queues.yaml) and is configured in the provider directory you are running. A queue with no entry in `land.yaml` gets the noop lander by design, so it will appear to land without pushing anything. **`PROVIDER=git` lands report `error`.** Read Runway's log and look for the git command that failed. A change whose branch was never pushed to the sandbox, or a `demo-requests` run whose `PROVIDER` did not match the stack's, both surface here as a failed fetch or cherry-pick. -**`PROVIDER=github`: the push is rejected on the first try.** Branch protection on `main` — required status checks, or a linear-history or no-force-push rule — applies to the merger like anyone else. Either relax it on the scratch repo or add the token's identity to the bypass list. +**`PROVIDER=github`: the push is rejected on the first try.** Branch protection on `main` — required status checks, or a linear-history or no-force-push rule — applies to the lander like anyone else. Either relax it on the scratch repo or add the token's identity to the bypass list. **`PROVIDER=github`: the change lands but the pull request stays open.** Two causes, distinguishable in Runway's logs. If the change came from a **fork**, this is expected and permanent: the head branch lives in the contributor's repository, which this stack has no business writing to, and the log says `no head branch on this remote for change`. Otherwise it is **protection on the head branch** blocking the force update, logged as `could not move change head branch`. The land itself succeeded either way — the failure is reported and deliberately not retried, because the push already happened and cannot be undone. diff --git a/doc/howto/TESTING.md b/doc/howto/TESTING.md index c85d941e5..c1cc36c29 100644 --- a/doc/howto/TESTING.md +++ b/doc/howto/TESTING.md @@ -30,7 +30,7 @@ SubmitQueue uses **two separate databases** to demonstrate proper architectural ### 2. Queue Database - **Purpose**: Messaging infrastructure (queue messages, offsets, partition leases) - **Schema**: `platform/extension/messagequeue/mysql/schema` -- **Used by**: Gateway (publishes and consumes), Orchestrator (publishes and consumes), Runway (consumes merge work and publishes results) +- **Used by**: Gateway (publishes and consumes), Orchestrator (publishes and consumes), Runway (consumes land work and publishes results) - **Connection**: `QUEUE_MYSQL_DSN` **Why separate?** diff --git a/doc/rfc/change-uri.md b/doc/rfc/change-uri.md index 05fd0c959..797eca8fb 100644 --- a/doc/rfc/change-uri.md +++ b/doc/rfc/change-uri.md @@ -6,7 +6,7 @@ A change URI is the system-wide identity of a code change — a Pull Request, a Every change URI is an RFC 3986 URI of the form `scheme://{host[:port]}/{path}`, with a uniform division of labor: -- **scheme** — the provider *model*: how to parse the path and which extension family (change provider, merge checker, pusher) can act on it. One scheme per model — deployment flavors of the same model (github.com vs. GitHub Enterprise) do **not** get their own schemes, because the flavor is derivable from the host and two spellings for one instance would break identity. +- **scheme** — the provider *model*: how to parse the path and which extension family (change provider, land checker, pusher) can act on it. One scheme per model — deployment flavors of the same model (github.com vs. GitHub Enterprise) do **not** get their own schemes, because the flavor is derivable from the host and two spellings for one instance would break identity. - **authority** — the provider *instance*: the `host[:port]` the change lives on. Mandatory. - **path** — the change within that instance, pinned to an exact code state (head SHA or diff ID), so staleness is detectable by comparing the pin against the provider's current state. diff --git a/doc/rfc/consumer-gate.md b/doc/rfc/consumer-gate.md index 342aac93e..851408b21 100644 --- a/doc/rfc/consumer-gate.md +++ b/doc/rfc/consumer-gate.md @@ -27,7 +27,7 @@ The re-check loop has a bounded cost: while a gate is closed, each blocked parti ### Gate identity: consumer group, optionally narrowed to a partition -Every controller subscribes with a unique consumer group (`orchestrator-batch`, `runway-merge`, …), so the consumer group *is* the controller's stable runtime name — the natural key for "stop this controller". A gate may optionally carry a partition key; absent one, it gates every partition. Partition-scoped gates keep unrelated traffic flowing through the same controller (e.g. parking one test queue's partition while other queues proceed), which matters if e2e scenarios ever run concurrently. +Every controller subscribes with a unique consumer group (`orchestrator-batch`, `runway-land`, …), so the consumer group *is* the controller's stable runtime name — the natural key for "stop this controller". A gate may optionally carry a partition key; absent one, it gates every partition. Partition-scoped gates keep unrelated traffic flowing through the same controller (e.g. parking one test queue's partition while other queues proceed), which matters if e2e scenarios ever run concurrently. ### Gate state is a separate extension @@ -75,9 +75,9 @@ If gate state cannot be read (directory missing, I/O error), the check logs, inc The cancellation scenario, expressed as stop → observe → start: -1. The test closes the gate for `runway-mergeconflictcheck` (all partitions, or scoped to the test queue's partition key), before landing. -2. It lands a request. The orchestrator runs it to the merge-conflict-check hand-off; runway's subscriber delivers the check message, and the gate parks it. -3. The test awaits the parked record — proof the controller is stopped *and* holding exactly this message. Runway itself is still running; its RPC surface and merge controller are untouched. +1. The test closes the gate for `runway-landconflictcheck` (all partitions, or scoped to the test queue's partition key), before landing. +2. It lands a request. The orchestrator runs it to the land-conflict-check hand-off; runway's subscriber delivers the check message, and the gate parks it. +3. The test awaits the parked record — proof the controller is stopped *and* holding exactly this message. Runway itself is still running; its RPC surface and land controller are untouched. 4. While stopped, the test observes and acts: it cancels the request, awaits the terminal `cancelled` status through the existing event plane, and asserts no batch ever enrolled the request. 5. The test opens the gate. Within a re-check tick the postponed delivery redelivers, clears the open gate, and proceeds into the controller as a fresh attempt (postponing resets retry accounting); runway answers the now-stale check, and the test asserts the signal is dropped for the halted request. diff --git a/doc/rfc/hook-framework.md b/doc/rfc/hook-framework.md index bd035a8ca..e4a5ae601 100644 --- a/doc/rfc/hook-framework.md +++ b/doc/rfc/hook-framework.md @@ -4,9 +4,9 @@ Fire-and-forget side effects for pipeline lifecycle events: one shared event con ## Problem -The pipelines emit lifecycle transitions — a request lands or fails, a batch merges, a build finishes — but nothing can react outside pipeline state: no warehouse export, no PR comments or closes on merge events, no notifications or audit trails. The log topic is not this seam: SubmitQueue request statuses only, consumed solely to build gateway read models. +The pipelines emit lifecycle transitions — a request lands or fails, a batch lands, a build finishes — but nothing can react outside pipeline state: no warehouse export, no PR comments or closes on land events, no notifications or audit trails. The log topic is not this seam: SubmitQueue request statuses only, consumed solely to build gateway read models. -Two requirements: side effects must never stall or fail the pipeline, and "fire and forget" must not mean lossy — a merge-failure comment that silently never posts is a support ticket. +Two requirements: side effects must never stall or fail the pipeline, and "fire and forget" must not mean lossy — a land-failure comment that silently never posts is a support ticket. ## Proposal @@ -54,7 +54,7 @@ Delivery promise: - `api/base/hook/`: no owning domain, so the message-queue location rule extends — platform-owned contracts live under `api/base/`. - Envelope = only fields every consumer keys on uniformly; subject, queue, and error are occurrence facts → payload. `source`/`type` are strings, not enums, for additive evolution. -- Payload (`Struct`): shaped per type, add-only, documented by its domain; must carry the subject's id and transient facts (merge step outcomes, build failure detail) — the event is their only durable record. Never entity snapshots; hooks resolve entities from stores. +- Payload (`Struct`): shaped per type, add-only, documented by its domain; must carry the subject's id and transient facts (land step outcomes, build failure detail) — the event is their only durable record. Never entity snapshots; hooks resolve entities from stores. ### Hooks and dispatch @@ -93,7 +93,7 @@ message HookEvent { } ``` -A failed batch, carrying merge-result facts persisted nowhere else (protojson: int64 as string, empty fields omitted): +A failed batch, carrying land-result facts persisted nowhere else (protojson: int64 as string, empty fields omitted): ```json { @@ -105,7 +105,7 @@ A failed batch, carrying merge-result facts persisted nowhere else (protojson: i "payload": { "batch_id": "batch-778", "queue": "go-monorepo", - "error": "merge conflict", + "error": "land conflict", "failed_step": "sq-12346", "conflict_paths": ["foo/bar.go"] } @@ -116,7 +116,7 @@ A failed batch, carrying merge-result facts persisted nowhere else (protojson: i - **A contract per domain.** N schemas, N hook shapes, N warehouse tables; one envelope absorbs differences additively. - **Inline hook calls.** Couples pipeline latency to integrations; a crash between write and call silently drops the notification. -- **A second consumer group on the log topic.** Request statuses only; no path to batch, build, merge, or other domains. +- **A second consumer group on the log topic.** Request statuses only; no path to batch, build, land, or other domains. - **Enums for source/type.** protojson rejects unknown enum values; every addition would break consumers. - **Subject, queue, or error on the envelope.** Occurrence facts; they live in the payload. No major event platform carries a top-level error. - **Entity snapshots as payload.** Stale on redelivery; competes with the store; drags domain schemas into the shared contract. diff --git a/doc/rfc/index.md b/doc/rfc/index.md index d3319e661..e33e03104 100644 --- a/doc/rfc/index.md +++ b/doc/rfc/index.md @@ -13,7 +13,7 @@ Design documents and technical proposals, grouped by scope. Shared/cross-cutting ## SubmitQueue -- [Orchestrator Workflow](submitqueue/workflow.md) - Queue-driven controller pipeline from gateway entry through batching, scoring, build, merge, and conclude +- [Orchestrator Workflow](submitqueue/workflow.md) - Queue-driven controller pipeline from gateway entry through batching, scoring, build, land, and conclude - [Gateway History APIs](submitqueue/history-api.md) - Request lifecycle history exposed through separate request ID and change ID endpoints - [Build Runner](submitqueue/build-runner.md) - Vendor-agnostic BuildRunner interface, provider-neutral BuildStatus lifecycle, and how the orchestrator wires it into the build stage - [Extension Contract](submitqueue/extension-contract.md) - When extensions take orchestrator identity (request/batch) and resolve granular content themselves vs. take controller-resolved data; revises the BuildRunner base/head contract @@ -24,7 +24,7 @@ Design documents and technical proposals, grouped by scope. Shared/cross-cutting ## Stovepipe -- [Stovepipe Workflow](stovepipe/workflow.md) - Post-merge validation pipeline overview: ingest, process, build, record greenness, analyze projects, notify downstream +- [Stovepipe Workflow](stovepipe/workflow.md) - Post-land validation pipeline overview: ingest, process, build, record greenness, analyze projects, notify downstream - [Process stage](stovepipe/steps/process.md) - Build-strategy decision, per-queue concurrency gate, backlog coalescing, entity model, platform prerequisites - [Build stage](stovepipe/steps/build.md) - Trigger-only stage and Stovepipe's URI-based BuildRunner contract - [Buildsignal stage](stovepipe/steps/buildsignal.md) - Build polling, terminal status persistence, and the handoff to record @@ -34,4 +34,4 @@ Design documents and technical proposals, grouped by scope. Shared/cross-cutting ## Runway -- [Runway Workflow](runway/workflow.md) - Landing service: merge-conflict checking and merging on behalf of SubmitQueue +- [Runway Workflow](runway/workflow.md) - Landing service: land-conflict checking and landing on behalf of SubmitQueue diff --git a/doc/rfc/messagequeue-contract.md b/doc/rfc/messagequeue-contract.md index c52201e53..06220c9d6 100644 --- a/doc/rfc/messagequeue-contract.md +++ b/doc/rfc/messagequeue-contract.md @@ -57,7 +57,7 @@ So the option earns its place as the single, language-neutral source of truth fo ### Go binding: the generated `protopb` -The generated message types in `protopb` are the Go binding, sitting beside `proto/` exactly as for the RPC contracts. The contract package adds only thin helpers — `protojson` (de)serialization and the `topic_keys` reflection lookup. Shared field types (`change.Change`, `mergestrategy.MergeStrategy`) are themselves shared protos under `api/base/{change,mergestrategy}/proto`, imported by every contract that needs them. +The generated message types in `protopb` are the Go binding, sitting beside `proto/` exactly as for the RPC contracts. The contract package adds only thin helpers — `protojson` (de)serialization and the `topic_keys` reflection lookup. Shared field types (`change.Change`, `landstrategy.Strategy`) are themselves shared protos under `api/base/{change,landstrategy}/proto`, imported by every contract that needs them. ## Example @@ -82,9 +82,9 @@ message ExampleRequest { message ExampleResult { // One shape, two queues: the same result is published under the check-result - // key for a dry run and the merge-result key for a committing run. + // key for a dry run and the land-result key for a committing run. option (uber.base.messagequeue.topic_keys) = "example-check-result"; - option (uber.base.messagequeue.topic_keys) = "example-merge-result"; + option (uber.base.messagequeue.topic_keys) = "example-land-result"; string id = 1; // Echoes the request's correlation id. bool success = 2; diff --git a/doc/rfc/runway/workflow.md b/doc/rfc/runway/workflow.md index b6d1cf464..96c7157ab 100644 --- a/doc/rfc/runway/workflow.md +++ b/doc/rfc/runway/workflow.md @@ -1,22 +1,26 @@ # Runway Workflow -Runway is the landing service: it owns VCS operations — mergeability checking and landing — on behalf of SubmitQueue. Runway is a single service (the domain *is* the service): it subscribes to two inbound topics (`merge-conflict-check`, `runway-merge`) and publishes results to two outbound topics (`merge-conflict-check-signal`, `merge-signal`). It is a consumer-only service with no gateway; work arrives via topic queues and results leave via topic queues. +Runway is the landing service: it owns VCS operations — landability checking and landing — on behalf of SubmitQueue. Runway is a single service (the domain *is* the service): it subscribes to two inbound topics (`land-conflict-check`, `runway-land`) and publishes results to two outbound topics (`land-conflict-check-signal`, `land-signal`). It is a consumer-only service with no gateway; work arrives via topic queues and results leave via topic queues. -## Merge-conflict check and merge +## Land-conflict check and land The two queues are the same shape but different commit semantics: -- **merge-conflict-check** is a dry run. A merge request carries an ordered sequence of steps (changes + merge strategy). Runway performs a read-only trial merge and publishes per-step mergeability results back. +- **land-conflict-check** is a dry run. A land request carries an ordered sequence of steps (changes + land strategy). Runway performs a read-only trial land and publishes per-step landability results back. -- **merge** is the committing version. A merge request carries the same payload but Runway commits the result and reports the revisions it produced (per-step output IDs). +- **land** is the committing version. A land request carries the same payload but Runway commits the result and reports the revisions it produced (per-step output IDs). -- **promote** is a special `PROMOTE` strategy on the `merge` queue: it pushes a commit to a ref as-is (`--ff-only`). The primary use case is forwarding a landed SHA from `main` to `verified/main` without creating a new merge commit. +- **promote** is a special `PROMOTE` strategy on the `land` queue: it pushes a commit to a ref as-is (`--ff-only`). The primary use case is forwarding a landed SHA from `main` to `verified/main` without creating a new merge commit. -These are independent input-output flows. A merge-conflict check can run without a merge ever running, and a merge does not depend on a prior check. +These are independent input-output flows. A land-conflict check can run without a land ever running, and a land does not depend on a prior check. + +## Vocabulary + +*Land* is SubmitQueue's verb for integrating a change into the target branch: the public API (`Land`, `LandRequest`), the request lifecycle (`landing`/`landed`), the batch state (`landing`), the topics (`runway-land`, `land-conflict-check`, `land-signal`), and the Runway extension (`lander.Lander`) all use it. *Merge* appears only where it names something outside SubmitQueue's vocabulary: the `MERGE` land strategy (which creates a git merge commit), git primitives (`git merge`, `merge-base`, `MERGE_HEAD`), and external systems' own terms (GitHub marks a PR "merged", GitLab has "merge requests"). New code should not introduce merge-named identifiers for domain concepts. ## Branch serialization -The partition key `repo/target` on both inbound topics serializes all VCS operations for a given branch. The message queue delivers messages with the same partition key to the same consumer in order, so at most one merge-conflict check or merge operation is in flight for any given branch at any time. +The partition key `repo/target` on both inbound topics serializes all VCS operations for a given branch. The message queue delivers messages with the same partition key to the same consumer in order, so at most one land-conflict check or land operation is in flight for any given branch at any time. The outbound topics partition by SubmitQueue queue name, matching SubmitQueue's fan-out model where state updates for the same queue are serialized. @@ -27,25 +31,25 @@ The outbound topics partition by SubmitQueue queue name, matching SubmitQueue's │ submitqueue orchestrator │ └──────────┬───────────────────────────┬──────────────┘ │ │ - MergeRequest (dry run) MergeRequest (commit) - │ │ - ▼ ▼ - [merge-conflict-check] [runway-merge] - │ │ - merge-conflict-check ctrl merge ctrl - (read-only) (apply + commit) - │ │ - MergeResult MergeResult - │ │ - ▼ ▼ - [merge-conflict-check-signal] [merge-signal] - │ │ - ▼ ▼ - ┌──────────┬───────────────────────────┬──────────────┐ - │ merge-conflict-check- merge-signal ctrl │ - │ signal ctrl (update batch state, │ - │ (update request fan out to conclude) │ - │ mergeability) │ + LandRequest (dry run) LandRequest (commit) + │ │ + ▼ ▼ + [land-conflict-check] [runway-land] + │ │ + land-conflict-check ctrl land ctrl + (read-only) (apply + commit) + │ │ + LandResult LandResult + │ │ + ▼ ▼ + [land-conflict-check-signal] [land-signal] + │ │ + ▼ ▼ + ┌──────────┬───────────────────────────┬──────────────┐ + │ land-conflict-check- land-signal ctrl │ + │ signal ctrl (update batch state, │ + │ (update request fan out to conclude) │ + │ landability) │ │ submitqueue orchestrator │ └─────────────────────────────────────────────────────┘ ``` @@ -54,37 +58,37 @@ The outbound topics partition by SubmitQueue queue name, matching SubmitQueue's | Controller | In | Out | One-line role | |---|---|---|---| -| **merge-conflict-check** | MergeRequest | MergeResult -> merge-conflict-check-signal | Dry-run merge: check mergeability of ordered steps against the target branch (read-only) | -| **merge** | MergeRequest | MergeResult -> merge-signal | Apply, commit, and report per-step output IDs | +| **land-conflict-check** | LandRequest | LandResult -> land-conflict-check-signal | Dry-run land: check landability of ordered steps against the target branch (read-only) | +| **land** | LandRequest | LandResult -> land-signal | Apply, commit, and report per-step output IDs | -The merge-conflict-check controller always publishes a result — even when all steps are mergeable — so SubmitQueue receives a definitive answer. On infrastructure error it nacks for retry. +The land-conflict-check controller always publishes a result — even when all steps are landable — so SubmitQueue receives a definitive answer. On infrastructure error it nacks for retry. -The merge controller publishes a conflict result (and acks) when the merge detects a conflict; SubmitQueue handles rebatching. On infrastructure error it nacks for retry. On success it publishes per-step outcomes (output IDs of the revisions produced) so SubmitQueue can update its request state. +The land controller publishes a conflict result (and acks) when the land detects a conflict; SubmitQueue handles rebatching. On infrastructure error it nacks for retry. On success it publishes per-step outcomes (output IDs of the revisions produced) so SubmitQueue can update its request state. ## Terminal failures and dead-lettering -Runway is stateless and the sole responder on the client's correlation id: SubmitQueue records the in-flight work before publishing and waits for exactly one `MergeResult` echoing that id. Every request must therefore resolve to a result — success or failure — or the client waits forever. +Runway is stateless and the sole responder on the client's correlation id: SubmitQueue records the in-flight work before publishing and waits for exactly one `LandResult` echoing that id. Every request must therefore resolve to a result — success or failure — or the client waits forever. Failures split into two classes: -- **Named terminal outcomes** — a merge conflict or an invalid request (an unknown/unsupported strategy, a malformed change URI, or an invalid `PROMOTE` composition). These can never succeed on retry, so the controller publishes a `FAILED` `MergeResult` (with a reason) and acks, rather than nacking. The merger surfaces them as the `ErrConflict` / `ErrInvalidRequest` sentinels; `IsTerminal` is the single classification point. +- **Named terminal outcomes** — a land conflict or an invalid request (an unknown/unsupported strategy, a malformed change URI, or an invalid `PROMOTE` composition). These can never succeed on retry, so the controller publishes a `FAILED` `LandResult` (with a reason) and acks, rather than nacking. The lander surfaces them as the `ErrConflict` / `ErrInvalidRequest` sentinels; `IsTerminal` is the single classification point. - **Infrastructure faults** — fetch/network/auth failures, a push rejected for a reason other than a moved tip, and so on. These are nacked for retry. -An infrastructure fault that never recovers would exhaust retries and dead-letter. Because nothing consumed those dead-letter topics, such a request produced no signal and left the client's correlation id unresolved. Runway closes that gap with a **DLQ reconciler**: a dedicated consumer subscribes to the inbound topics' `_dlq` queues and, for each dead-lettered request, republishes a `FAILED` `MergeResult` (echoing the correlation id) to the corresponding signal topic. Unlike the SubmitQueue/Stovepipe DLQ reconcilers it writes no entity state — the signal is the resolution. It runs under an always-retryable error policy so a transient publish failure retries indefinitely rather than dead-lettering again. A payload that cannot even be decoded carries no correlation id and is dropped. +An infrastructure fault that never recovers would exhaust retries and dead-letter. Because nothing consumed those dead-letter topics, such a request produced no signal and left the client's correlation id unresolved. Runway closes that gap with a **DLQ reconciler**: a dedicated consumer subscribes to the inbound topics' `_dlq` queues and, for each dead-lettered request, republishes a `FAILED` `LandResult` (echoing the correlation id) to the corresponding signal topic. Unlike the SubmitQueue/Stovepipe DLQ reconcilers it writes no entity state — the signal is the resolution. It runs under an always-retryable error policy so a transient publish failure retries indefinitely rather than dead-lettering again. A payload that cannot even be decoded carries no correlation id and is dropped. Together these guarantee the client's correlation id always resolves: the primary controllers handle what they can name, and the reconciler is the backstop for everything else. ## Idempotency -Runway has no persistent state — no request store, no job store, no database. Idempotency is achieved through the VCS contract: merge detects already-pushed changes (revisions reachable from HEAD) and treats them as already-landed. Merge-conflict check is read-only and naturally idempotent. +Runway has no persistent state — no request store, no job store, no database. Idempotency is achieved through the VCS contract: land detects already-pushed changes (revisions reachable from HEAD) and treats them as already-landed. Land-conflict check is read-only and naturally idempotent. ## Ownership by service ### Runway -Runway is a single service. It subscribes to two inbound topics (`merge-conflict-check`, `runway-merge`), performs VCS operations through a pluggable extension, and publishes results to two outbound topics (`merge-conflict-check-signal`, `merge-signal`). It owns no persistent data. +Runway is a single service. It subscribes to two inbound topics (`land-conflict-check`, `runway-land`), performs VCS operations through a pluggable extension, and publishes results to two outbound topics (`land-conflict-check-signal`, `land-signal`). It owns no persistent data. ### Shared: the messaging queue -Runway communicates with SubmitQueue only through the messaging queue. The contract is owned by Runway and published under `api/runway/messagequeue/`; both inbound and outbound topic keys live there. SubmitQueue publishes `MergeRequest` messages and consumes the `MergeResult` signals. +Runway communicates with SubmitQueue only through the messaging queue. The contract is owned by Runway and published under `api/runway/messagequeue/`; both inbound and outbound topic keys live there. SubmitQueue publishes `LandRequest` messages and consumes the `LandResult` signals. diff --git a/doc/rfc/sql-queue-rfc.md b/doc/rfc/sql-queue-rfc.md index 34bd5c840..2c743e649 100644 --- a/doc/rfc/sql-queue-rfc.md +++ b/doc/rfc/sql-queue-rfc.md @@ -18,7 +18,7 @@ MySQL-based distributed message queue with immutable message log, per-consumer-g ### Motivation SubmitQueue needs a reliable message queue for coordinating asynchronous workflows: -- **Orchestrator** publishes merge jobs and speculative build requests to workers +- **Orchestrator** publishes land jobs and speculative build requests to workers - **Workers** need distributed coordination without duplicate processing - **Crash recovery** must preserve exactly where processing stopped @@ -223,7 +223,7 @@ See `platform/extension/messagequeue/mysql/schema/queue_subscriber_heartbeats.sq ### Dead Letter Queue -DLQ messages are stored in the same `queue_messages` table under a different topic name (original topic + DLQ suffix, e.g., `merge_queue_dlq`). This allows DLQ messages to be consumed using the normal subscriber with the DLQ topic name. DLQ-specific fields (`failed_at`, `failure_count`, `last_error`, `original_topic`) are populated when a message is moved to DLQ; they are zero/empty for normal messages. +DLQ messages are stored in the same `queue_messages` table under a different topic name (original topic + DLQ suffix, e.g., `land_queue_dlq`). This allows DLQ messages to be consumed using the normal subscriber with the DLQ topic name. DLQ-specific fields (`failed_at`, `failure_count`, `last_error`, `original_topic`) are populated when a message is moved to DLQ; they are zero/empty for normal messages. ## Message Flow @@ -387,7 +387,7 @@ For our use case, we need ordering per repository. With Watermill: With our custom implementation: - Single `queue_messages` table for all topics and partitions -- Rows like `('merge_events', 'repo-123', offset, ...)` provide ordering within partition +- Rows like `('land_events', 'repo-123', offset, ...)` provide ordering within partition - No schema migrations for new repos or topics - Ordering guaranteed within `(topic, partition_key)` @@ -453,7 +453,7 @@ The current design separates the immutable message log from per-consumer-group d **At-Least-Once vs Exactly-Once** - Simpler, better performance - Applications must handle duplicates -- Mitigation: Idempotency keys (e.g., merge request ID) +- Mitigation: Idempotency keys (e.g., land request ID) ## Appendix diff --git a/doc/rfc/stovepipe/steps/build.md b/doc/rfc/stovepipe/steps/build.md index 0501c9900..f630870b2 100644 --- a/doc/rfc/stovepipe/steps/build.md +++ b/doc/rfc/stovepipe/steps/build.md @@ -121,7 +121,7 @@ Both domains have a `build` controller that triggers via a build-runner extensio ### SubmitQueue -SubmitQueue validates **stacks of changes** before merging. Its `build` controller loads `base []entity.Batch` (ordered dependency batches) and `head entity.Batch` and triggers: +SubmitQueue validates **stacks of changes** before landing. Its `build` controller loads `base []entity.Batch` (ordered dependency batches) and `head entity.Batch` and triggers: ```go buildID, err := buildRunner.Trigger(ctx, base, head, metadata) diff --git a/doc/rfc/stovepipe/steps/process.md b/doc/rfc/stovepipe/steps/process.md index e541139d6..8f6962733 100644 --- a/doc/rfc/stovepipe/steps/process.md +++ b/doc/rfc/stovepipe/steps/process.md @@ -73,7 +73,7 @@ A slot is held from admit until the build goes terminal (`process → build → ## Raising `max_concurrent` (speculative validation) -Setting `max_concurrent = N > 1` overlaps validations to start work sooner, and it is **safe** — because Stovepipe validates **already-landed, linear trunk heads**, not pre-merge candidates. Successive commits (`G0 → A → B → C …`) each contain everything below them, so validating `G0..B` already tests A+B together. (A pre-merge queue serializes to catch "two changes green alone, broken combined"; that risk isn't present here.) A green result for head `H` on baseline `B` is an immutable property of `H`, true no matter where last-green moves afterward. +Setting `max_concurrent = N > 1` overlaps validations to start work sooner, and it is **safe** — because Stovepipe validates **already-landed, linear trunk heads**, not pre-land candidates. Successive commits (`G0 → A → B → C …`) each contain everything below them, so validating `G0..B` already tests A+B together. (A pre-land queue serializes to catch "two changes green alone, broken combined"; that risk isn't present here.) A green result for head `H` on baseline `B` is an immutable property of `H`, true no matter where last-green moves afterward. The scheme: each admit pins its baseline to last-green *at admit time*; a green head is adopted even if last-green has since advanced. A late green result is either the newest (adopt) or already behind the pointer (**moot** — dropped, never a regression). diff --git a/doc/rfc/stovepipe/workflow.md b/doc/rfc/stovepipe/workflow.md index bad91d8d8..00da86a97 100644 --- a/doc/rfc/stovepipe/workflow.md +++ b/doc/rfc/stovepipe/workflow.md @@ -1,6 +1,6 @@ # Stovepipe Workflow -Stovepipe answers one question for the rest of the company: **at which commit is this thing green?** It continuously polls a repository branch for its latest commit, validates that commit, works out which projects (if any) are broken at it, records the result, and notifies downstream systems so they can gate deployments on a known-good commit. It is a post-merge service: code lands first, Stovepipe finds out whether it was good. +Stovepipe answers one question for the rest of the company: **at which commit is this thing green?** It continuously polls a repository branch for its latest commit, validates that commit, works out which projects (if any) are broken at it, records the result, and notifies downstream systems so they can gate deployments on a known-good commit. It is a post-land service: code lands first, Stovepipe finds out whether it was good. The pipeline is a queue-driven chain of small, single-purpose controllers, in the same style as SubmitQueue (SQ). Each controller consumes one topic, advances one entity, and publishes to the next topic. Most hops carry only an **ID** and the controller reloads the entity from storage; the entry hop carries the caller's input because there is no row to load yet. The high-level shape is: @@ -23,7 +23,7 @@ Everything Stovepipe records greenness *about* is a URI: a specific commit on a ### Queue — the unit of identity for "what we validate" -Stovepipe reuses SQ's **Queue** concept for the same two reasons SQ does — to **namespace the generated IDs** and to give callers a **stable handle for the repo+ref being validated** — plus a third that is specific to a post-merge validator: a Queue **owns the last-known-good URI** and the greenness history for its branch. +Stovepipe reuses SQ's **Queue** concept for the same two reasons SQ does — to **namespace the generated IDs** and to give callers a **stable handle for the repo+ref being validated** — plus a third that is specific to a post-land validator: a Queue **owns the last-known-good URI** and the greenness history for its branch. A Queue is named by a **stable logical string** (e.g. `monorepo/main`), and that name is what the ingest API takes — *not* a raw URI. SourceControl/config resolves the Queue name to a concrete VCS URI base. This keeps callers (and the external poller) free of VCS detail: they say "the `monorepo/main` Queue has moved", and Stovepipe resolves what that means. diff --git a/doc/rfc/submitqueue/extension-contract.md b/doc/rfc/submitqueue/extension-contract.md index 80c241ff2..3ca9bd0cd 100644 --- a/doc/rfc/submitqueue/extension-contract.md +++ b/doc/rfc/submitqueue/extension-contract.md @@ -25,9 +25,9 @@ Both unblock with the shape `conflict` already uses: accept identity, resolve in | `dependency` | `entity.Batch` + active `[]entity.Batch` | **nothing** — the batch it analyzes is already persisted, with `Contains` set to `[requestID]` | `entity.Batch`, `[]entity.Batch` → `conflict` | | `score` | `entity.Batch`, then each `entity.Request` | batch → requests | `request.Change` per request, then multiplies the scores → `scorer` | | `build` | `entity.Batch`, then `collectChanges` | batch → requests → changes, **flattening batch boundaries** | base `[]Change`, head `[]Change` → `buildrunner` | -| `merge` | `entity.Batch`, then `collectChanges` | batch → requests → changes | `[]Change` → `pusher` | +| `land` | `entity.Batch`, then `collectChanges` | batch → requests → changes | `[]Change` → `pusher` | -Two facts this grounds: `conflict` already resolves nothing (the baseline), and the batch→changes walk is **already duplicated** in `build`/`merge` `collectChanges` — the shared resolver below only consolidates it. +Two facts this grounds: `conflict` already resolves nothing (the baseline), and the batch→changes walk is **already duplicated** in `build`/`land` `collectChanges` — the shared resolver below only consolidates it. ## Verdict @@ -37,18 +37,18 @@ Two facts this grounds: `conflict` already resolves nothing (the baseline), and | `scorer.Scorer` | score | flat `Change`, per request | `entity.Batch` — resolve + reduce internally | one batch score (`float64`) — unchanged | request store + change provider | | `changeprovider.ChangeProvider` | validate | `Change` | `entity.Request` | per-URI change info (`[]ChangeInfo`, `URI`-tagged) — unchanged | none — it *is* the resolver | | `buildrunner.BuildRunner` | build | base/head `[]Change` | base `[]entity.Batch` + head `entity.Batch` | build id, then status/cancel (`BuildID`, `BuildStatus`) — unchanged | request store + change provider | -| `pusher.Pusher` *(removed)* | merge | — | **moved out-of-process to runway** (`merge` / `merge-signal`); see the note below the table | — | — | +| `pusher.Pusher` *(removed)* | land | — | **moved out-of-process to runway** (`land` / `land-signal`); see the note below the table | — | — | | `storage`, `changestore`, `queueconfig` | — | keys + entities | unchanged — resolution targets | entities | — | -**Outputs are unchanged.** This RFC moves the *input* toward identity; the four live return contracts — conflicts, score, change info, build id/status — are exactly what they are today. (The `pusher` row is not an in-process extension: merge runs out-of-process in runway, so its output is not part of this catalog — see the note below.) No other output shape changes. +**Outputs are unchanged.** This RFC moves the *input* toward identity; the four live return contracts — conflicts, score, change info, build id/status — are exactly what they are today. (The `pusher` row is not an in-process extension: land runs out-of-process in runway, so its output is not part of this catalog — see the note below.) No other output shape changes. -The validate-time mergeability **check** and the **merge** itself both run **asynchronously and out-of-process** in runway rather than as in-process extensions, over the one shared `MergeRequest`/`MergeResult` contract — a check is a dry run of a merge. `validate` hands off directly to runway (→ `merge-conflict-check`, result back via `mergeconflictsignal`); `merge` hands the batch to runway (→ `merge`, result back via `mergesignal`) rather than calling an in-process `pusher`. See [workflow.md](workflow.md). The in-process `mergechecker` and `pusher` packages are unused on the pipeline path. +The validate-time landability **check** and the **land** itself both run **asynchronously and out-of-process** in runway rather than as in-process extensions, over the one shared `LandRequest`/`LandResult` contract — a check is a dry run of a land. `validate` hands off directly to runway (→ `land-conflict-check`, result back via `landconflictsignal`); `land` hands the batch to runway (→ `land`, result back via `landsignal`) rather than calling an in-process `pusher`. See [workflow.md](workflow.md). The in-process `landchecker` and `pusher` packages are unused on the pipeline path. Non-obvious points: - **scorer** — owning the batch moves batch-level reduction (today the controller's multiplicative product) into the scorer, where the `composite` reduce step already lives. - **buildrunner** — this **revises** [build-runner.md](build-runner.md), which deliberately kept batches out of the boundary. The base/head split survives, expressed as batches; the provider still operates on changes (the shared resolver produces them inside the extension). Cost: a `buildrunner` / `pusher` implementation now depends on a request store + change provider. -- **pusher** — a *list* of batches (not one) designs for a merge-train: land several ready batches, or a batch with not-yet-landed deps, in one atomic push. Today merge pushes a single batch because deps are already on trunk. Since the input is now a list, the output groups outcomes per batch (`BatchID`-tagged, with per-change commit detail kept underneath) instead of one flat per-change list — the only output shape this RFC changes. Push atomicity is unchanged (all-or-nothing across the whole call), so a per-batch *status* is intentionally omitted: a partial-landing train would be a separate, larger change to the atomicity contract. +- **pusher** — a *list* of batches (not one) designs for a land-train: land several ready batches, or a batch with not-yet-landed deps, in one atomic push. Today land pushes a single batch because deps are already on trunk. Since the input is now a list, the output groups outcomes per batch (`BatchID`-tagged, with per-change commit detail kept underneath) instead of one flat per-change list — the only output shape this RFC changes. Push atomicity is unchanged (all-or-nothing across the whole call), so a per-batch *status* is intentionally omitted: a partial-landing train would be a separate, larger change to the atomicity contract. ## Mechanism @@ -60,5 +60,5 @@ Dependencies are injected per-extension at the existing `Factory.For` (wiring: ` - **Status quo (controller resolves).** Keeps extensions pure and trivially testable, but thickens controllers and caps every extension at what the controller chose to pre-compute — the two blocked features are that ceiling. - **Literal string IDs.** An extra read per call when the controller already holds the entity; pass thin reference entities instead. -- **Per-implementation batch→changes resolution.** How the `build`/`merge` duplication arose; one shared resolver instead. +- **Per-implementation batch→changes resolution.** How the `build`/`land` duplication arose; one shared resolver instead. - *Acknowledged:* decision extensions gain dependencies and are no longer pure functions — mitigated by their existing mock packages and `Factory` injection. diff --git a/doc/rfc/submitqueue/modular-queue-wiring.md b/doc/rfc/submitqueue/modular-queue-wiring.md index 1acdddb89..a3e716e10 100644 --- a/doc/rfc/submitqueue/modular-queue-wiring.md +++ b/doc/rfc/submitqueue/modular-queue-wiring.md @@ -281,12 +281,12 @@ row appears on topic "start", partition key "monorepo/exp" gateway orchestrator stovepipe runway ──────────────────────────────────────────────────────────────────────────────────────────── Deps seams counter · storage · changeprovider · storage · counter · storage · - queueconfig.Store · buildrunner · scorer sourcecontrol. merger Factory + queueconfig.Store · buildrunner · scorer sourcecontrol. lander Factory requestlog store analyzer · validator Factory · (+7 speculation) queueconfig.Store - Stages log start · cancel · process mergeconflictcheck · - (rows) validate · batch · merge + Stages log start · cancel · process landconflictcheck · + (rows) validate · batch · land … (+ DLQ column) Controllers Gateway Orchestrator Stovepipe Runway diff --git a/doc/rfc/submitqueue/speculation-generator-best-first.md b/doc/rfc/submitqueue/speculation-generator-best-first.md index 458e5e007..5558109fc 100644 --- a/doc/rfc/submitqueue/speculation-generator-best-first.md +++ b/doc/rfc/submitqueue/speculation-generator-best-first.md @@ -447,7 +447,7 @@ The ordering stays the same. `CandidatePath.RankingScore` contains this logarith - `Succeeded` fixes an assumption to succeeds. - `Failed` or `Cancelled` fixes an assumption to fails. - `Cancelling` remains undecided because cancellation may lose a race with completion. -- `Merging` also remains undecided, because a merge can fail. It is tempting to treat it as committed to landing and skip the scorer call, but that puts a state-specific policy inside the search: whether a path betting against a merging batch is worth funding is a question of price, and price belongs to the scorer. The allocator draws the same line — "no batch state enters this decision" — and the generator holds it too. Nothing is lost by staying open: a single passed path still waits for the merge result, while passed paths covering every outcome let the controller bypass the dependency (see [speculation.md](speculation.md)). Funding the unlikely side spends budget, which is the allocator's to ration. +- `Landing` also remains undecided, because a land can fail. It is tempting to treat it as committed to landing and skip the scorer call, but that puts a state-specific policy inside the search: whether a path betting against a landing batch is worth funding is a question of price, and price belongs to the scorer. The allocator draws the same line — "no batch state enters this decision" — and the generator holds it too. Nothing is lost by staying open: a single passed path still waits for the land result, while passed paths covering every outcome let the controller bypass the dependency (see [speculation.md](speculation.md)). Funding the unlikely side spends budget, which is the allocator's to ration. - A fixed assumption stays in the returned path but contributes probability 1 and has no flip. - A shared dependency is scored once per run. diff --git a/doc/rfc/submitqueue/speculation.md b/doc/rfc/submitqueue/speculation.md index 1887cff7e..526bfa9c3 100644 --- a/doc/rfc/submitqueue/speculation.md +++ b/doc/rfc/submitqueue/speculation.md @@ -1,20 +1,20 @@ # Speculation -A merge queue that verifies one change at a time is limited by its slowest build. Speculation removes that limit: it builds a batch early, against an assumption about how the conflicting batches ahead of it will resolve, so a valid build is usually ready by the time they do. +A land queue that verifies one change at a time is limited by its slowest build. Speculation removes that limit: it builds a batch early, against an assumption about how the conflicting batches ahead of it will resolve, so a valid build is usually ready by the time they do. -Work enters SubmitQueue as **batches** — changes verified and merged together. Two batches **conflict** when they touch the same code, which makes the earlier one a **dependency** of the later. A **path** is one set of assumptions about how a batch's dependencies resolve, and the batch it builds is the path's **head**. +Work enters SubmitQueue as **batches** — changes verified and landed together. Two batches **conflict** when they touch the same code, which makes the earlier one a **dependency** of the later. A **path** is one set of assumptions about how a batch's dependencies resolve, and the batch it builds is the path's **head**. -On every queue update the **speculate controller** reruns from scratch: it reads the current state, applies the incoming signals, asks a pluggable **Speculator** which paths are worth building within the CI budget, and persists only those. Everything else is recomputed next time, never stored. A batch normally merges after its dependencies resolve and a matching build has passed; complete passed coverage of every unresolved outcome lets it bypass those dependencies. +On every queue update the **speculate controller** reruns from scratch: it reads the current state, applies the incoming signals, asks a pluggable **Speculator** which paths are worth building within the CI budget, and persists only those. Everything else is recomputed next time, never stored. A batch normally lands after its dependencies resolve and a matching build has passed; complete passed coverage of every unresolved outcome lets it bypass those dependencies. ## The speculation run -The speculate controller runs whenever the queue changes — after a new batch, a completed build, a merge result, or a cancel. Each publishes a **dirty signal** carrying the changed batch ID, partitioned by the queue so a queue's runs happen one at a time. The dirty signal is an internal queue contract — payload in `submitqueue/core/messagequeue`, topic key in `submitqueue/core/topickey`. +The speculate controller runs whenever the queue changes — after a new batch, a completed build, a land result, or a cancel. Each publishes a **dirty signal** carrying the changed batch ID, partitioned by the queue so a queue's runs happen one at a time. The dirty signal is an internal queue contract — payload in `submitqueue/core/messagequeue`, topic key in `submitqueue/core/topickey`. ``` dirty(queue) — "trigger a run" — published after: - a new batch - a build completes (success/failure/cancellation) - - a merge result arrives (success/failure) + - a land result arrives (success/failure) - a cancel │ │ carries the changed batch ID, partitioned by queue, so a @@ -31,15 +31,15 @@ The speculate controller runs whenever the queue changes — after a new batch, │ → paths to build, paths to preempt │ 4 check validate that output: drop actions it shouldn't propose │ (non-Speculating head, refuted, incoherent, terminal path) - │ 5 write record each head's decisions; send build / cancel / merge messages + │ 5 write record each head's decisions; send build / cancel / land messages │ (a head whose write loses is re-planned on the next run) │ ├─▶ build / cancel (path ID, attempt) → build (orchestrator/controller/build): │ reserve → BuildRunner.Trigger(base, head) → record build ID, mark path building │ CI runs → buildsignal marks path passed/failed/cancelled ─▶ dirty(queue) │ - └─▶ merge (batch) → Runway performs the merge - → mergesignal marks the batch succeeded/failed ─▶ dirty(queue) + └─▶ land (batch) → Runway performs the land + → landsignal marks the batch succeeded/failed ─▶ dirty(queue) ``` ### State reconciliation @@ -54,8 +54,8 @@ Every write is a compare-and-swap: a writer that loses re-reads on a later run. Verdicts are controller-owned facts: the Speculator can neither compute nor veto them. -- **Merge.** Each path carries an assumption about every dependency — *succeeds* (built on top of) or *fails* (built without). Normally, once a path's build has passed and every dependency has finished the way the path assumed — one assumed *succeeds* has merged, one assumed *fails* has failed or been cancelled — the speculate controller moves the head to Merging and hands it to Runway. A dependency that is merely *merging* has not finished, because a merge can fail, so a single matching path still waits for the answer. Complete passed coverage is the exception described in Bypass large diff: it lets a head merge before those answers arrive. If the hand-off is lost, the next run re-sends it. The same run sets the head's remaining in-flight paths *cancelling*: once the head can merge they cannot help, and they hold CI slots until they stop. The mergesignal controller records Runway's terminal result: success marks the head Succeeded, while failure marks it Failed. The result publishes a single dirty signal — no per-dependent fan-out — and the next run refutes paths whose assumption disagrees with the result: *fails* assumptions after success, *succeeds* assumptions after failure. The hand-off is idempotent, so Runway reports success without another merge when the change is already present. A chain ordinarily merges one at a time, but a fully covered head can bypass its unsettled predecessors. -- **Failure (no viable path).** A batch fails when every possible future has a failed build — no path can pass, so it can never merge. +- **Land.** Each path carries an assumption about every dependency — *succeeds* (built on top of) or *fails* (built without). Normally, once a path's build has passed and every dependency has finished the way the path assumed — one assumed *succeeds* has landed, one assumed *fails* has failed or been cancelled — the speculate controller moves the head to Landing and hands it to Runway. A dependency that is merely *landing* has not finished, because a land can fail, so a single matching path still waits for the answer. Complete passed coverage is the exception described in Bypass large diff: it lets a head land before those answers arrive. If the hand-off is lost, the next run re-sends it. The same run sets the head's remaining in-flight paths *cancelling*: once the head can land they cannot help, and they hold CI slots until they stop. The landsignal controller records Runway's terminal result: success marks the head Succeeded, while failure marks it Failed. The result publishes a single dirty signal — no per-dependent fan-out — and the next run refutes paths whose assumption disagrees with the result: *fails* assumptions after success, *succeeds* assumptions after failure. The hand-off is idempotent, so Runway reports success without another land when the change is already present. A chain ordinarily lands one at a time, but a fully covered head can bypass its unsettled predecessors. +- **Failure (no viable path).** A batch fails when every possible future has a failed build — no path can pass, so it can never land. - **Cancel.** A cancelled batch is driven terminal: its in-flight paths are set *cancelling*, then the batch is marked Cancelled once they stop (see Cancellation). ### Conflict relaxation @@ -64,23 +64,23 @@ Conflict analysis is conservative — it flags any *possible* conflict — so he **Not implemented.** An earlier design expressed it per path, with a third assumption value — *ignored* — meaning "this path makes no claim about this dependency". Nothing ever produced one, and the value has been removed rather than left as vocabulary the system could not create. -When relaxation is built, it belongs in the **controller**, as a trim of the dependency list before the snapshot is handed over: the Speculator then sees a head whose dependencies are exactly the ones that count, and a path stays a total function over them — one assumption per dependency, each *succeeds* or *fails*, with no third state to reason about. That keeps the decision where the other correctness decisions live, since dropping a dependency is a judgement about what may land untested, not about which candidate is most promising. It also keeps every consumer honest by construction: a merge gate, a refutation check, or a generator cannot forget to special-case a value that does not exist. +When relaxation is built, it belongs in the **controller**, as a trim of the dependency list before the snapshot is handed over: the Speculator then sees a head whose dependencies are exactly the ones that count, and a path stays a total function over them — one assumption per dependency, each *succeeds* or *fails*, with no third state to reason about. That keeps the decision where the other correctness decisions live, since dropping a dependency is a judgement about what may land untested, not about which candidate is most promising. It also keeps every consumer honest by construction: a land gate, a refutation check, or a generator cannot forget to special-case a value that does not exist. The open question that design has to answer is what a stored path means once the trim changes between runs — a path built against a trimmed list no longer lines up with a head whose list has grown back, and `isWellFormed` rejects it. The per-path marker made that case self-describing; a trim does not, so the trim has to be either stable for a head's lifetime or recorded alongside the path. -Example of the payoff either way: `H` conflicts with `B1` and weak `B2`. Relax `B2`, and `H` merges once `B1` merges and its build passes — even if `B2` later merges. Without it, `H` waits on both. +Example of the payoff either way: `H` conflicts with `B1` and weak `B2`. Relax `B2`, and `H` lands once `B1` lands and its build passes — even if `B2` later lands. Without it, `H` waits on both. ### Bypass large diff -If a batch's passed builds cover *every* way its dependencies could resolve, the outcome is the same either way — so it can merge now, ahead of them. Classic case: a small change stuck behind a slow one is built both with and without it; both pass, and it merges immediately. +If a batch's passed builds cover *every* way its dependencies could resolve, the outcome is the same either way — so it can land now, ahead of them. Classic case: a small change stuck behind a slow one is built both with and without it; both pass, and it lands immediately. The controller checks coverage over only the dependencies that have not settled yet. Settled dependencies pin each surviving path to the outcome that actually happened; for every combination of the remaining dependencies, the path set must contain a passed, unbroken path with that combination of assumptions. If any combination is missing, unbuilt, failed, or contradicted by a settled dependency, the head waits normally. The check only observes paths the Speculator already funded — it does not fund the exponential path space itself or alter the queue's build budget. -Coverage makes the bypass sound because whichever way the dependencies later resolve, a passed build already validated the resulting set of changes. The build order and merge order differ: a path assuming dependency `D` succeeds validates `D` then head `H`, while bypass lands `H` before `D`. SubmitQueue treats those orders as content-equivalent. Runway still performs the real merge, so if the reordered changes conflict textually, the older dependency can fail after the newer head has bypassed it; this is an accepted cost of landing the fully covered head early rather than a licence to put unmergeable content on the target. +Coverage makes the bypass sound because whichever way the dependencies later resolve, a passed build already validated the resulting set of changes. The build order and land order differ: a path assuming dependency `D` succeeds validates `D` then head `H`, while bypass lands `H` before `D`. SubmitQueue treats those orders as content-equivalent. Runway still performs the real land, so if the reordered changes conflict textually, the older dependency can fail after the newer head has bypassed it; this is an accepted cost of landing the fully covered head early rather than a licence to put unlandable content on the target. ### Cancellation -Cancellation is best-effort: a batch marked *cancelling* may still merge if a merge wins the race, so terminal states prevail. A cancel sets the intent; a later run drives it terminal. +Cancellation is best-effort: a batch marked *cancelling* may still land if a land wins the race, so terminal states prevail. A cancel sets the intent; a later run drives it terminal. Two kinds of cancel, split by owner: @@ -93,7 +93,7 @@ Cancelling a path sends a cancel (path ID, attempt) to the build controller, whi ## Speculator Extension -The one extension. It decides *which paths to build and which running ones to cancel* — nothing else; the controller handles the rest (reconciling facts, cancelling ruled-out paths, verdicts, checking output). A swapped-in Speculator changes which paths run, never whether a batch merges or fails. +The one extension. It decides *which paths to build and which running ones to cancel* — nothing else; the controller handles the rest (reconciling facts, cancelling ruled-out paths, verdicts, checking output). A swapped-in Speculator changes which paths run, never whether a batch lands or fails. **The contract** is `Speculate(batches, pathSets) → []Speculation`: @@ -116,6 +116,6 @@ Signatures live in code and are not copied here, so they cannot drift. This sect **Entities** — [`submitqueue/entity/speculation.go`](../../../submitqueue/entity/speculation.go). A `SpeculationPath` is a head batch plus one `PathDependency` per dependency in queue order, each carrying a `DependencyAssumption`: *succeeds* or *fails*. A `SpeculationPathEntry` is the stored record of one chosen path, keyed by a hash of its content, plus its status and attempt number; it holds no build reference — the execution record has that, keyed by (path ID, attempt) — and no ranking score, which means nothing outside the run that produced it. A `SpeculationPathSet` is one head's chosen paths, live and recently finished, under a single version for compare-and-swap. Every logical path is self-describing, but a store may encode the common head and ordered dependency IDs once per set and keep each path's assumptions positionally — one bit per dependency. -**Speculator** — [`submitqueue/extension/speculation/speculator`](../../../submitqueue/extension/speculation/speculator/README.md). `Speculate` takes one queue snapshot (the batches and their path sets) and returns the build and cancel actions it proposes; a path it wants left alone has no entry. Actions must target Speculating heads. Verdicts stay controller-owned, so there is no merge or fail action. +**Speculator** — [`submitqueue/extension/speculation/speculator`](../../../submitqueue/extension/speculation/speculator/README.md). `Speculate` takes one queue snapshot (the batches and their path sets) and returns the build and cancel actions it proposes; a path it wants left alone has no entry. Actions must target Speculating heads. Verdicts stay controller-owned, so there is no land or fail action. **Generator and Allocator** — [`generator`](../../../submitqueue/extension/speculation/generator/README.md) and [`allocator`](../../../submitqueue/extension/speculation/allocator/README.md), the two composition points inside the default Speculator. The Generator opens a pull-based stream of candidate paths over the batches; the Allocator spends the build budget over that stream, reconciling it against the path sets. Both abort on a cancelled context. diff --git a/doc/rfc/submitqueue/workflow.md b/doc/rfc/submitqueue/workflow.md index b50e3958c..7952815d8 100644 --- a/doc/rfc/submitqueue/workflow.md +++ b/doc/rfc/submitqueue/workflow.md @@ -1,8 +1,8 @@ # Orchestrator Workflow -The orchestrator processes land requests through a queue-driven pipeline of small, single-purpose controllers. The gateway accepts a request over RPC and hands it off asynchronously; from there each controller consumes one topic, advances the request or batch, and publishes to the next topic. Most hops carry only an ID — the controller fetches the entity from storage — while a few entry points (`start`, `buildsignal`, `log`) carry the full payload because there is no row to fetch yet. Some stages cross a service boundary: they publish a full payload to the other service's queue and consume a full payload back, because neither service can read the other's storage. (The `validate` and `merge` stages both hand work to runway — a merge-conflict check and the merge itself — and consume its result on `mergeconflictsignal` / `mergesignal`.) See the queue-payload-boundary rule in [AGENTS.md](../../../AGENTS.md). +The orchestrator processes land requests through a queue-driven pipeline of small, single-purpose controllers. The gateway accepts a request over RPC and hands it off asynchronously; from there each controller consumes one topic, advances the request or batch, and publishes to the next topic. Most hops carry only an ID — the controller fetches the entity from storage — while a few entry points (`start`, `buildsignal`, `log`) carry the full payload because there is no row to fetch yet. Some stages cross a service boundary: they publish a full payload to the other service's queue and consume a full payload back, because neither service can read the other's storage. (The `validate` and `land` stages both hand work to runway — a land-conflict check and the land itself — and consume its result on `landconflictsignal` / `landsignal`.) See the queue-payload-boundary rule in [AGENTS.md](../../../AGENTS.md). -The pipeline has two cycles: `speculate → build → buildsignal → speculate` (CI feedback loop) and `merge → runway → mergesignal → speculate` (land the batch out of process, then advance the next). `conclude` is the only stage that transitions a request to a terminal state; `log` is an append-only sink that any controller can publish to via `submitqueue/core/request.PublishLog`. +The pipeline has two cycles: `speculate → build → buildsignal → speculate` (CI feedback loop) and `land → runway → landsignal → speculate` (land the batch out of process, then advance the next). `conclude` is the only stage that transitions a request to a terminal state; `log` is an append-only sink that any controller can publish to via `submitqueue/core/request.PublishLog`. ## Diagram @@ -24,18 +24,18 @@ The pipeline has two cycles: `speculate → build → buildsignal → speculate` | | Dedup, fetch metadata, publish | | | check request to runway | | +----------------+-----------------+ - | MergeRequest - | v - | #################################### - | # runway (separate service) # - | # Dry-run merge, emit result # - | ####################+############### - | MergeResult - | v - | +----------------------------------+ - | | mergeconflictsignal | - | | Correlate result, gate request | - | +----------------+-----------------+ + | LandRequest + | v + | #################################### + | # runway (separate service) # + | # Dry-run land, emit result # + | ####################+############### + | LandResult + | v + | +----------------------------------+ + | | landconflictsignal | + | | Correlate result, gate request | + | +----------------+-----------------+ | | RequestID | v | +----------------------------------+ @@ -50,20 +50,20 @@ The pipeline has two cycles: `speculate → build → buildsignal → speculate` | | +------+-----------------+---------+ | | | BatchID | | BatchID | | | v v | - | | +------------------+ +------------------+ | - | | | build | | merge | | - | | | Trigger CI build | | Publish to runway| | - | | +--------+---------+ +--------+---------+ | - | | Build | MergeRequest | - | | v v | - | | +------------------+ #################### | - | +--| buildsignal | # runway (sep.) # | - | BatchID | Feed CI result | # Merge, emit res. # | - | | back to spec. | ########+########### | - | +------------------+ MergeResult | - | ^ v | - | Build (ext.CI) | +------------------+ | - | | | mergesignal |--+ + | | +------------------+ +------------------+ | + | | | build | | land | | + | | | Trigger CI build | | Publish to runway| | + | | +--------+---------+ +--------+---------+ | + | | Build | LandRequest | + | | v v | + | | +------------------+ #################### | + | +--| buildsignal | # runway (sep.) # | + | BatchID | Feed CI result | # Land, emit res. # | + | | back to spec. | ########+########### | + | +------------------+ LandResult | + | ^ v | + | Build (ext.CI) | +------------------+ | + | | | landsignal |--+ | | | Gate batch + fan | | | | +--------+---------+ | | | | BatchID | @@ -82,14 +82,14 @@ The pipeline has two cycles: `speculate → build → buildsignal → speculate` |---|---|---|---| | **gateway/Land** | RPC | start | Accept request, mint ID, log Accepted, hand off async | | **start** | LandRequest | validate, log | Persist Request and emit Started log | -| **validate** | RequestID | merge-conflict-check (runway) | Dedup, fetch change metadata, claim changes, then publish the full check request to runway (keyed by the request id, the correlation id) | -| **mergeconflictsignal** | MergeResult | batch | Correlate runway's result; advance if mergeable, fail if conflicted | +| **validate** | RequestID | land-conflict-check (runway) | Dedup, fetch change metadata, claim changes, then publish the full check request to runway (keyed by the request id, the correlation id) | +| **landconflictsignal** | LandResult | batch | Correlate runway's result; advance if landable, fail if conflicted | | **batch** | RequestID | speculate | Group request into a Batch with dependencies | -| **speculate** | BatchID | build, merge | (stub) Decide whether to verify via CI or land | +| **speculate** | BatchID | build, land | (stub) Decide whether to verify via CI or land | | **build** | BatchID | buildsignal | Trigger CI build for the batch | | **buildsignal** | Build | speculate | Feed CI result back into speculation | -| **merge** | BatchID | merge (runway) | Build the full merge request from the batch's member requests and publish to runway, keyed by the batch id (the correlation id) | -| **mergesignal** | MergeResult | conclude, speculate | Correlate runway's result; mark the batch Succeeded/Failed and fan out | +| **land** | BatchID | land (runway) | Build the full land request from the batch's member requests and publish to runway, keyed by the batch id (the correlation id) | +| **landsignal** | LandResult | conclude, speculate | Correlate runway's result; mark the batch Succeeded/Failed and fan out | | **conclude** | BatchID | — | Map terminal batch state to request state | | **log** | RequestLog | — | Gateway-owned sink: persists request log events to storage | @@ -97,7 +97,7 @@ The pipeline has two cycles: `speculate → build → buildsignal → speculate` Every *consumed* primary pipeline topic above is paired with a `{topic}_dlq` subscription consumed by a dedicated DLQ controller. The `log` topic is the exception: the orchestrator only publishes to it (the gateway is the sole consumer that persists the request log), so it has no orchestrator-side subscription and therefore no DLQ. The consumer framework moves a message to its DLQ once the primary controller returns a non-retryable error or exhausts retries on a retryable one; without the DLQ side the affected request would stay in a non-terminal state forever and the gateway would still report it as "in progress". -The DLQ controllers do not re-attempt the failed work. They decode the payload to recover the affected request (`RequestID`) or batch (`BatchID`) and drive the entity to a terminal failed state — `RequestStateError` for requests, `BatchStateFailed` for batches, with fan-out to the member requests. A DLQ whose topic carries a full payload rather than a bare ID recovers the id from that payload instead — the `mergeconflictsignal` and `mergesignal` DLQs read it from the runway `MergeResult` the producer echoed back. State writes use the same optimistic-locking CAS as the primary pipeline, so a late primary-pipeline update wins cleanly and a version mismatch is asked back for redelivery. +The DLQ controllers do not re-attempt the failed work. They decode the payload to recover the affected request (`RequestID`) or batch (`BatchID`) and drive the entity to a terminal failed state — `RequestStateError` for requests, `BatchStateFailed` for batches, with fan-out to the member requests. A DLQ whose topic carries a full payload rather than a bare ID recovers the id from that payload instead — the `landconflictsignal` and `landsignal` DLQs read it from the runway `LandResult` the producer echoed back. State writes use the same optimistic-locking CAS as the primary pipeline, so a late primary-pipeline update wins cleanly and a version mismatch is asked back for redelivery. DLQ consumers are wired with `errs.AlwaysRetryableProcessor` and a very high `Retry.MaxAttempts`, with their own DLQ disabled. That combination makes reconciliation effectively non-droppable: any failure is forced retryable rather than escalating to a second-level dead-letter that nobody consumes. The trade-off is that a genuinely unprocessable DLQ message — typically a malformed payload — must be removed by an operator. diff --git a/platform/base/mergestrategy/BUILD.bazel b/platform/base/landstrategy/BUILD.bazel similarity index 55% rename from platform/base/mergestrategy/BUILD.bazel rename to platform/base/landstrategy/BUILD.bazel index 40523b2ef..d64690f50 100644 --- a/platform/base/mergestrategy/BUILD.bazel +++ b/platform/base/landstrategy/BUILD.bazel @@ -2,7 +2,7 @@ load("@rules_go//go:def.bzl", "go_library") go_library( name = "go_default_library", - srcs = ["mergestrategy.go"], - importpath = "github.com/uber/submitqueue/platform/base/mergestrategy", + srcs = ["landstrategy.go"], + importpath = "github.com/uber/submitqueue/platform/base/landstrategy", visibility = ["//visibility:public"], ) diff --git a/platform/base/landstrategy/landstrategy.go b/platform/base/landstrategy/landstrategy.go new file mode 100644 index 000000000..29fcc015b --- /dev/null +++ b/platform/base/landstrategy/landstrategy.go @@ -0,0 +1,35 @@ +// 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 landstrategy holds the shared source-control integration strategy +// used across SubmitQueue, runway, and other repo-local domains. It names how a +// change is integrated into the target branch (rebase, squash-rebase, merge, +// promote). +package landstrategy + +// Strategy defines the possible source control integration methods. +type Strategy string + +const ( + // StrategyUnknown is the unknown strategy. It is set by default when the structure is initialized. It should never be seen in the system and is used for error control. + StrategyUnknown Strategy = "" + // StrategyRebase rebases commits onto the target branch before landing. + StrategyRebase Strategy = "rebase" + // StrategySquashRebase squashes commits into a single commit before rebasing on top of the target branch. + StrategySquashRebase Strategy = "squash_rebase" + // StrategyMerge merges commits into the target branch by creating a separate merge commit, preserving the commit history along with hashes. + StrategyMerge Strategy = "merge" + // StrategyPromote integrates the exact revision as-is, with no content transform — it advances the target branch to an already-existing commit rather than producing new revisions. The implementer maps it to its backend (git fast-forward, Mercurial bookmark advance, Subversion/Perforce copy). Used to promote an already-landed/verified commit onto another branch. + StrategyPromote Strategy = "promote" +) diff --git a/platform/base/mergestrategy/mergestrategy.go b/platform/base/mergestrategy/mergestrategy.go deleted file mode 100644 index aa117ce5c..000000000 --- a/platform/base/mergestrategy/mergestrategy.go +++ /dev/null @@ -1,35 +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 mergestrategy holds the shared source-control integration strategy -// used across SubmitQueue, runway, and other repo-local domains. It names how a -// change is integrated into the target branch (rebase, squash-rebase, merge, -// promote). -package mergestrategy - -// MergeStrategy defines the possible source control integration methods. -type MergeStrategy string - -const ( - // MergeStrategyUnknown is the unknown strategy. It is set by default when the structure is initialized. It should never be seen in the system and is used for error control. - MergeStrategyUnknown MergeStrategy = "" - // MergeStrategyRebase rebases commits onto the target branch before landing. - MergeStrategyRebase MergeStrategy = "rebase" - // MergeStrategySquashRebase squashes commits into a single commit before rebasing on top of the target branch. - MergeStrategySquashRebase MergeStrategy = "squash_rebase" - // MergeStrategyMerge merges commits into the target branch by creating a separate merge commit, preserving the commit history along with hashes. - MergeStrategyMerge MergeStrategy = "merge" - // MergeStrategyPromote integrates the exact revision as-is, with no content transform — it advances the target branch to an already-existing commit rather than producing new revisions. The implementer maps it to its backend (git fast-forward, Mercurial bookmark advance, Subversion/Perforce copy). Used to promote an already-landed/verified commit onto another branch. - MergeStrategyPromote MergeStrategy = "promote" -) diff --git a/platform/errs/README.md b/platform/errs/README.md index b86c3b104..c17569d10 100644 --- a/platform/errs/README.md +++ b/platform/errs/README.md @@ -178,7 +178,7 @@ In particular, **do not reach for `NewRetryableError` just because replaying the ## Extensions Return Go Errors -Extension interfaces (`MergeChecker`, `Storage`, `Publisher`) return `error` values and may define domain-specific sentinels. Most sentinels remain unclassified because their meaning depends on the call site; for example, `storage.ErrNotFound` might be a user error in one controller and an infrastructure error in another. A sentinel whose classification is intrinsic in every context may carry that classification at its declaration. `storage.ErrVersionMismatch`, for example, is always a retryable infrastructure error because it reports a lost optimistic-concurrency race. +Extension interfaces (`LandChecker`, `Storage`, `Publisher`) return `error` values and may define domain-specific sentinels. Most sentinels remain unclassified because their meaning depends on the call site; for example, `storage.ErrNotFound` might be a user error in one controller and an infrastructure error in another. A sentinel whose classification is intrinsic in every context may carry that classification at its declaration. `storage.ErrVersionMismatch`, for example, is always a retryable infrastructure error because it reports a lost optimistic-concurrency race. Controllers should return intrinsically classified sentinels without adding another framework wrapper. The declaration remains reusable across implementations while every caller observes the same classification. diff --git a/platform/extension/consumergate/file/store_test.go b/platform/extension/consumergate/file/store_test.go index 1ab0abf07..bf5f5b983 100644 --- a/platform/extension/consumergate/file/store_test.go +++ b/platform/extension/consumergate/file/store_test.go @@ -72,7 +72,7 @@ func TestIsGated(t *testing.T) { { name: "gate on one group leaves other groups open", close: []consumergate.Key{{ConsumerGroup: "orchestrator-batch"}}, - group: "runway-merge", + group: "runway-land", partition: "queue-a", want: false, }, @@ -128,8 +128,8 @@ func TestParkedRecordLifecycle(t *testing.T) { store := New(t.TempDir()) parked := consumergate.Parked{ - ConsumerGroup: "runway-mergeconflictcheck", - Topic: "merge-conflict-check", + ConsumerGroup: "runway-landconflictcheck", + Topic: "land-conflict-check", MessageID: "e2e-queue/42", PartitionKey: "e2e-queue", Payload: []byte(`{"id":"e2e-queue/42"}`), diff --git a/platform/extension/messagequeue/README.md b/platform/extension/messagequeue/README.md index ab3b30837..3a143f2e7 100644 --- a/platform/extension/messagequeue/README.md +++ b/platform/extension/messagequeue/README.md @@ -100,7 +100,7 @@ for delivery := range deliveries { A message ID is the deduplication key, scoped to its topic and partition key. A backend matches a publish against messages it still holds — including ones already consumed, since reclamation is lazy and may lag delivery by an unbounded interval — and a collision is reported to the publisher as a success that stored nothing. There is no error to retry and no row to deliver. -The ID therefore names the *occasion* to publish, not the entity published about. An entity's own ID buys exactly one message for that entity for as long as the backend remembers the first, so a stage that announces a batch at creation and another that wakes it after a merge would collide, and the wake-up would vanish. +The ID therefore names the *occasion* to publish, not the entity published about. An entity's own ID buys exactly one message for that entity for as long as the backend remembers the first, so a stage that announces a batch at creation and another that wakes it after a land would collide, and the wake-up would vanish. Producers do not choose IDs by hand. They publish through `platform/publish`, whose `IntentID(entityID, cause...)` composes the entity with the cause of this particular message: a retry of the same cause dedups, which is what makes redelivery safe, while a new cause about the same entity can never be swallowed. `UniqueID` is the fallback for a cause with nothing stable to name it by, and it trades that idempotency for guaranteed delivery. diff --git a/platform/extension/messagequeue/mysql/README.md b/platform/extension/messagequeue/mysql/README.md index c9fa8c69a..914665e64 100644 --- a/platform/extension/messagequeue/mysql/README.md +++ b/platform/extension/messagequeue/mysql/README.md @@ -26,11 +26,11 @@ defer q.Close() // Publish msg := entityqueue.NewMessage("msg-id", []byte(`{"data": "value"}`), "repo-123", nil) -q.Publisher().Publish(ctx, "merge_events", msg) +q.Publisher().Publish(ctx, "land_events", msg) // Subscribe with per-subscription config subConfig := extqueue.DefaultSubscriptionConfig("worker-1", "orchestrator") -deliveryCh, _ := q.Subscriber().Subscribe(ctx, "merge_events", subConfig) +deliveryCh, _ := q.Subscriber().Subscribe(ctx, "land_events", subConfig) for delivery := range deliveryCh { if err := process(delivery.Message()); err != nil { delivery.Nack(ctx) // Retry diff --git a/platform/extension/messagequeue/mysql/ctl/README.md b/platform/extension/messagequeue/mysql/ctl/README.md index e6cb64c2c..8cbbf1780 100644 --- a/platform/extension/messagequeue/mysql/ctl/README.md +++ b/platform/extension/messagequeue/mysql/ctl/README.md @@ -25,14 +25,14 @@ Via Make (uses Bazel): ```bash make run-queue-admin ARGS="list-topics" -make run-queue-admin ARGS="topic-stats --topic merge_queue" +make run-queue-admin ARGS="topic-stats --topic land_queue" ``` Via Bazel directly: ```bash bazel run //platform/extension/messagequeue/mysql/ctl -- list-topics -bazel run //platform/extension/messagequeue/mysql/ctl -- topic-stats --topic merge_queue +bazel run //platform/extension/messagequeue/mysql/ctl -- topic-stats --topic land_queue ``` ## Commands @@ -44,20 +44,20 @@ bazel run //platform/extension/messagequeue/mysql/ctl -- topic-stats --topic mer queue-admin list-topics # Detailed stats for a topic (total messages, DLQ count, partitions, consumer groups) -queue-admin topic-stats --topic merge_queue +queue-admin topic-stats --topic land_queue ``` ### Inspect Messages ```bash # List messages (default limit 50) -queue-admin list-messages --topic merge_queue +queue-admin list-messages --topic land_queue # Filter by partition, custom limit -queue-admin list-messages --topic merge_queue --partition uber/cadence --limit 10 +queue-admin list-messages --topic land_queue --partition uber/cadence --limit 10 # Full message details including payload and metadata -queue-admin inspect-message --topic merge_queue --message-id msg-123 +queue-admin inspect-message --topic land_queue --message-id msg-123 ``` ### Manage Messages @@ -66,13 +66,13 @@ Destructive commands prompt for confirmation by default. Use `--no-interactive` ```bash # Delete a single message -queue-admin delete-message --topic merge_queue --message-id msg-123 +queue-admin delete-message --topic land_queue --message-id msg-123 # Purge all messages from a topic -queue-admin purge-topic --topic merge_queue +queue-admin purge-topic --topic land_queue # Skip confirmation prompt (for scripting) -queue-admin purge-topic --topic merge_queue --no-interactive +queue-admin purge-topic --topic land_queue --no-interactive ``` ### Dead Letter Queue (DLQ) @@ -81,26 +81,26 @@ DLQ messages live in the same `queue_messages` table under `topic + "_dlq"` (def ```bash # List DLQ messages -queue-admin list-dlq --topic merge_queue +queue-admin list-dlq --topic land_queue # Inspect a DLQ message (use the DLQ topic name) -queue-admin inspect-message --topic merge_queue_dlq --message-id msg-456 +queue-admin inspect-message --topic land_queue_dlq --message-id msg-456 # Move a DLQ message back to the original topic -queue-admin requeue-dlq --topic merge_queue --message-id msg-456 +queue-admin requeue-dlq --topic land_queue --message-id msg-456 # Purge all DLQ messages -queue-admin purge-dlq --topic merge_queue +queue-admin purge-dlq --topic land_queue # Custom DLQ suffix (if not using default "_dlq") -queue-admin list-dlq --topic merge_queue --dlq-suffix _dead +queue-admin list-dlq --topic land_queue --dlq-suffix _dead ``` ### Consumer Lag ```bash # Per-partition lag for all consumer groups on a topic -queue-admin consumer-lag --topic merge_queue +queue-admin consumer-lag --topic land_queue ``` Output shows `ACKED` (last processed offset), `LATEST` (newest message offset), and `LAG` (unprocessed count) per partition per consumer group. @@ -115,10 +115,10 @@ queue-admin list-offsets queue-admin list-offsets --consumer-group orchestrator # Reset offset to 0 (reprocess all messages) -queue-admin reset-offset --consumer-group orchestrator --topic merge_queue --partition uber/cadence +queue-admin reset-offset --consumer-group orchestrator --topic land_queue --partition uber/cadence # Reset to a specific offset -queue-admin reset-offset --consumer-group orchestrator --topic merge_queue --partition uber/cadence --offset 42 +queue-admin reset-offset --consumer-group orchestrator --topic land_queue --partition uber/cadence --offset 42 ``` ### Partition Leases @@ -132,7 +132,7 @@ queue-admin stale-leases # default 60s threshold queue-admin stale-leases --threshold 30000 # 30s threshold # Force-release a stuck lease -queue-admin release-lease --consumer-group orchestrator --topic merge_queue --partition uber/cadence +queue-admin release-lease --consumer-group orchestrator --topic land_queue --partition uber/cadence ``` ### JSON Output @@ -141,6 +141,6 @@ Add `--json` to any read command for machine-readable output: ```bash queue-admin list-topics --json -queue-admin consumer-lag --topic merge_queue --json -queue-admin list-messages --topic merge_queue --json | jq '.[] | .ID' +queue-admin consumer-lag --topic land_queue --json +queue-admin list-messages --topic land_queue --json | jq '.[] | .ID' ``` diff --git a/platform/extension/messagequeue/mysql/schema/queue_messages.sql b/platform/extension/messagequeue/mysql/schema/queue_messages.sql index a3a655365..1bb5bf406 100644 --- a/platform/extension/messagequeue/mysql/schema/queue_messages.sql +++ b/platform/extension/messagequeue/mysql/schema/queue_messages.sql @@ -1,7 +1,7 @@ -- MESSAGES TABLE (Immutable Log) -- Single table for all topics. Partition key determines distribution across workers. -- Messages are append-only; per-consumer-group delivery tracking is in queue_delivery_state. --- Example: topic="merge_queue", partition_key="uber/cadence" +-- Example: topic="land_queue", partition_key="uber/cadence" CREATE TABLE IF NOT EXISTS queue_messages ( -- Auto-incrementing global offset for ordering diff --git a/platform/git/exec/gitexec.go b/platform/git/exec/gitexec.go index 0f7a6d8ad..6be79dc3d 100644 --- a/platform/git/exec/gitexec.go +++ b/platform/git/exec/gitexec.go @@ -15,7 +15,7 @@ // Package gitexec locates a git binary and composes the environment git runs // in. It is the single source of truth for that environment across every // SubmitQueue caller — demo tooling, the change provider's repository, and the -// Runway merger. +// Runway lander. // // The environment has two halves. The scrub set denies git all ambient // configuration that could change what a command produces — a global hooks diff --git a/platform/hook/README.md b/platform/hook/README.md index ec6a8a87d..12e1f5561 100644 --- a/platform/hook/README.md +++ b/platform/hook/README.md @@ -4,7 +4,7 @@ The consumer side of the hooks framework: the stage that turns hook events on a ## Why a stage at all -Side effects must never stall or fail the pipeline, and "fire and forget" must not mean lossy — a merge-failure comment that silently never posts is a support ticket. A durable queue between the two resolves the tension: the producer's obligation ends once the event is enqueued, which is fast and local, and everything after that gets real retry semantics and a dead-letter queue. +Side effects must never stall or fail the pipeline, and "fire and forget" must not mean lossy — a land-failure comment that silently never posts is a support ticket. A durable queue between the two resolves the tension: the producer's obligation ends once the event is enqueued, which is fast and local, and everything after that gets real retry semantics and a dead-letter queue. Calling hooks inline would give up both halves. It couples pipeline latency to whatever an integration talks to, and a crash between the state write and the call drops the notification with nothing to replay. diff --git a/platform/hook/dlq.go b/platform/hook/dlq.go index b34b1556a..f73e84fde 100644 --- a/platform/hook/dlq.go +++ b/platform/hook/dlq.go @@ -41,7 +41,7 @@ const reconcileOp = "reconcile" // // That is a deliberate step up from the log topic's DLQ, which warns and moves // on. Dropping an observability row costs a gap in a read model; dropping a -// merge-failure comment costs a support ticket, and nothing else in the system +// land-failure comment costs a support ticket, and nothing else in the system // will notice it is missing. type DLQController struct { logger *zap.SugaredLogger diff --git a/platform/metrics/README.md b/platform/metrics/README.md index b88cf6146..7a2426222 100644 --- a/platform/metrics/README.md +++ b/platform/metrics/README.md @@ -85,7 +85,7 @@ There is no default bucket set. The package exports four common sets: |-----|-------|---------| | `FastLatencyBuckets` | ~100µs – 5s | Fast in-process work such as scoring, cache lookups, and CPU-bound operations | | `StorageLatencyBuckets` | ~1ms – 1m | Storage and message-queue round trips such as database reads, writes, publishing, and consuming | -| `LongLatencyBuckets` | ~5ms – 4h | Long-running pipeline work and external calls such as builds, merges, pushes, and provider calls | +| `LongLatencyBuckets` | ~5ms – 4h | Long-running pipeline work and external calls such as builds, lands, pushes, and provider calls | | `ChangeAgeBuckets` | ~1m – 30d | Elapsed time measured from a source-control change's commit timestamp rather than from work this system started | Pass one of these sets or a custom `tally.DurationBuckets` to `Begin` or `NamedHistogram`. diff --git a/platform/metrics/metrics.go b/platform/metrics/metrics.go index 5bba9793f..aab5feb62 100644 --- a/platform/metrics/metrics.go +++ b/platform/metrics/metrics.go @@ -108,7 +108,7 @@ var ( } // LongLatencyBuckets suits long-running pipeline work and external calls - // (~5ms to hours): builds, merges, git pushes, and external provider calls. + // (~5ms to hours): builds, lands, git pushes, and external provider calls. LongLatencyBuckets = tally.DurationBuckets{ 5 * time.Millisecond, 10 * time.Millisecond, diff --git a/platform/publish/publish_test.go b/platform/publish/publish_test.go index 23ec08ae7..6dab1f773 100644 --- a/platform/publish/publish_test.go +++ b/platform/publish/publish_test.go @@ -143,8 +143,8 @@ func TestIntentID(t *testing.T) { { name: "single cause", entityID: "batch-1", - cause: []string{"merged"}, - want: "batch-1/merged", + cause: []string{"landed"}, + want: "batch-1/landed", }, { name: "multiple causes join in order", @@ -164,9 +164,9 @@ func TestIntentID(t *testing.T) { // The convention only works if the same cause is repeatable and a different // cause is distinguishable — the two properties every call site relies on. func TestIntentID_StableAcrossCallsAndDistinctPerCause(t *testing.T) { - assert.Equal(t, IntentID("batch-1", "merged"), IntentID("batch-1", "merged")) - assert.NotEqual(t, IntentID("batch-1", "merged"), IntentID("batch-1")) - assert.NotEqual(t, IntentID("batch-1", "merged"), IntentID("batch-1", "cancelling")) + assert.Equal(t, IntentID("batch-1", "landed"), IntentID("batch-1", "landed")) + assert.NotEqual(t, IntentID("batch-1", "landed"), IntentID("batch-1")) + assert.NotEqual(t, IntentID("batch-1", "landed"), IntentID("batch-1", "cancelling")) } func TestUniqueID(t *testing.T) { diff --git a/runway/README.md b/runway/README.md index 68e1baa96..d7a0901bf 100644 --- a/runway/README.md +++ b/runway/README.md @@ -1,27 +1,27 @@ # Runway -Runway owns the merge queues defined by the external contract in -[`api/runway/messagequeue`](../api/runway/messagequeue): it consumes merge-conflict-check and merge +Runway owns the land queues defined by the external contract in +[`api/runway/messagequeue`](../api/runway/messagequeue): it consumes land-conflict-check and land requests, performs the work, and (eventually) publishes the result to the corresponding signal queue. SubmitQueue is a client of these queues. Runway is a single service (the domain *is* the service); its controllers live directly under -[`controller/`](controller). It consumes Runway's merge queues: +[`controller/`](controller). It consumes Runway's land queues: -- `merge-conflict-check` — dry-run check that an ordered sequence of merge steps applies cleanly, without committing. -- `merge` — committing merge: apply and commit the ordered steps. +- `land-conflict-check` — dry-run check that an ordered sequence of land steps applies cleanly, without committing. +- `land` — committing land: apply and commit the ordered steps. -Each controller deserializes the `MergeRequest`, obtains a `Merger` for the request's queue from the [`merger`](extension/merger) extension, applies the ordered steps, and publishes a `MergeResult` to the corresponding signal queue (`merge-conflict-check-signal` / `merge-signal`). `merge` commits and reports the produced revisions; `merge-conflict-check` is a dry run that reports mergeability with empty outputs. +Each controller deserializes the `LandRequest`, obtains a `Lander` for the request's queue from the [`lander`](extension/lander) extension, applies the ordered steps, and publishes a `LandResult` to the corresponding signal queue (`land-conflict-check-signal` / `land-signal`). `land` commits and reports the produced revisions; `land-conflict-check` is a dry run that reports landability with empty outputs. -## Merger extension +## Lander extension -[`extension/merger`](extension/merger) is the pluggable merge contract. Implementations: +[`extension/lander`](extension/lander) is the pluggable land contract. Implementations: -- [`git`](extension/merger/git) — a git-CLI backend that honors each step's strategy (REBASE, SQUASH_REBASE, MERGE, PROMOTE; DEFAULT resolves to a per-instance default). See its [README](extension/merger/git/README.md). +- [`git`](extension/lander/git) — a git-CLI backend that honors each step's strategy (REBASE, SQUASH_REBASE, MERGE, PROMOTE; DEFAULT resolves to a per-instance default). See its [README](extension/lander/git/README.md). - `noop` — always-succeeds stub for local development. ## Failure handling -A merge outcome the controller can name is published as a `FAILED` result and acked, not retried: a merge conflict (`merger.ErrConflict`) or an invalid request (`merger.ErrInvalidRequest` — unknown strategy, malformed change URI, invalid PROMOTE composition). The `merger.IsTerminal` helper draws that line. Any other error is an infrastructure fault and is nacked for retry. +A land outcome the controller can name is published as a `FAILED` result and acked, not retried: a land conflict (`lander.ErrConflict`) or an invalid request (`lander.ErrInvalidRequest` — unknown strategy, malformed change URI, invalid PROMOTE composition). The `lander.IsTerminal` helper draws that line. Any other error is an infrastructure fault and is nacked for retry. -Because Runway is stateless and the sole responder on the client's correlation id, a request that exhausts retries (or hits an unexpected fault) must still resolve the client. The inbound topics dead-letter by default; the [`dlq`](controller/dlq) reconciler drains those `_dlq` topics and republishes a `FAILED` `MergeResult` to the signal topic so the correlation id never hangs. +Because Runway is stateless and the sole responder on the client's correlation id, a request that exhausts retries (or hits an unexpected fault) must still resolve the client. The inbound topics dead-letter by default; the [`dlq`](controller/dlq) reconciler drains those `_dlq` topics and republishes a `FAILED` `LandResult` to the signal topic so the correlation id never hangs. diff --git a/runway/controller/dlq/dlq.go b/runway/controller/dlq/dlq.go index d53f25c74..265843409 100644 --- a/runway/controller/dlq/dlq.go +++ b/runway/controller/dlq/dlq.go @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package dlq reconciles dead-lettered merge requests back to the client. +// Package dlq reconciles dead-lettered land requests back to the client. // -// Runway's inbound merge topics dead-letter a message after the primary +// Runway's inbound land topics dead-letter a message after the primary // controller returns a non-retryable error or exhausts retries on a retryable // one. Runway is stateless and the sole responder on the client's correlation // id, so a dead-lettered request that produced no signal would leave the client @@ -23,9 +23,9 @@ // this reconciler is the backstop for everything else (unexpected faults, retry // exhaustion). // -// On each delivery from a `{topic}_dlq` topic it decodes the same MergeRequest +// On each delivery from a `{topic}_dlq` topic it decodes the same LandRequest // payload the primary controller consumes (the queue preserves the bytes -// verbatim), and republishes a FAILED MergeResult — echoing the correlation id +// verbatim), and republishes a FAILED LandResult — echoing the correlation id // — to the corresponding signal topic. Unlike the stovepipe/orchestrator DLQ // reconcilers it writes no entity state (Runway has none); the signal is the // resolution. A payload that cannot be decoded carries no correlation id to @@ -54,7 +54,7 @@ import ( // subscription's topic name matches the controller's TopicKey(). const topicSuffix = "_dlq" -// TopicKey returns the DLQ topic key for a primary merge topic. Exported so the +// TopicKey returns the DLQ topic key for a primary land topic. Exported so the // wiring layer builds matching pairs without duplicating the suffix literal. func TopicKey(main consumer.TopicKey) consumer.TopicKey { return consumer.TopicKey(string(main) + topicSuffix) @@ -63,7 +63,7 @@ func TopicKey(main consumer.TopicKey) consumer.TopicKey { // Verify Controller implements consumer.Controller at compile time. var _ consumer.Controller = (*Controller)(nil) -// Controller consumes a merge topic's dead-letter queue and republishes a +// Controller consumes a land topic's dead-letter queue and republishes a // terminal FAILED result to the corresponding signal topic. type Controller struct { logger *zap.SugaredLogger @@ -89,11 +89,11 @@ type Params struct { Logger *zap.SugaredLogger } -// NewController creates a DLQ reconciler for a merge topic's dead-letter queue. +// NewController creates a DLQ reconciler for a land topic's dead-letter queue. func NewController(p Params) *Controller { return &Controller{ - logger: p.Logger.Named("merge_dlq_controller"), - metricsScope: p.Scope.SubScope("merge_dlq_controller"), + logger: p.Logger.Named("land_dlq_controller"), + metricsScope: p.Scope.SubScope("land_dlq_controller"), registry: p.Registry, topicKey: p.TopicKey, signalTopicKey: p.SignalTopicKey, @@ -101,7 +101,7 @@ func NewController(p Params) *Controller { } } -// Process decodes the dead-lettered merge request and republishes a FAILED +// Process decodes the dead-lettered land request and republishes a FAILED // result to the signal topic so the client's correlation id resolves. Returns // nil to ack; an error to nack (retried indefinitely under // AlwaysRetryableProcessor). @@ -111,12 +111,12 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er msg := delivery.Message() meta := delivery.Metadata() - request := &runwaymq.MergeRequest{} + request := &runwaymq.LandRequest{} if err := runwaymq.Unmarshal(msg.Payload, request); err != nil { // No correlation id is recoverable, so there is nothing to resolve for // the client. Log and ack (drop) rather than retry forever. metrics.NamedCounter(c.metricsScope, opName, "undecodable", 1) - c.logger.Errorw("dlq reconcile: undecodable merge request, dropping", + c.logger.Errorw("dlq reconcile: undecodable land request, dropping", "err", err, "original_topic", meta["dlq.original_topic"], ) @@ -125,7 +125,7 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er reason := meta["dlq.last_error"] if reason == "" { - reason = "runway failed to process the merge request" + reason = "runway failed to process the land request" } c.logger.Warnw("dlq reconcile: publishing terminal failure", @@ -136,7 +136,7 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er "last_error", reason, ) - result := &runwaymq.MergeResult{ + result := &runwaymq.LandResult{ Id: request.GetId(), Outcome: runwaypb.Outcome_FAILED, Reason: fmt.Sprintf("dead-lettered: %s", reason), @@ -152,16 +152,16 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er return nil } -// publish serializes a MergeResult and publishes it to the signal topic. +// publish serializes a LandResult and publishes it to the signal topic. // // Named for the dead letter, because the live handler answers the same request // on the same topic under the bare correlation ID. Reusing that ID would let // this terminal failure be deduplicated against an answer that was already // sent, and the caller would go on waiting for a result nothing will produce. -func (c *Controller) publish(ctx context.Context, result *runwaymq.MergeResult, partitionKey string) error { +func (c *Controller) publish(ctx context.Context, result *runwaymq.LandResult, partitionKey string) error { payload, err := runwaymq.Marshal(result) if err != nil { - return fmt.Errorf("failed to serialize merge result: %w", err) + return fmt.Errorf("failed to serialize land result: %w", err) } if err := publish.Message(ctx, c.registry, c.signalTopicKey, diff --git a/runway/controller/dlq/dlq_test.go b/runway/controller/dlq/dlq_test.go index 360852834..4e4cc013b 100644 --- a/runway/controller/dlq/dlq_test.go +++ b/runway/controller/dlq/dlq_test.go @@ -70,7 +70,7 @@ func newRegistry(t *testing.T, ctrl *gomock.Controller) (consumer.TopicRegistry, q.EXPECT().Publisher().Return(pub).AnyTimes() registry, err := consumer.NewTopicRegistry([]consumer.TopicConfig{ - {Key: runwaymq.TopicKeyMergeSignal, Name: "merge-signal", Queue: q}, + {Key: runwaymq.TopicKeyLandSignal, Name: "land-signal", Queue: q}, }) require.NoError(t, err) return registry, &published @@ -82,9 +82,9 @@ func newController(t *testing.T, registry consumer.TopicRegistry) *Controller { Logger: zaptest.NewLogger(t).Sugar(), Scope: tally.NoopScope, Registry: registry, - TopicKey: TopicKey(runwaymq.TopicKeyMerge), - SignalTopicKey: runwaymq.TopicKeyMergeSignal, - ConsumerGroup: "runway-merge-dlq", + TopicKey: TopicKey(runwaymq.TopicKeyLand), + SignalTopicKey: runwaymq.TopicKeyLandSignal, + ConsumerGroup: "runway-land-dlq", }) } @@ -93,17 +93,17 @@ func TestProcess_DecodableRepublishesFailure(t *testing.T) { registry, published := newRegistry(t, ctrl) controller := newController(t, registry) - req := &runwaymq.MergeRequest{ + req := &runwaymq.LandRequest{ Id: testID, QueueName: testQueue, - Steps: []*runwaymq.MergeStep{{StepId: "step-1"}}, + Steps: []*runwaymq.LandStep{{StepId: "step-1"}}, } payload, err := runwaymq.Marshal(req) require.NoError(t, err) meta := map[string]string{ "dlq.last_error": "boom: connection refused", - "dlq.original_topic": "runway-merge", + "dlq.original_topic": "runway-land", } delivery := newDelivery(t, ctrl, payload, meta) @@ -111,9 +111,9 @@ func TestProcess_DecodableRepublishesFailure(t *testing.T) { require.Len(t, *published, 1) got := (*published)[0] - assert.Equal(t, "merge-signal", got.topic) + assert.Equal(t, "land-signal", got.topic) - result := &runwaymq.MergeResult{} + result := &runwaymq.LandResult{} require.NoError(t, runwaymq.Unmarshal(got.msg.Payload, result)) assert.Equal(t, testID, result.Id) assert.Equal(t, runwaypb.Outcome_FAILED, result.Outcome) @@ -125,7 +125,7 @@ func TestProcess_UndecodableAcksAndPublishesNothing(t *testing.T) { registry, published := newRegistry(t, ctrl) controller := newController(t, registry) - delivery := newDelivery(t, ctrl, []byte("{bad"), map[string]string{"dlq.original_topic": "runway-merge"}) + delivery := newDelivery(t, ctrl, []byte("{bad"), map[string]string{"dlq.original_topic": "runway-land"}) require.NoError(t, controller.Process(context.Background(), delivery)) assert.Empty(t, *published) diff --git a/runway/controller/merge/BUILD.bazel b/runway/controller/land/BUILD.bazel similarity index 85% rename from runway/controller/merge/BUILD.bazel rename to runway/controller/land/BUILD.bazel index 17434cd1a..ba944b772 100644 --- a/runway/controller/merge/BUILD.bazel +++ b/runway/controller/land/BUILD.bazel @@ -2,8 +2,8 @@ load("@rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", - srcs = ["merge.go"], - importpath = "github.com/uber/submitqueue/runway/controller/merge", + srcs = ["land.go"], + importpath = "github.com/uber/submitqueue/runway/controller/land", visibility = ["//visibility:public"], deps = [ "//api/runway/messagequeue:go_default_library", @@ -11,7 +11,7 @@ go_library( "//platform/consumer:go_default_library", "//platform/metrics:go_default_library", "//platform/publish:go_default_library", - "//runway/extension/merger:go_default_library", + "//runway/extension/lander:go_default_library", "@com_github_uber_go_tally//:go_default_library", "@org_uber_go_zap//:go_default_library", ], @@ -19,7 +19,7 @@ go_library( go_test( name = "go_default_test", - srcs = ["merge_test.go"], + srcs = ["land_test.go"], embed = [":go_default_library"], deps = [ "//api/runway/messagequeue:go_default_library", @@ -28,8 +28,8 @@ go_test( "//platform/consumer:go_default_library", "//platform/consumer/mock:go_default_library", "//platform/extension/messagequeue/mock:go_default_library", - "//runway/extension/merger:go_default_library", - "//runway/extension/merger/mock:go_default_library", + "//runway/extension/lander:go_default_library", + "//runway/extension/lander/mock: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", diff --git a/runway/controller/merge/merge.go b/runway/controller/land/land.go similarity index 65% rename from runway/controller/merge/merge.go rename to runway/controller/land/land.go index 8a98cb3d4..06321b744 100644 --- a/runway/controller/merge/merge.go +++ b/runway/controller/land/land.go @@ -12,15 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package merge consumes committing merge requests from Runway's merge queue. A -// request asks Runway to apply an ordered sequence of merge steps onto the merge +// Package land consumes committing land requests from Runway's land queue. A +// request asks Runway to apply an ordered sequence of land steps onto the land // target and commit the result. // -// The controller obtains a Merger for the request's merge target, calls Merge, -// and publishes the MergeResult (with the produced revisions) to the merge-signal -// queue. A merge conflict is an expected outcome (ack + publish FAILED result), +// The controller obtains a Lander for the request's land target, calls Land, +// and publishes the LandResult (with the produced revisions) to the land-signal +// queue. A land conflict is an expected outcome (ack + publish FAILED result), // not an infrastructure error. -package merge +package land import ( "context" @@ -33,61 +33,61 @@ import ( "github.com/uber/submitqueue/platform/consumer" "github.com/uber/submitqueue/platform/metrics" "github.com/uber/submitqueue/platform/publish" - "github.com/uber/submitqueue/runway/extension/merger" + "github.com/uber/submitqueue/runway/extension/lander" "go.uber.org/zap" ) // Verify Controller implements consumer.Controller interface at compile time. var _ consumer.Controller = (*Controller)(nil) -// Controller handles merge queue messages. +// Controller handles land queue messages. type Controller struct { logger *zap.SugaredLogger metricsScope tally.Scope - mergerFactory merger.Factory + landerFactory lander.Factory registry consumer.TopicRegistry topicKey consumer.TopicKey consumerGroup string } -// Params are the parameters for creating a new merge controller. +// Params are the parameters for creating a new land controller. type Params struct { TopicKey consumer.TopicKey ConsumerGroup string - MergerFactory merger.Factory + LanderFactory lander.Factory Registry consumer.TopicRegistry Scope tally.Scope Logger *zap.SugaredLogger } -// NewController creates a new merge controller for the runway service. +// NewController creates a new land controller for the runway service. func NewController(p Params) *Controller { return &Controller{ - logger: p.Logger.Named("merge_controller"), - metricsScope: p.Scope.SubScope("merge_controller"), - mergerFactory: p.MergerFactory, + logger: p.Logger.Named("land_controller"), + metricsScope: p.Scope.SubScope("land_controller"), + landerFactory: p.LanderFactory, registry: p.Registry, topicKey: p.TopicKey, consumerGroup: p.ConsumerGroup, } } -// Process deserializes the merge request, performs the committing merge, and +// Process deserializes the land request, performs the committing land, and // publishes the result. Returns nil to ack, or an error to nack. func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) error { const opName = "process" msg := delivery.Message() - request := &runwaymq.MergeRequest{} + request := &runwaymq.LandRequest{} if err := runwaymq.Unmarshal(msg.Payload, request); err != nil { metrics.NamedCounter(c.metricsScope, opName, "deserialize_errors", 1) - return fmt.Errorf("failed to deserialize merge request: %w", err) + return fmt.Errorf("failed to deserialize land request: %w", err) } - c.logger.Infow("received merge request", + c.logger.Infow("received land request", "id", request.Id, "queue_name", request.QueueName, "step_count", len(request.Steps), @@ -95,33 +95,33 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er "partition_key", msg.PartitionKey, ) - m, err := c.mergerFactory.For(merger.Config{QueueName: request.GetQueueName()}) + m, err := c.landerFactory.For(lander.Config{QueueName: request.GetQueueName()}) if err != nil { metrics.NamedCounter(c.metricsScope, opName, "factory_errors", 1) - return fmt.Errorf("failed to create merger for queue %s: %w", request.GetQueueName(), err) + return fmt.Errorf("failed to create lander for queue %s: %w", request.GetQueueName(), err) } - result, err := m.Merge(ctx, request) + result, err := m.Land(ctx, request) if err != nil { - if !merger.IsTerminal(err) { - metrics.NamedCounter(c.metricsScope, opName, "merge_errors", 1) - return fmt.Errorf("failed to merge for %s: %w", request.GetId(), err) + if !lander.IsTerminal(err) { + metrics.NamedCounter(c.metricsScope, opName, "land_errors", 1) + return fmt.Errorf("failed to land for %s: %w", request.GetId(), err) } - if errors.Is(err, merger.ErrInvalidRequest) { + if errors.Is(err, lander.ErrInvalidRequest) { metrics.NamedCounter(c.metricsScope, opName, "invalid_requests", 1) - c.logger.Infow("invalid merge request", + c.logger.Infow("invalid land request", "id", request.GetId(), "queue_name", request.GetQueueName(), "err", err, ) } else { - metrics.NamedCounter(c.metricsScope, opName, "merge_conflicts", 1) - c.logger.Infow("merge conflict detected", + metrics.NamedCounter(c.metricsScope, opName, "land_conflicts", 1) + c.logger.Infow("land conflict detected", "id", request.GetId(), "queue_name", request.GetQueueName(), ) } - result = &runwaymq.MergeResult{ + result = &runwaymq.LandResult{ Id: request.GetId(), Outcome: runwaypb.Outcome_FAILED, Reason: err.Error(), @@ -132,25 +132,25 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er // queue without loading state first. result.QueueName = request.GetQueueName() - if err := c.publish(ctx, runwaymq.TopicKeyMergeSignal, result, msg.PartitionKey); err != nil { + if err := c.publish(ctx, runwaymq.TopicKeyLandSignal, result, msg.PartitionKey); err != nil { metrics.NamedCounter(c.metricsScope, opName, "publish_errors", 1) - return fmt.Errorf("failed to publish merge result for %s: %w", request.GetId(), err) + return fmt.Errorf("failed to publish land result for %s: %w", request.GetId(), err) } return nil } -// publish serializes a MergeResult and publishes it to the given signal topic. +// publish serializes a LandResult and publishes it to the given signal topic. // // The message ID is the correlation ID with no cause: a request is answered // once, so a redelivery that re-answers it is meant to dedup rather than tell // the caller twice. The dead-letter path answers the same request when this // one never could, and names itself so it cannot be mistaken for a repeat of // this answer. -func (c *Controller) publish(ctx context.Context, key consumer.TopicKey, result *runwaymq.MergeResult, partitionKey string) error { +func (c *Controller) publish(ctx context.Context, key consumer.TopicKey, result *runwaymq.LandResult, partitionKey string) error { payload, err := runwaymq.Marshal(result) if err != nil { - return fmt.Errorf("failed to serialize merge result: %w", err) + return fmt.Errorf("failed to serialize land result: %w", err) } if err := publish.Message(ctx, c.registry, key, publish.IntentID(result.GetId()), payload, partitionKey); err != nil { @@ -162,7 +162,7 @@ func (c *Controller) publish(ctx context.Context, key consumer.TopicKey, result // Name returns the controller name for logging and metrics. func (c *Controller) Name() string { - return "merge" + return "land" } // TopicKey returns the topic key this controller subscribes to. diff --git a/runway/controller/merge/merge_test.go b/runway/controller/land/land_test.go similarity index 70% rename from runway/controller/merge/merge_test.go rename to runway/controller/land/land_test.go index c7900863c..d7380dd2c 100644 --- a/runway/controller/merge/merge_test.go +++ b/runway/controller/land/land_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package merge +package land import ( "context" @@ -28,8 +28,8 @@ import ( "github.com/uber/submitqueue/platform/consumer" consumermock "github.com/uber/submitqueue/platform/consumer/mock" queuemock "github.com/uber/submitqueue/platform/extension/messagequeue/mock" - "github.com/uber/submitqueue/runway/extension/merger" - mergermock "github.com/uber/submitqueue/runway/extension/merger/mock" + "github.com/uber/submitqueue/runway/extension/lander" + landermock "github.com/uber/submitqueue/runway/extension/lander/mock" "go.uber.org/mock/gomock" "go.uber.org/zap/zaptest" ) @@ -49,7 +49,7 @@ func newDelivery(t *testing.T, ctrl *gomock.Controller, payload []byte) *consume return d } -func requestPayload(t *testing.T, req *runwaymq.MergeRequest) []byte { +func requestPayload(t *testing.T, req *runwaymq.LandRequest) []byte { t.Helper() payload, err := runwaymq.Marshal(req) require.NoError(t, err) @@ -69,40 +69,40 @@ func newRegistry(t *testing.T, ctrl *gomock.Controller, publishErr error) (consu q.EXPECT().Publisher().Return(pub).AnyTimes() registry, err := consumer.NewTopicRegistry([]consumer.TopicConfig{ - {Key: runwaymq.TopicKeyMergeSignal, Name: "merge-signal", Queue: q}, + {Key: runwaymq.TopicKeyLandSignal, Name: "land-signal", Queue: q}, }) require.NoError(t, err) return registry, pub } -func newController(t *testing.T, factory merger.Factory, registry consumer.TopicRegistry) *Controller { +func newController(t *testing.T, factory lander.Factory, registry consumer.TopicRegistry) *Controller { t.Helper() return NewController(Params{ Logger: zaptest.NewLogger(t).Sugar(), Scope: tally.NoopScope, - MergerFactory: factory, + LanderFactory: factory, Registry: registry, - TopicKey: runwaymq.TopicKeyMerge, - ConsumerGroup: "runway-merge", + TopicKey: runwaymq.TopicKeyLand, + ConsumerGroup: "runway-land", }) } func TestNewController(t *testing.T) { ctrl := gomock.NewController(t) - factory := mergermock.NewMockFactory(ctrl) + factory := landermock.NewMockFactory(ctrl) registry, _ := newRegistry(t, ctrl, nil) controller := newController(t, factory, registry) require.NotNil(t, controller) - assert.Equal(t, runwaymq.TopicKeyMerge, controller.TopicKey()) - assert.Equal(t, "runway-merge", controller.ConsumerGroup()) - assert.Equal(t, "merge", controller.Name()) + assert.Equal(t, runwaymq.TopicKeyLand, controller.TopicKey()) + assert.Equal(t, "runway-land", controller.ConsumerGroup()) + assert.Equal(t, "land", controller.Name()) } func TestProcess_Success(t *testing.T) { ctrl := gomock.NewController(t) - expectedResult := &runwaymq.MergeResult{ + expectedResult := &runwaymq.LandResult{ Id: testID, Outcome: runwaypb.Outcome_SUCCEEDED, Steps: []*runwaymq.StepResult{ @@ -110,11 +110,11 @@ func TestProcess_Success(t *testing.T) { }, } - m := mergermock.NewMockMerger(ctrl) - m.EXPECT().Merge(gomock.Any(), gomock.Any()).Return(expectedResult, nil) + m := landermock.NewMockLander(ctrl) + m.EXPECT().Land(gomock.Any(), gomock.Any()).Return(expectedResult, nil) - factory := mergermock.NewMockFactory(ctrl) - factory.EXPECT().For(merger.Config{QueueName: testQueue}).Return(m, nil) + factory := landermock.NewMockFactory(ctrl) + factory.EXPECT().For(lander.Config{QueueName: testQueue}).Return(m, nil) var gotTopic string var gotPayload []byte @@ -129,23 +129,23 @@ func TestProcess_Success(t *testing.T) { q := queuemock.NewMockQueue(ctrl) q.EXPECT().Publisher().Return(pub).AnyTimes() registry, err := consumer.NewTopicRegistry([]consumer.TopicConfig{ - {Key: runwaymq.TopicKeyMergeSignal, Name: "merge-signal", Queue: q}, + {Key: runwaymq.TopicKeyLandSignal, Name: "land-signal", Queue: q}, }) require.NoError(t, err) controller := newController(t, factory, registry) - req := &runwaymq.MergeRequest{ + req := &runwaymq.LandRequest{ Id: testID, QueueName: testQueue, - Steps: []*runwaymq.MergeStep{{StepId: "step-1"}}, + Steps: []*runwaymq.LandStep{{StepId: "step-1"}}, } delivery := newDelivery(t, ctrl, requestPayload(t, req)) require.NoError(t, controller.Process(context.Background(), delivery)) - assert.Equal(t, "merge-signal", gotTopic) - result := &runwaymq.MergeResult{} + assert.Equal(t, "land-signal", gotTopic) + result := &runwaymq.LandResult{} require.NoError(t, runwaymq.Unmarshal(gotPayload, result)) assert.Equal(t, testID, result.Id) assert.Equal(t, runwaypb.Outcome_SUCCEEDED, result.Outcome) @@ -154,14 +154,14 @@ func TestProcess_Success(t *testing.T) { assert.Equal(t, "abc123", result.Steps[0].Outputs[0].Id) } -func TestProcess_MergeConflict(t *testing.T) { +func TestProcess_LandConflict(t *testing.T) { ctrl := gomock.NewController(t) - m := mergermock.NewMockMerger(ctrl) - m.EXPECT().Merge(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("conflict in foo.go: %w", merger.ErrConflict)) + m := landermock.NewMockLander(ctrl) + m.EXPECT().Land(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("conflict in foo.go: %w", lander.ErrConflict)) - factory := mergermock.NewMockFactory(ctrl) - factory.EXPECT().For(merger.Config{QueueName: testQueue}).Return(m, nil) + factory := landermock.NewMockFactory(ctrl) + factory.EXPECT().For(lander.Config{QueueName: testQueue}).Return(m, nil) var gotTopic string var gotPayload []byte @@ -176,23 +176,23 @@ func TestProcess_MergeConflict(t *testing.T) { q := queuemock.NewMockQueue(ctrl) q.EXPECT().Publisher().Return(pub).AnyTimes() registry, err := consumer.NewTopicRegistry([]consumer.TopicConfig{ - {Key: runwaymq.TopicKeyMergeSignal, Name: "merge-signal", Queue: q}, + {Key: runwaymq.TopicKeyLandSignal, Name: "land-signal", Queue: q}, }) require.NoError(t, err) controller := newController(t, factory, registry) - req := &runwaymq.MergeRequest{ + req := &runwaymq.LandRequest{ Id: testID, QueueName: testQueue, - Steps: []*runwaymq.MergeStep{{StepId: "step-1"}}, + Steps: []*runwaymq.LandStep{{StepId: "step-1"}}, } delivery := newDelivery(t, ctrl, requestPayload(t, req)) require.NoError(t, controller.Process(context.Background(), delivery)) - assert.Equal(t, "merge-signal", gotTopic) - result := &runwaymq.MergeResult{} + assert.Equal(t, "land-signal", gotTopic) + result := &runwaymq.LandResult{} require.NoError(t, runwaymq.Unmarshal(gotPayload, result)) assert.Equal(t, testID, result.Id) assert.Equal(t, runwaypb.Outcome_FAILED, result.Outcome) @@ -202,11 +202,11 @@ func TestProcess_MergeConflict(t *testing.T) { func TestProcess_InvalidRequest(t *testing.T) { ctrl := gomock.NewController(t) - m := mergermock.NewMockMerger(ctrl) - m.EXPECT().Merge(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("bad strategy: %w", merger.ErrInvalidRequest)) + m := landermock.NewMockLander(ctrl) + m.EXPECT().Land(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("bad strategy: %w", lander.ErrInvalidRequest)) - factory := mergermock.NewMockFactory(ctrl) - factory.EXPECT().For(merger.Config{QueueName: testQueue}).Return(m, nil) + factory := landermock.NewMockFactory(ctrl) + factory.EXPECT().For(lander.Config{QueueName: testQueue}).Return(m, nil) var gotTopic string var gotPayload []byte @@ -221,45 +221,45 @@ func TestProcess_InvalidRequest(t *testing.T) { q := queuemock.NewMockQueue(ctrl) q.EXPECT().Publisher().Return(pub).AnyTimes() registry, err := consumer.NewTopicRegistry([]consumer.TopicConfig{ - {Key: runwaymq.TopicKeyMergeSignal, Name: "merge-signal", Queue: q}, + {Key: runwaymq.TopicKeyLandSignal, Name: "land-signal", Queue: q}, }) require.NoError(t, err) controller := newController(t, factory, registry) - req := &runwaymq.MergeRequest{ + req := &runwaymq.LandRequest{ Id: testID, QueueName: testQueue, - Steps: []*runwaymq.MergeStep{{StepId: "step-1"}}, + Steps: []*runwaymq.LandStep{{StepId: "step-1"}}, } delivery := newDelivery(t, ctrl, requestPayload(t, req)) require.NoError(t, controller.Process(context.Background(), delivery)) - assert.Equal(t, "merge-signal", gotTopic) - result := &runwaymq.MergeResult{} + assert.Equal(t, "land-signal", gotTopic) + result := &runwaymq.LandResult{} require.NoError(t, runwaymq.Unmarshal(gotPayload, result)) assert.Equal(t, testID, result.Id) assert.Equal(t, runwaypb.Outcome_FAILED, result.Outcome) assert.NotEmpty(t, result.Reason) } -func TestProcess_MergerInfraError(t *testing.T) { +func TestProcess_LanderInfraError(t *testing.T) { ctrl := gomock.NewController(t) - m := mergermock.NewMockMerger(ctrl) - m.EXPECT().Merge(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("git timeout")) + m := landermock.NewMockLander(ctrl) + m.EXPECT().Land(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("git timeout")) - factory := mergermock.NewMockFactory(ctrl) - factory.EXPECT().For(merger.Config{QueueName: testQueue}).Return(m, nil) + factory := landermock.NewMockFactory(ctrl) + factory.EXPECT().For(lander.Config{QueueName: testQueue}).Return(m, nil) registry, _ := newRegistry(t, ctrl, nil) controller := newController(t, factory, registry) - req := &runwaymq.MergeRequest{ + req := &runwaymq.LandRequest{ Id: testID, QueueName: testQueue, - Steps: []*runwaymq.MergeStep{{StepId: "step-1"}}, + Steps: []*runwaymq.LandStep{{StepId: "step-1"}}, } delivery := newDelivery(t, ctrl, requestPayload(t, req)) @@ -270,16 +270,16 @@ func TestProcess_MergerInfraError(t *testing.T) { func TestProcess_FactoryError(t *testing.T) { ctrl := gomock.NewController(t) - factory := mergermock.NewMockFactory(ctrl) - factory.EXPECT().For(merger.Config{QueueName: testQueue}).Return(nil, fmt.Errorf("unknown queue")) + factory := landermock.NewMockFactory(ctrl) + factory.EXPECT().For(lander.Config{QueueName: testQueue}).Return(nil, fmt.Errorf("unknown queue")) registry, _ := newRegistry(t, ctrl, nil) controller := newController(t, factory, registry) - req := &runwaymq.MergeRequest{ + req := &runwaymq.LandRequest{ Id: testID, QueueName: testQueue, - Steps: []*runwaymq.MergeStep{{StepId: "step-1"}}, + Steps: []*runwaymq.LandStep{{StepId: "step-1"}}, } delivery := newDelivery(t, ctrl, requestPayload(t, req)) @@ -290,24 +290,24 @@ func TestProcess_FactoryError(t *testing.T) { func TestProcess_PublishError(t *testing.T) { ctrl := gomock.NewController(t) - expectedResult := &runwaymq.MergeResult{ + expectedResult := &runwaymq.LandResult{ Id: testID, Outcome: runwaypb.Outcome_SUCCEEDED, } - m := mergermock.NewMockMerger(ctrl) - m.EXPECT().Merge(gomock.Any(), gomock.Any()).Return(expectedResult, nil) + m := landermock.NewMockLander(ctrl) + m.EXPECT().Land(gomock.Any(), gomock.Any()).Return(expectedResult, nil) - factory := mergermock.NewMockFactory(ctrl) - factory.EXPECT().For(merger.Config{QueueName: testQueue}).Return(m, nil) + factory := landermock.NewMockFactory(ctrl) + factory.EXPECT().For(lander.Config{QueueName: testQueue}).Return(m, nil) registry, _ := newRegistry(t, ctrl, fmt.Errorf("publish failed")) controller := newController(t, factory, registry) - req := &runwaymq.MergeRequest{ + req := &runwaymq.LandRequest{ Id: testID, QueueName: testQueue, - Steps: []*runwaymq.MergeStep{{StepId: "step-1"}}, + Steps: []*runwaymq.LandStep{{StepId: "step-1"}}, } delivery := newDelivery(t, ctrl, requestPayload(t, req)) @@ -317,7 +317,7 @@ func TestProcess_PublishError(t *testing.T) { func TestProcess_DeserializeError(t *testing.T) { ctrl := gomock.NewController(t) - factory := mergermock.NewMockFactory(ctrl) + factory := landermock.NewMockFactory(ctrl) registry, _ := newRegistry(t, ctrl, nil) controller := newController(t, factory, registry) diff --git a/runway/controller/mergeconflictcheck/BUILD.bazel b/runway/controller/landconflictcheck/BUILD.bazel similarity index 83% rename from runway/controller/mergeconflictcheck/BUILD.bazel rename to runway/controller/landconflictcheck/BUILD.bazel index dc9ac9274..863287532 100644 --- a/runway/controller/mergeconflictcheck/BUILD.bazel +++ b/runway/controller/landconflictcheck/BUILD.bazel @@ -2,8 +2,8 @@ load("@rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", - srcs = ["mergeconflictcheck.go"], - importpath = "github.com/uber/submitqueue/runway/controller/mergeconflictcheck", + srcs = ["landconflictcheck.go"], + importpath = "github.com/uber/submitqueue/runway/controller/landconflictcheck", visibility = ["//visibility:public"], deps = [ "//api/runway/messagequeue:go_default_library", @@ -11,7 +11,7 @@ go_library( "//platform/consumer:go_default_library", "//platform/metrics:go_default_library", "//platform/publish:go_default_library", - "//runway/extension/merger:go_default_library", + "//runway/extension/lander:go_default_library", "@com_github_uber_go_tally//:go_default_library", "@org_uber_go_zap//:go_default_library", ], @@ -19,7 +19,7 @@ go_library( go_test( name = "go_default_test", - srcs = ["mergeconflictcheck_test.go"], + srcs = ["landconflictcheck_test.go"], embed = [":go_default_library"], deps = [ "//api/runway/messagequeue:go_default_library", @@ -28,8 +28,8 @@ go_test( "//platform/consumer:go_default_library", "//platform/consumer/mock:go_default_library", "//platform/extension/messagequeue/mock:go_default_library", - "//runway/extension/merger:go_default_library", - "//runway/extension/merger/mock:go_default_library", + "//runway/extension/lander:go_default_library", + "//runway/extension/lander/mock: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", diff --git a/runway/controller/mergeconflictcheck/mergeconflictcheck.go b/runway/controller/landconflictcheck/landconflictcheck.go similarity index 63% rename from runway/controller/mergeconflictcheck/mergeconflictcheck.go rename to runway/controller/landconflictcheck/landconflictcheck.go index 202f879b7..070699f9b 100644 --- a/runway/controller/mergeconflictcheck/mergeconflictcheck.go +++ b/runway/controller/landconflictcheck/landconflictcheck.go @@ -12,15 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package mergeconflictcheck consumes dry-run merge-conflict check requests from -// Runway's merge-conflict-check queue. A request asks whether an ordered sequence -// of merge steps can be applied cleanly onto the merge target. +// Package landconflictcheck consumes dry-run land-conflict check requests from +// Runway's land-conflict-check queue. A request asks whether an ordered sequence +// of land steps can be applied cleanly onto the land target. // -// The controller obtains a Merger for the request's merge target, calls -// CheckMergeability, and publishes the MergeResult to the -// merge-conflict-check-signal queue. A merge conflict is an expected outcome +// The controller obtains a Lander for the request's land target, calls +// CheckLandability, and publishes the LandResult to the +// land-conflict-check-signal queue. A land conflict is an expected outcome // (ack + publish FAILED result), not an infrastructure error. -package mergeconflictcheck +package landconflictcheck import ( "context" @@ -33,61 +33,61 @@ import ( "github.com/uber/submitqueue/platform/consumer" "github.com/uber/submitqueue/platform/metrics" "github.com/uber/submitqueue/platform/publish" - "github.com/uber/submitqueue/runway/extension/merger" + "github.com/uber/submitqueue/runway/extension/lander" "go.uber.org/zap" ) // Verify Controller implements consumer.Controller interface at compile time. var _ consumer.Controller = (*Controller)(nil) -// Controller handles merge-conflict-check queue messages. +// Controller handles land-conflict-check queue messages. type Controller struct { logger *zap.SugaredLogger metricsScope tally.Scope - mergerFactory merger.Factory + landerFactory lander.Factory registry consumer.TopicRegistry topicKey consumer.TopicKey consumerGroup string } -// Params are the parameters for creating a new merge-conflict-check controller. +// Params are the parameters for creating a new land-conflict-check controller. type Params struct { TopicKey consumer.TopicKey ConsumerGroup string - MergerFactory merger.Factory + LanderFactory lander.Factory Registry consumer.TopicRegistry Scope tally.Scope Logger *zap.SugaredLogger } -// NewController creates a new merge-conflict-check controller for the runway service. +// NewController creates a new land-conflict-check controller for the runway service. func NewController(p Params) *Controller { return &Controller{ - logger: p.Logger.Named("mergeconflictcheck_controller"), - metricsScope: p.Scope.SubScope("mergeconflictcheck_controller"), - mergerFactory: p.MergerFactory, + logger: p.Logger.Named("landconflictcheck_controller"), + metricsScope: p.Scope.SubScope("landconflictcheck_controller"), + landerFactory: p.LanderFactory, registry: p.Registry, topicKey: p.TopicKey, consumerGroup: p.ConsumerGroup, } } -// Process deserializes the merge request, performs a dry-run merge check, and +// Process deserializes the land request, performs a dry-run landability check, and // publishes the result. Returns nil to ack, or an error to nack. func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) error { const opName = "process" msg := delivery.Message() - request := &runwaymq.MergeRequest{} + request := &runwaymq.LandRequest{} if err := runwaymq.Unmarshal(msg.Payload, request); err != nil { metrics.NamedCounter(c.metricsScope, opName, "deserialize_errors", 1) - return fmt.Errorf("failed to deserialize merge request: %w", err) + return fmt.Errorf("failed to deserialize land request: %w", err) } - c.logger.Infow("received merge-conflict-check request", + c.logger.Infow("received land-conflict-check request", "id", request.Id, "queue_name", request.QueueName, "step_count", len(request.Steps), @@ -95,33 +95,33 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er "partition_key", msg.PartitionKey, ) - m, err := c.mergerFactory.For(merger.Config{QueueName: request.GetQueueName()}) + m, err := c.landerFactory.For(lander.Config{QueueName: request.GetQueueName()}) if err != nil { metrics.NamedCounter(c.metricsScope, opName, "factory_errors", 1) - return fmt.Errorf("failed to create merger for queue %s: %w", request.GetQueueName(), err) + return fmt.Errorf("failed to create lander for queue %s: %w", request.GetQueueName(), err) } - result, err := m.CheckMergeability(ctx, request) + result, err := m.CheckLandability(ctx, request) if err != nil { - if !merger.IsTerminal(err) { + if !lander.IsTerminal(err) { metrics.NamedCounter(c.metricsScope, opName, "check_errors", 1) - return fmt.Errorf("failed to check mergeability for %s: %w", request.GetId(), err) + return fmt.Errorf("failed to check landability for %s: %w", request.GetId(), err) } - if errors.Is(err, merger.ErrInvalidRequest) { + if errors.Is(err, lander.ErrInvalidRequest) { metrics.NamedCounter(c.metricsScope, opName, "invalid_requests", 1) - c.logger.Infow("invalid merge request", + c.logger.Infow("invalid land request", "id", request.GetId(), "queue_name", request.GetQueueName(), "err", err, ) } else { - metrics.NamedCounter(c.metricsScope, opName, "merge_conflicts", 1) - c.logger.Infow("merge conflict detected", + metrics.NamedCounter(c.metricsScope, opName, "land_conflicts", 1) + c.logger.Infow("land conflict detected", "id", request.GetId(), "queue_name", request.GetQueueName(), ) } - result = &runwaymq.MergeResult{ + result = &runwaymq.LandResult{ Id: request.GetId(), Outcome: runwaypb.Outcome_FAILED, Reason: err.Error(), @@ -132,23 +132,23 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er // queue without loading state first. result.QueueName = request.GetQueueName() - if err := c.publish(ctx, runwaymq.TopicKeyMergeConflictCheckSignal, result, msg.PartitionKey); err != nil { + if err := c.publish(ctx, runwaymq.TopicKeyLandConflictCheckSignal, result, msg.PartitionKey); err != nil { metrics.NamedCounter(c.metricsScope, opName, "publish_errors", 1) - return fmt.Errorf("failed to publish merge-conflict-check result for %s: %w", request.GetId(), err) + return fmt.Errorf("failed to publish land-conflict-check result for %s: %w", request.GetId(), err) } return nil } -// publish serializes a MergeResult and publishes it to the given signal topic. +// publish serializes a LandResult and publishes it to the given signal topic. // // The message ID is the correlation ID with no cause: a check is answered // once, so a redelivery that re-answers it is meant to dedup rather than tell // the caller twice. -func (c *Controller) publish(ctx context.Context, key consumer.TopicKey, result *runwaymq.MergeResult, partitionKey string) error { +func (c *Controller) publish(ctx context.Context, key consumer.TopicKey, result *runwaymq.LandResult, partitionKey string) error { payload, err := runwaymq.Marshal(result) if err != nil { - return fmt.Errorf("failed to serialize merge result: %w", err) + return fmt.Errorf("failed to serialize land result: %w", err) } if err := publish.Message(ctx, c.registry, key, publish.IntentID(result.GetId()), payload, partitionKey); err != nil { @@ -160,7 +160,7 @@ func (c *Controller) publish(ctx context.Context, key consumer.TopicKey, result // Name returns the controller name for logging and metrics. func (c *Controller) Name() string { - return "merge-conflict-check" + return "land-conflict-check" } // TopicKey returns the topic key this controller subscribes to. diff --git a/runway/controller/mergeconflictcheck/mergeconflictcheck_test.go b/runway/controller/landconflictcheck/landconflictcheck_test.go similarity index 67% rename from runway/controller/mergeconflictcheck/mergeconflictcheck_test.go rename to runway/controller/landconflictcheck/landconflictcheck_test.go index 13215239b..94b8ebe1c 100644 --- a/runway/controller/mergeconflictcheck/mergeconflictcheck_test.go +++ b/runway/controller/landconflictcheck/landconflictcheck_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package mergeconflictcheck +package landconflictcheck import ( "context" @@ -28,8 +28,8 @@ import ( "github.com/uber/submitqueue/platform/consumer" consumermock "github.com/uber/submitqueue/platform/consumer/mock" queuemock "github.com/uber/submitqueue/platform/extension/messagequeue/mock" - "github.com/uber/submitqueue/runway/extension/merger" - mergermock "github.com/uber/submitqueue/runway/extension/merger/mock" + "github.com/uber/submitqueue/runway/extension/lander" + landermock "github.com/uber/submitqueue/runway/extension/lander/mock" "go.uber.org/mock/gomock" "go.uber.org/zap/zaptest" ) @@ -49,7 +49,7 @@ func newDelivery(t *testing.T, ctrl *gomock.Controller, payload []byte) *consume return d } -func requestPayload(t *testing.T, req *runwaymq.MergeRequest) []byte { +func requestPayload(t *testing.T, req *runwaymq.LandRequest) []byte { t.Helper() payload, err := runwaymq.Marshal(req) require.NoError(t, err) @@ -69,40 +69,40 @@ func newRegistry(t *testing.T, ctrl *gomock.Controller, publishErr error) (consu q.EXPECT().Publisher().Return(pub).AnyTimes() registry, err := consumer.NewTopicRegistry([]consumer.TopicConfig{ - {Key: runwaymq.TopicKeyMergeConflictCheckSignal, Name: "merge-conflict-check-signal", Queue: q}, + {Key: runwaymq.TopicKeyLandConflictCheckSignal, Name: "land-conflict-check-signal", Queue: q}, }) require.NoError(t, err) return registry, pub } -func newController(t *testing.T, factory merger.Factory, registry consumer.TopicRegistry) *Controller { +func newController(t *testing.T, factory lander.Factory, registry consumer.TopicRegistry) *Controller { t.Helper() return NewController(Params{ Logger: zaptest.NewLogger(t).Sugar(), Scope: tally.NoopScope, - MergerFactory: factory, + LanderFactory: factory, Registry: registry, - TopicKey: runwaymq.TopicKeyMergeConflictCheck, - ConsumerGroup: "runway-mergeconflictcheck", + TopicKey: runwaymq.TopicKeyLandConflictCheck, + ConsumerGroup: "runway-landconflictcheck", }) } func TestNewController(t *testing.T) { ctrl := gomock.NewController(t) - factory := mergermock.NewMockFactory(ctrl) + factory := landermock.NewMockFactory(ctrl) registry, _ := newRegistry(t, ctrl, nil) controller := newController(t, factory, registry) require.NotNil(t, controller) - assert.Equal(t, runwaymq.TopicKeyMergeConflictCheck, controller.TopicKey()) - assert.Equal(t, "runway-mergeconflictcheck", controller.ConsumerGroup()) - assert.Equal(t, "merge-conflict-check", controller.Name()) + assert.Equal(t, runwaymq.TopicKeyLandConflictCheck, controller.TopicKey()) + assert.Equal(t, "runway-landconflictcheck", controller.ConsumerGroup()) + assert.Equal(t, "land-conflict-check", controller.Name()) } func TestProcess_Success(t *testing.T) { ctrl := gomock.NewController(t) - expectedResult := &runwaymq.MergeResult{ + expectedResult := &runwaymq.LandResult{ Id: testID, Outcome: runwaypb.Outcome_SUCCEEDED, Steps: []*runwaymq.StepResult{ @@ -110,11 +110,11 @@ func TestProcess_Success(t *testing.T) { }, } - m := mergermock.NewMockMerger(ctrl) - m.EXPECT().CheckMergeability(gomock.Any(), gomock.Any()).Return(expectedResult, nil) + m := landermock.NewMockLander(ctrl) + m.EXPECT().CheckLandability(gomock.Any(), gomock.Any()).Return(expectedResult, nil) - factory := mergermock.NewMockFactory(ctrl) - factory.EXPECT().For(merger.Config{QueueName: testQueue}).Return(m, nil) + factory := landermock.NewMockFactory(ctrl) + factory.EXPECT().For(lander.Config{QueueName: testQueue}).Return(m, nil) var gotTopic string var gotPayload []byte @@ -129,36 +129,36 @@ func TestProcess_Success(t *testing.T) { q := queuemock.NewMockQueue(ctrl) q.EXPECT().Publisher().Return(pub).AnyTimes() registry, err := consumer.NewTopicRegistry([]consumer.TopicConfig{ - {Key: runwaymq.TopicKeyMergeConflictCheckSignal, Name: "merge-conflict-check-signal", Queue: q}, + {Key: runwaymq.TopicKeyLandConflictCheckSignal, Name: "land-conflict-check-signal", Queue: q}, }) require.NoError(t, err) controller := newController(t, factory, registry) - req := &runwaymq.MergeRequest{ + req := &runwaymq.LandRequest{ Id: testID, QueueName: testQueue, - Steps: []*runwaymq.MergeStep{{StepId: "step-1"}}, + Steps: []*runwaymq.LandStep{{StepId: "step-1"}}, } delivery := newDelivery(t, ctrl, requestPayload(t, req)) require.NoError(t, controller.Process(context.Background(), delivery)) - assert.Equal(t, "merge-conflict-check-signal", gotTopic) - result := &runwaymq.MergeResult{} + assert.Equal(t, "land-conflict-check-signal", gotTopic) + result := &runwaymq.LandResult{} require.NoError(t, runwaymq.Unmarshal(gotPayload, result)) assert.Equal(t, testID, result.Id) assert.Equal(t, runwaypb.Outcome_SUCCEEDED, result.Outcome) } -func TestProcess_MergeConflict(t *testing.T) { +func TestProcess_LandConflict(t *testing.T) { ctrl := gomock.NewController(t) - m := mergermock.NewMockMerger(ctrl) - m.EXPECT().CheckMergeability(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("conflict in foo.go: %w", merger.ErrConflict)) + m := landermock.NewMockLander(ctrl) + m.EXPECT().CheckLandability(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("conflict in foo.go: %w", lander.ErrConflict)) - factory := mergermock.NewMockFactory(ctrl) - factory.EXPECT().For(merger.Config{QueueName: testQueue}).Return(m, nil) + factory := landermock.NewMockFactory(ctrl) + factory.EXPECT().For(lander.Config{QueueName: testQueue}).Return(m, nil) var gotTopic string var gotPayload []byte @@ -173,23 +173,23 @@ func TestProcess_MergeConflict(t *testing.T) { q := queuemock.NewMockQueue(ctrl) q.EXPECT().Publisher().Return(pub).AnyTimes() registry, err := consumer.NewTopicRegistry([]consumer.TopicConfig{ - {Key: runwaymq.TopicKeyMergeConflictCheckSignal, Name: "merge-conflict-check-signal", Queue: q}, + {Key: runwaymq.TopicKeyLandConflictCheckSignal, Name: "land-conflict-check-signal", Queue: q}, }) require.NoError(t, err) controller := newController(t, factory, registry) - req := &runwaymq.MergeRequest{ + req := &runwaymq.LandRequest{ Id: testID, QueueName: testQueue, - Steps: []*runwaymq.MergeStep{{StepId: "step-1"}}, + Steps: []*runwaymq.LandStep{{StepId: "step-1"}}, } delivery := newDelivery(t, ctrl, requestPayload(t, req)) require.NoError(t, controller.Process(context.Background(), delivery)) - assert.Equal(t, "merge-conflict-check-signal", gotTopic) - result := &runwaymq.MergeResult{} + assert.Equal(t, "land-conflict-check-signal", gotTopic) + result := &runwaymq.LandResult{} require.NoError(t, runwaymq.Unmarshal(gotPayload, result)) assert.Equal(t, testID, result.Id) assert.Equal(t, runwaypb.Outcome_FAILED, result.Outcome) @@ -199,11 +199,11 @@ func TestProcess_MergeConflict(t *testing.T) { func TestProcess_InvalidRequest(t *testing.T) { ctrl := gomock.NewController(t) - m := mergermock.NewMockMerger(ctrl) - m.EXPECT().CheckMergeability(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("bad strategy: %w", merger.ErrInvalidRequest)) + m := landermock.NewMockLander(ctrl) + m.EXPECT().CheckLandability(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("bad strategy: %w", lander.ErrInvalidRequest)) - factory := mergermock.NewMockFactory(ctrl) - factory.EXPECT().For(merger.Config{QueueName: testQueue}).Return(m, nil) + factory := landermock.NewMockFactory(ctrl) + factory.EXPECT().For(lander.Config{QueueName: testQueue}).Return(m, nil) var gotTopic string var gotPayload []byte @@ -218,45 +218,45 @@ func TestProcess_InvalidRequest(t *testing.T) { q := queuemock.NewMockQueue(ctrl) q.EXPECT().Publisher().Return(pub).AnyTimes() registry, err := consumer.NewTopicRegistry([]consumer.TopicConfig{ - {Key: runwaymq.TopicKeyMergeConflictCheckSignal, Name: "merge-conflict-check-signal", Queue: q}, + {Key: runwaymq.TopicKeyLandConflictCheckSignal, Name: "land-conflict-check-signal", Queue: q}, }) require.NoError(t, err) controller := newController(t, factory, registry) - req := &runwaymq.MergeRequest{ + req := &runwaymq.LandRequest{ Id: testID, QueueName: testQueue, - Steps: []*runwaymq.MergeStep{{StepId: "step-1"}}, + Steps: []*runwaymq.LandStep{{StepId: "step-1"}}, } delivery := newDelivery(t, ctrl, requestPayload(t, req)) require.NoError(t, controller.Process(context.Background(), delivery)) - assert.Equal(t, "merge-conflict-check-signal", gotTopic) - result := &runwaymq.MergeResult{} + assert.Equal(t, "land-conflict-check-signal", gotTopic) + result := &runwaymq.LandResult{} require.NoError(t, runwaymq.Unmarshal(gotPayload, result)) assert.Equal(t, testID, result.Id) assert.Equal(t, runwaypb.Outcome_FAILED, result.Outcome) assert.NotEmpty(t, result.Reason) } -func TestProcess_MergerInfraError(t *testing.T) { +func TestProcess_LanderInfraError(t *testing.T) { ctrl := gomock.NewController(t) - m := mergermock.NewMockMerger(ctrl) - m.EXPECT().CheckMergeability(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("git timeout")) + m := landermock.NewMockLander(ctrl) + m.EXPECT().CheckLandability(gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("git timeout")) - factory := mergermock.NewMockFactory(ctrl) - factory.EXPECT().For(merger.Config{QueueName: testQueue}).Return(m, nil) + factory := landermock.NewMockFactory(ctrl) + factory.EXPECT().For(lander.Config{QueueName: testQueue}).Return(m, nil) registry, _ := newRegistry(t, ctrl, nil) controller := newController(t, factory, registry) - req := &runwaymq.MergeRequest{ + req := &runwaymq.LandRequest{ Id: testID, QueueName: testQueue, - Steps: []*runwaymq.MergeStep{{StepId: "step-1"}}, + Steps: []*runwaymq.LandStep{{StepId: "step-1"}}, } delivery := newDelivery(t, ctrl, requestPayload(t, req)) @@ -267,16 +267,16 @@ func TestProcess_MergerInfraError(t *testing.T) { func TestProcess_FactoryError(t *testing.T) { ctrl := gomock.NewController(t) - factory := mergermock.NewMockFactory(ctrl) - factory.EXPECT().For(merger.Config{QueueName: testQueue}).Return(nil, fmt.Errorf("unknown queue")) + factory := landermock.NewMockFactory(ctrl) + factory.EXPECT().For(lander.Config{QueueName: testQueue}).Return(nil, fmt.Errorf("unknown queue")) registry, _ := newRegistry(t, ctrl, nil) controller := newController(t, factory, registry) - req := &runwaymq.MergeRequest{ + req := &runwaymq.LandRequest{ Id: testID, QueueName: testQueue, - Steps: []*runwaymq.MergeStep{{StepId: "step-1"}}, + Steps: []*runwaymq.LandStep{{StepId: "step-1"}}, } delivery := newDelivery(t, ctrl, requestPayload(t, req)) @@ -287,24 +287,24 @@ func TestProcess_FactoryError(t *testing.T) { func TestProcess_PublishError(t *testing.T) { ctrl := gomock.NewController(t) - expectedResult := &runwaymq.MergeResult{ + expectedResult := &runwaymq.LandResult{ Id: testID, Outcome: runwaypb.Outcome_SUCCEEDED, } - m := mergermock.NewMockMerger(ctrl) - m.EXPECT().CheckMergeability(gomock.Any(), gomock.Any()).Return(expectedResult, nil) + m := landermock.NewMockLander(ctrl) + m.EXPECT().CheckLandability(gomock.Any(), gomock.Any()).Return(expectedResult, nil) - factory := mergermock.NewMockFactory(ctrl) - factory.EXPECT().For(merger.Config{QueueName: testQueue}).Return(m, nil) + factory := landermock.NewMockFactory(ctrl) + factory.EXPECT().For(lander.Config{QueueName: testQueue}).Return(m, nil) registry, _ := newRegistry(t, ctrl, fmt.Errorf("publish failed")) controller := newController(t, factory, registry) - req := &runwaymq.MergeRequest{ + req := &runwaymq.LandRequest{ Id: testID, QueueName: testQueue, - Steps: []*runwaymq.MergeStep{{StepId: "step-1"}}, + Steps: []*runwaymq.LandStep{{StepId: "step-1"}}, } delivery := newDelivery(t, ctrl, requestPayload(t, req)) @@ -314,7 +314,7 @@ func TestProcess_PublishError(t *testing.T) { func TestProcess_DeserializeError(t *testing.T) { ctrl := gomock.NewController(t) - factory := mergermock.NewMockFactory(ctrl) + factory := landermock.NewMockFactory(ctrl) registry, _ := newRegistry(t, ctrl, nil) controller := newController(t, factory, registry) diff --git a/runway/extension/merger/BUILD.bazel b/runway/extension/lander/BUILD.bazel similarity index 66% rename from runway/extension/merger/BUILD.bazel rename to runway/extension/lander/BUILD.bazel index e84294608..df7caa35f 100644 --- a/runway/extension/merger/BUILD.bazel +++ b/runway/extension/lander/BUILD.bazel @@ -2,8 +2,8 @@ load("@rules_go//go:def.bzl", "go_library") go_library( name = "go_default_library", - srcs = ["merger.go"], - importpath = "github.com/uber/submitqueue/runway/extension/merger", + srcs = ["lander.go"], + importpath = "github.com/uber/submitqueue/runway/extension/lander", visibility = ["//visibility:public"], deps = ["//api/runway/messagequeue:go_default_library"], ) diff --git a/runway/extension/merger/fake/BUILD.bazel b/runway/extension/lander/fake/BUILD.bazel similarity index 76% rename from runway/extension/merger/fake/BUILD.bazel rename to runway/extension/lander/fake/BUILD.bazel index 4f3f61c8f..387d2565c 100644 --- a/runway/extension/merger/fake/BUILD.bazel +++ b/runway/extension/lander/fake/BUILD.bazel @@ -3,13 +3,13 @@ load("@rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", srcs = ["fake.go"], - importpath = "github.com/uber/submitqueue/runway/extension/merger/fake", + importpath = "github.com/uber/submitqueue/runway/extension/lander/fake", visibility = ["//visibility:public"], deps = [ "//api/runway/messagequeue:go_default_library", "//api/runway/messagequeue/protopb:go_default_library", "//platform/fakemarker:go_default_library", - "//runway/extension/merger:go_default_library", + "//runway/extension/lander:go_default_library", ], ) @@ -19,10 +19,10 @@ go_test( embed = [":go_default_library"], deps = [ "//api/base/change/protopb:go_default_library", - "//api/base/mergestrategy/protopb:go_default_library", + "//api/base/landstrategy/protopb:go_default_library", "//api/runway/messagequeue:go_default_library", "//api/runway/messagequeue/protopb:go_default_library", - "//runway/extension/merger:go_default_library", + "//runway/extension/lander:go_default_library", "@com_github_stretchr_testify//assert:go_default_library", "@com_github_stretchr_testify//require:go_default_library", ], diff --git a/runway/extension/merger/fake/fake.go b/runway/extension/lander/fake/fake.go similarity index 64% rename from runway/extension/merger/fake/fake.go rename to runway/extension/lander/fake/fake.go index 6dc2f482c..080886f70 100644 --- a/runway/extension/merger/fake/fake.go +++ b/runway/extension/lander/fake/fake.go @@ -12,15 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package fake provides a merger.Merger whose outcome is driven by the request -// payload. With no marker it succeeds like the noop merger, behaving as a +// Package fake provides a lander.Lander whose outcome is driven by the request +// payload. With no marker it succeeds like the noop lander, behaving as a // best-case stub for wiring and baselines. A failure can be injected end-to-end -// (e.g. from an e2e merge request) by embedding a marker token in a change URI +// (e.g. from an e2e land request) by embedding a marker token in a change URI // of the form "sq-fake=": // -// sq-fake=merge-conflict -> merger.ErrConflict -// sq-fake=merge-invalid -> merger.ErrInvalidRequest -// sq-fake=merge-error -> a plain (non-retryable) error +// sq-fake=land-conflict -> lander.ErrConflict +// sq-fake=land-invalid -> lander.ErrInvalidRequest +// sq-fake=land-error -> a plain (non-retryable) error // // The first token found across the request's steps, in order, decides the // outcome for the whole request. This lets a single running stack exercise @@ -36,40 +36,40 @@ import ( runwaymq "github.com/uber/submitqueue/api/runway/messagequeue" runwaypb "github.com/uber/submitqueue/api/runway/messagequeue/protopb" "github.com/uber/submitqueue/platform/fakemarker" - "github.com/uber/submitqueue/runway/extension/merger" + "github.com/uber/submitqueue/runway/extension/lander" ) // Recognized marker tokens. See the package doc for the convention. const ( - tokenConflict = "merge-conflict" - tokenInvalid = "merge-invalid" - tokenError = "merge-error" + tokenConflict = "land-conflict" + tokenInvalid = "land-invalid" + tokenError = "land-error" ) -var _ merger.Merger = (*Merger)(nil) +var _ lander.Lander = (*Lander)(nil) -// Merger is a Merger that succeeds unless a marker token in a change URI +// Lander is a Lander that succeeds unless a marker token in a change URI // requests otherwise. -type Merger struct { - // cfg is the per-queue identity this merger was built for. - cfg merger.Config - // seq mints the synthetic revision ids returned by Merge. It is supplied by - // the caller so a factory that builds one Merger per queue can still hand +type Lander struct { + // cfg is the per-queue identity this lander was built for. + cfg lander.Config + // seq mints the synthetic revision ids returned by Land. It is supplied by + // the caller so a factory that builds one Lander per queue can still hand // out ids that are unique across every queue in the process. seq *atomic.Uint64 } -// New returns a Merger bound to the queue named in cfg that defaults to success +// New returns a Lander bound to the queue named in cfg that defaults to success // and honors marker tokens embedded in change URIs. seq must be non-nil and is -// shared, not owned: callers minting ids from more than one Merger must pass the +// shared, not owned: callers minting ids from more than one Lander must pass the // same counter to each. -func New(cfg merger.Config, seq *atomic.Uint64) *Merger { - return &Merger{cfg: cfg, seq: seq} +func New(cfg lander.Config, seq *atomic.Uint64) *Lander { + return &Lander{cfg: cfg, seq: seq} } -// CheckMergeability reports the request as mergeable unless a recognized marker +// CheckLandability reports the request as landable unless a recognized marker // token asks for a failure. Outputs are empty, as for any dry run. -func (m *Merger) CheckMergeability(_ context.Context, req *runwaymq.MergeRequest) (*runwaymq.MergeResult, error) { +func (m *Lander) CheckLandability(_ context.Context, req *runwaymq.LandRequest) (*runwaymq.LandResult, error) { if err := injectedFailure(req); err != nil { return nil, err } @@ -78,16 +78,16 @@ func (m *Merger) CheckMergeability(_ context.Context, req *runwaymq.MergeRequest for i, s := range req.GetSteps() { steps[i] = &runwaymq.StepResult{StepId: s.GetStepId()} } - return &runwaymq.MergeResult{ + return &runwaymq.LandResult{ Id: req.GetId(), Outcome: runwaypb.Outcome_SUCCEEDED, Steps: steps, }, nil } -// Merge reports the request as merged unless a recognized marker token asks for +// Land reports the request as landed unless a recognized marker token asks for // a failure, producing one synthetic revision id per step. -func (m *Merger) Merge(_ context.Context, req *runwaymq.MergeRequest) (*runwaymq.MergeResult, error) { +func (m *Lander) Land(_ context.Context, req *runwaymq.LandRequest) (*runwaymq.LandResult, error) { if err := injectedFailure(req); err != nil { return nil, err } @@ -100,7 +100,7 @@ func (m *Merger) Merge(_ context.Context, req *runwaymq.MergeRequest) (*runwaymq Outputs: []*runwaymq.StepOutput{{Id: fmt.Sprintf("%040x", n)}}, } } - return &runwaymq.MergeResult{ + return &runwaymq.LandResult{ Id: req.GetId(), Outcome: runwaypb.Outcome_SUCCEEDED, Steps: steps, @@ -109,13 +109,13 @@ func (m *Merger) Merge(_ context.Context, req *runwaymq.MergeRequest) (*runwaymq // injectedFailure returns the error the request's marker token asks for, or nil // when no step carries a recognized token. -func injectedFailure(req *runwaymq.MergeRequest) error { +func injectedFailure(req *runwaymq.LandRequest) error { for _, s := range req.GetSteps() { switch fakemarker.Token(s.GetChange().GetUris()) { case tokenConflict: - return fmt.Errorf("fake: marked conflicting on step %s: %w", s.GetStepId(), merger.ErrConflict) + return fmt.Errorf("fake: marked conflicting on step %s: %w", s.GetStepId(), lander.ErrConflict) case tokenInvalid: - return fmt.Errorf("fake: marked invalid on step %s: %w", s.GetStepId(), merger.ErrInvalidRequest) + return fmt.Errorf("fake: marked invalid on step %s: %w", s.GetStepId(), lander.ErrInvalidRequest) case tokenError: return fmt.Errorf("fake: marked failing on step %s", s.GetStepId()) } diff --git a/runway/extension/merger/fake/fake_test.go b/runway/extension/lander/fake/fake_test.go similarity index 71% rename from runway/extension/merger/fake/fake_test.go rename to runway/extension/lander/fake/fake_test.go index 0bdf25156..0762e0854 100644 --- a/runway/extension/merger/fake/fake_test.go +++ b/runway/extension/lander/fake/fake_test.go @@ -22,24 +22,24 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" changepb "github.com/uber/submitqueue/api/base/change/protopb" - strategypb "github.com/uber/submitqueue/api/base/mergestrategy/protopb" + strategypb "github.com/uber/submitqueue/api/base/landstrategy/protopb" runwaymq "github.com/uber/submitqueue/api/runway/messagequeue" runwaypb "github.com/uber/submitqueue/api/runway/messagequeue/protopb" - "github.com/uber/submitqueue/runway/extension/merger" + "github.com/uber/submitqueue/runway/extension/lander" ) // testCfg is the per-queue identity used by every case in this file. -var testCfg = merger.Config{QueueName: "test-queue"} +var testCfg = lander.Config{QueueName: "test-queue"} const baseURI = "github://github.example.com/uber/repo/pull/1/abcdef0123456789abcdef0123456789abcdef01" // requestWith builds a two-step request whose second step carries the given // URIs, so tests can prove the token is found on any step, not just the first. -func requestWith(uris ...string) *runwaymq.MergeRequest { - return &runwaymq.MergeRequest{ +func requestWith(uris ...string) *runwaymq.LandRequest { + return &runwaymq.LandRequest{ Id: "queue-a/42", QueueName: "queue-a", - Steps: []*runwaymq.MergeStep{ + Steps: []*runwaymq.LandStep{ { StepId: "queue-a/1", Change: &changepb.Change{Uris: []string{baseURI}}, @@ -57,8 +57,8 @@ func requestWith(uris ...string) *runwaymq.MergeRequest { func TestUnmarkedRequestSucceeds(t *testing.T) { req := requestWith(baseURI) - t.Run("check mergeability reports no outputs", func(t *testing.T) { - res, err := New(testCfg, new(atomic.Uint64)).CheckMergeability(context.Background(), req) + t.Run("check landability reports no outputs", func(t *testing.T) { + res, err := New(testCfg, new(atomic.Uint64)).CheckLandability(context.Background(), req) require.NoError(t, err) assert.Equal(t, req.GetId(), res.GetId()) @@ -70,8 +70,8 @@ func TestUnmarkedRequestSucceeds(t *testing.T) { assert.Empty(t, res.GetSteps()[1].GetOutputs()) }) - t.Run("merge reports one output per step", func(t *testing.T) { - res, err := New(testCfg, new(atomic.Uint64)).Merge(context.Background(), req) + t.Run("land reports one output per step", func(t *testing.T) { + res, err := New(testCfg, new(atomic.Uint64)).Land(context.Background(), req) require.NoError(t, err) assert.Equal(t, req.GetId(), res.GetId()) @@ -92,8 +92,8 @@ func TestMarkedRequestFails(t *testing.T) { token string want error // nil means "an error that is neither terminal sentinel" }{ - {name: "conflict", token: tokenConflict, want: merger.ErrConflict}, - {name: "invalid", token: tokenInvalid, want: merger.ErrInvalidRequest}, + {name: "conflict", token: tokenConflict, want: lander.ErrConflict}, + {name: "invalid", token: tokenInvalid, want: lander.ErrInvalidRequest}, {name: "plain error", token: tokenError, want: nil}, } @@ -103,13 +103,13 @@ func TestMarkedRequestFails(t *testing.T) { for _, call := range []struct { name string - fn func(*runwaymq.MergeRequest) (*runwaymq.MergeResult, error) + fn func(*runwaymq.LandRequest) (*runwaymq.LandResult, error) }{ - {"CheckMergeability", func(r *runwaymq.MergeRequest) (*runwaymq.MergeResult, error) { - return New(testCfg, new(atomic.Uint64)).CheckMergeability(context.Background(), r) + {"CheckLandability", func(r *runwaymq.LandRequest) (*runwaymq.LandResult, error) { + return New(testCfg, new(atomic.Uint64)).CheckLandability(context.Background(), r) }}, - {"Merge", func(r *runwaymq.MergeRequest) (*runwaymq.MergeResult, error) { - return New(testCfg, new(atomic.Uint64)).Merge(context.Background(), r) + {"Land", func(r *runwaymq.LandRequest) (*runwaymq.LandResult, error) { + return New(testCfg, new(atomic.Uint64)).Land(context.Background(), r) }}, } { t.Run(call.name, func(t *testing.T) { @@ -119,10 +119,10 @@ func TestMarkedRequestFails(t *testing.T) { if tt.want != nil { assert.ErrorIs(t, err, tt.want) - assert.True(t, merger.IsTerminal(err), "sentinel failures must be terminal") + assert.True(t, lander.IsTerminal(err), "sentinel failures must be terminal") return } - assert.False(t, merger.IsTerminal(err), + assert.False(t, lander.IsTerminal(err), "an unmarked failure must not look terminal, so it dead-letters") }) } @@ -133,23 +133,23 @@ func TestMarkedRequestFails(t *testing.T) { func TestUnrecognizedTokenSucceeds(t *testing.T) { req := requestWith(baseURI + "?sq-fake=some-other-fakes-token") - res, err := New(testCfg, new(atomic.Uint64)).Merge(context.Background(), req) + res, err := New(testCfg, new(atomic.Uint64)).Land(context.Background(), req) require.NoError(t, err) assert.Equal(t, runwaypb.Outcome_SUCCEEDED, res.GetOutcome()) } func TestFirstRecognizedTokenWins(t *testing.T) { - req := &runwaymq.MergeRequest{ + req := &runwaymq.LandRequest{ Id: "queue-a/42", QueueName: "queue-a", - Steps: []*runwaymq.MergeStep{ + Steps: []*runwaymq.LandStep{ {StepId: "queue-a/1", Change: &changepb.Change{Uris: []string{baseURI + "?sq-fake=" + tokenConflict}}}, {StepId: "queue-a/2", Change: &changepb.Change{Uris: []string{baseURI + "?sq-fake=" + tokenInvalid}}}, }, } - _, err := New(testCfg, new(atomic.Uint64)).Merge(context.Background(), req) + _, err := New(testCfg, new(atomic.Uint64)).Land(context.Background(), req) require.Error(t, err) - assert.ErrorIs(t, err, merger.ErrConflict) - assert.NotErrorIs(t, err, merger.ErrInvalidRequest) + assert.ErrorIs(t, err, lander.ErrConflict) + assert.NotErrorIs(t, err, lander.ErrInvalidRequest) } diff --git a/runway/extension/merger/git/BUILD.bazel b/runway/extension/lander/git/BUILD.bazel similarity index 82% rename from runway/extension/merger/git/BUILD.bazel rename to runway/extension/lander/git/BUILD.bazel index 9dbc3feb6..55e620865 100644 --- a/runway/extension/merger/git/BUILD.bazel +++ b/runway/extension/lander/git/BUILD.bazel @@ -5,21 +5,21 @@ go_library( srcs = [ "author.go", "changeref.go", - "git_merger.go", + "git_lander.go", "headbranch.go", "objects.go", ], - importpath = "github.com/uber/submitqueue/runway/extension/merger/git", + importpath = "github.com/uber/submitqueue/runway/extension/lander/git", visibility = ["//visibility:public"], deps = [ - "//api/base/mergestrategy/protopb:go_default_library", + "//api/base/landstrategy/protopb:go_default_library", "//api/runway/messagequeue:go_default_library", "//api/runway/messagequeue/protopb:go_default_library", "//platform/base/change/git:go_default_library", "//platform/base/change/github:go_default_library", "//platform/git/exec:go_default_library", "//platform/metrics:go_default_library", - "//runway/extension/merger:go_default_library", + "//runway/extension/lander:go_default_library", "@com_github_uber_go_tally//:go_default_library", "@org_uber_go_zap//:go_default_library", ], @@ -28,7 +28,7 @@ go_library( go_test( name = "go_default_test", srcs = [ - "git_merger_test.go", + "git_lander_test.go", "headbranch_test.go", ], data = [ @@ -46,11 +46,11 @@ go_test( }, deps = [ "//api/base/change/protopb:go_default_library", - "//api/base/mergestrategy/protopb:go_default_library", + "//api/base/landstrategy/protopb:go_default_library", "//api/runway/messagequeue:go_default_library", "//api/runway/messagequeue/protopb:go_default_library", "//platform/git/exectest:go_default_library", - "//runway/extension/merger:go_default_library", + "//runway/extension/lander: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", diff --git a/runway/extension/merger/git/README.md b/runway/extension/lander/git/README.md similarity index 73% rename from runway/extension/merger/git/README.md rename to runway/extension/lander/git/README.md index 08241732c..a5744ed6f 100644 --- a/runway/extension/merger/git/README.md +++ b/runway/extension/lander/git/README.md @@ -1,6 +1,6 @@ -# git merger +# git lander -A `merger.Merger` backed by the `git` CLI operating on a local checkout. It applies a merge request's ordered steps onto a target branch, honoring each step's strategy, and — for a committing merge — pushes the result. It is constructed by the wiring layer (see [`service/runway`](../../../../service/runway)) with the checkout path, remote, target branch, pinned git runtime, committer identity, and the default strategy; the request itself carries only a queue name and the changes, so the merger reads change URIs straight from the payload (no store, no resolver). +A `lander.Lander` backed by the `git` CLI operating on a local checkout. It applies a land request's ordered steps onto a target branch, honoring each step's strategy, and — for a committing land — pushes the result. It is constructed by the wiring layer (see [`service/runway`](../../../../service/runway)) with the checkout path, remote, target branch, pinned git runtime, committer identity, and the default strategy; the request itself carries only a queue name and the changes, so the lander reads change URIs straight from the payload (no store, no resolver). ## Model @@ -23,11 +23,11 @@ An unrecognized scheme is a terminal invalid request. ## What a request must agree on -The supported providers are a property of this merger, not of the queue or of the wire contract: the URI scheme selects the parser, and a scheme with no case is a terminal invalid request. Nothing upstream filters on it, so an unsupported provider is first refused here. +The supported providers are a property of this lander, not of the queue or of the wire contract: the URI scheme selects the parser, and a scheme with no case is a terminal invalid request. Nothing upstream filters on it, so an unsupported provider is first refused here. -Beyond the scheme, every change in one request must come from the same provider. There is no sense in one merge being addressed through two of them, and the check runs before any git command, so an incoherent request costs nothing and leaves the checkout untouched. +Beyond the scheme, every change in one request must come from the same provider. There is no sense in one land being addressed through two of them, and the check runs before any git command, so an incoherent request costs nothing and leaves the checkout untouched. -Whether a change actually belongs to the repository this merger serves is not checked here — the merger is constrained to its checkout and remote by configuration, and a change it cannot fetch is refused on those grounds. +Whether a change actually belongs to the repository this lander serves is not checked here — the lander is constrained to its checkout and remote by configuration, and a change it cannot fetch is refused on those grounds. ## Object availability @@ -41,7 +41,7 @@ The same line is drawn inside an apply: a non-zero exit from git is not by itsel ## Staleness -Fetching by SHA guarantees the merger applies exactly the commit a URI names — not that the commit is still the change's head. A force-push leaves the superseded commit fetchable for some time on most hosts, so a successful fetch says nothing about freshness. When enabled, each change's canonical ref is read (one ref advertisement, no object transfer) and a mismatch is terminal. A ref that no longer exists yields no verdict. +Fetching by SHA guarantees the lander applies exactly the commit a URI names — not that the commit is still the change's head. A force-push leaves the superseded commit fetchable for some time on most hosts, so a successful fetch says nothing about freshness. When enabled, each change's canonical ref is read (one ref advertisement, no object transfer) and a mismatch is terminal. A ref that no longer exists yields no verdict. ## Strategies @@ -57,11 +57,11 @@ Fetching by SHA guarantees the merger applies exactly the commit a URI names — ## Authorship -Git records an author and a committer separately, and the merger keeps them apart: the committer is always the merger's configured identity, because it is what applied the change, while the author is the person who wrote it. +Git records an author and a committer separately, and the lander keeps them apart: the committer is always the lander's configured identity, because it is what applied the change, while the author is the person who wrote it. `REBASE` gets this for free — cherry-pick carries each commit's author across, so every landed commit keeps whoever wrote it. The strategies that mint a fresh commit do not: the squash commit and the `--no-ff` merge commit are new objects, and without attribution both would be credited to the service. Each is therefore authored as the author recorded on the commit its change's URI pins. For a change spanning several commits by different people, that is the head commit's author — the one identity the request actually names. -The author is read out of the local object store, so this costs no network and needs nothing on the wire: every referenced commit is already fetched and verified before a step is applied. A commit that records no usable author (either half missing) falls back to the committer identity rather than failing the merge. +The author is read out of the local object store, so this costs no network and needs nothing on the wire: every referenced commit is already fetched and verified before a step is applied. A commit that records no usable author (either half missing) falls back to the committer identity rather than failing the land. The author travels to git through `GIT_AUTHOR_NAME`/`GIT_AUTHOR_EMAIL` rather than `git commit --author`, because `git merge` has no `--author` flag and because the environment keeps the name and address as separate values — a single `Name
` string has to be parsed back apart, which a display name containing an angle bracket breaks. @@ -71,15 +71,15 @@ A repository migration arrives as an ordinary change in the target repo whose br `MERGE` is the only strategy that can serve this, because it is the only one that leaves the imported commits reachable under their original hashes — the picking strategies would rewrite every one of them, and have no range to compute in the first place, so they reject such a change outright. -The refusal is lifted by a per-instance option rather than always: it is a real safeguard, and without it a merge of the wrong object fails loudly instead of quietly producing a nonsense result. A queue that exists to perform imports turns it on. A refusal that surfaces without the option is reported as an invalid request, not as a conflict — nothing collided. +The refusal is lifted by a per-instance option rather than always: it is a real safeguard, and without it a land of the wrong object fails loudly instead of quietly producing a nonsense result. A queue that exists to perform imports turns it on. A refusal that surfaces without the option is reported as an invalid request, not as a conflict — nothing collided. -Redelivery is safe: once imported, the source head is contained in the target, so the change is skipped rather than merged twice. +Redelivery is safe: once imported, the source head is contained in the target, so the change is skipped rather than landed twice. ## Committing, dry-run, atomicity, contention -`Merge` commits and reports outputs; `CheckMergeability` runs the identical apply but never pushes, then resets the checkout to discard the local commits and reports empty outputs. A multi-step check commits its intermediate steps locally so it sees the same conflict surface a real merge would. +`Land` commits and reports outputs; `CheckLandability` runs the identical apply but never pushes, then resets the checkout to discard the local commits and reports empty outputs. A multi-step check commits its intermediate steps locally so it sees the same conflict surface a real land would. -For a committing merge nothing reaches the remote until every step has applied cleanly (a `PROMOTE` is itself a single atomic fast-forward ref update). A step that fails to apply aborts its in-progress git operation and returns without pushing. With head-branch updates enabled a merge writes two things rather than one — the head branches first, then the target — and only the target's push is the point of no return. If the push fails because the remote tip moved between reset and push, the whole reset/apply/push cycle is retried up to a bounded number of attempts; detection re-fetches the tip and compares it to the SHA the cycle was based on. +For a committing land nothing reaches the remote until every step has applied cleanly (a `PROMOTE` is itself a single atomic fast-forward ref update). A step that fails to apply aborts its in-progress git operation and returns without pushing. With head-branch updates enabled a land writes two things rather than one — the head branches first, then the target — and only the target's push is the point of no return. If the push fails because the remote tip moved between reset and push, the whole reset/apply/push cycle is retried up to a bounded number of attempts; detection re-fetches the tip and compares it to the SHA the cycle was based on. ## Head branches @@ -87,19 +87,19 @@ A provider decides whether a change merged while it processes the push to the ta Enabling head-branch updates closes that gap. Before the target is pushed, each change's head branch is moved to the commit that change became — its last replayed commit under `REBASE`, its single squashed commit under `SQUASH_REBASE`. The provider records that new head, and when the target push arrives moments later it finds exactly that commit reachable, so it marks the change merged. Nothing here knows what a pull request is: the branch is found by matching the change's pinned head SHA against the remote's branch tips, so the same mechanism serves a GitHub pull request, a GitLab merge request, or a bare branch. -**The ordering is the mechanism.** Moving the head branch *after* the target has been pushed leaves the provider comparing against the pre-merge head at the only moment it looks, and it records the change closed rather than merged — even though the branch ends up on a commit that is demonstrably in the target. Doing both in a single atomic push behaves the same way, since the provider still evaluates the target update against the head it had recorded beforehand. Only a separate, earlier push works. +**The ordering is the mechanism.** Moving the head branch *after* the target has been pushed leaves the provider comparing against the pre-land head at the only moment it looks, and it records the change closed rather than merged — even though the branch ends up on a commit that is demonstrably in the target. Doing both in a single atomic push behaves the same way, since the provider still evaluates the target update against the head it had recorded beforehand. Only a separate, earlier push works. -Three cases are declined rather than guessed at. A change whose head matches **no branch** on this remote is normally one proposed from a fork, whose branch lives in another repository and is not this merger's to move — such a change lands normally. A head matching **several branches** is ambiguous, and the URI does not say which one the change was proposed from, so rewriting a guess risks clobbering an unrelated branch. The **target branch itself** is never a candidate, so a change whose head coincides with the target tip cannot make the merger rewrite the branch it just landed on. +Three cases are declined rather than guessed at. A change whose head matches **no branch** on this remote is normally one proposed from a fork, whose branch lives in another repository and is not this lander's to move — such a change lands normally. A head matching **several branches** is ambiguous, and the URI does not say which one the change was proposed from, so rewriting a guess risks clobbering an unrelated branch. The **target branch itself** is never a candidate, so a change whose head coincides with the target tip cannot make the lander rewrite the branch it just landed on. -Each push carries a lease against the SHA the change's URI pinned, so an author who pushes in the window between reading the remote's branches and updating them fails the lease instead of losing their work. A failure to move a branch fails the merge, before the target is pushed: landing a change while knowing its head could not be moved produces exactly the half-merged state the option exists to prevent. The three declined cases above are not failures and do not stop the merge. +Each push carries a lease against the SHA the change's URI pinned, so an author who pushes in the window between reading the remote's branches and updating them fails the lease instead of losing their work. A failure to move a branch fails the land, before the target is pushed: landing a change while knowing its head could not be moved produces exactly the half-landed state the option exists to prevent. The three declined cases above are not failures and do not stop the land. A branch a failed attempt already moved is remembered for the next one. Once moved, it no longer sits at the SHA the URI pinned, so a retry could not find it by matching tips and would strand it on a commit that never landed; the attempt's resolved branch and the value the next lease must name are carried forward instead. -Off by default — moving a branch the merger was not asked to move is a surprise unless a deployment opted in. +Off by default — moving a branch the lander was not asked to move is a surprise unless a deployment opted in. ## Failure classification -A merge conflict surfaces as `merger.ErrConflict`. An unusable request surfaces as `merger.ErrInvalidRequest`: an unsupported strategy or URI scheme, a malformed URI, an invalid `PROMOTE` composition, a commit a reachable remote cannot supply, a change whose head has moved on, or a change sharing no history with the target under a picking strategy. Both are terminal — the controller publishes a `FAILED` result rather than retrying. Everything else (network/auth/push faults, and an unreachable remote) is returned as a plain error for the consumer to retry. +A land conflict surfaces as `lander.ErrConflict`. An unusable request surfaces as `lander.ErrInvalidRequest`: an unsupported strategy or URI scheme, a malformed URI, an invalid `PROMOTE` composition, a commit a reachable remote cannot supply, a change whose head has moved on, or a change sharing no history with the target under a picking strategy. Both are terminal — the controller publishes a `FAILED` result rather than retrying. Everything else (network/auth/push faults, and an unreachable remote) is returned as a plain error for the consumer to retry. The distinction between the last two matters operationally: a commit that is missing while the remote answers is a property of the request, whereas a remote that will not answer is a property of the moment. diff --git a/runway/extension/merger/git/author.go b/runway/extension/lander/git/author.go similarity index 92% rename from runway/extension/merger/git/author.go rename to runway/extension/lander/git/author.go index 408effb95..2ad378465 100644 --- a/runway/extension/merger/git/author.go +++ b/runway/extension/lander/git/author.go @@ -23,8 +23,8 @@ import ( // authorIdent is the person a commit is credited to, as distinct from the // committer that produced it. // -// Git records the two separately and the merger keeps them separate: the -// merger is always the committer, because it is what applied the change, while +// Git records the two separately and the lander keeps them separate: the +// lander is always the committer, because it is what applied the change, while // the author is carried over from the change itself. A strategy that mints a // fresh commit would otherwise credit the service for work someone else wrote. type authorIdent struct { @@ -69,14 +69,14 @@ func (a authorIdent) env() []string { // commitAuthor reads the author recorded on a commit. // // The commit is the one the change's URI pins, so the attribution follows what -// the request names rather than anything the merger infers. It is already +// the request names rather than anything the lander infers. It is already // present locally: every referenced commit is fetched and verified before a // step is applied, so this reads from the object store and costs no network. // // A commit with no readable author is not an error. It yields an incomplete // identity, and the caller falls back to the committer identity rather than -// failing a merge over attribution. -func (m *gitMerger) commitAuthor(ctx context.Context, sha string) (authorIdent, error) { +// failing the land over attribution. +func (m *gitLander) commitAuthor(ctx context.Context, sha string) (authorIdent, error) { // NUL-separated: a display name may contain spaces, angle brackets, quotes // or anything else a person put in their git config, and none of those can // be mistaken for the field separator. diff --git a/runway/extension/merger/git/changeref.go b/runway/extension/lander/git/changeref.go similarity index 89% rename from runway/extension/merger/git/changeref.go rename to runway/extension/lander/git/changeref.go index fcd19a9e8..939ee4429 100644 --- a/runway/extension/merger/git/changeref.go +++ b/runway/extension/lander/git/changeref.go @@ -20,10 +20,10 @@ import ( entitygit "github.com/uber/submitqueue/platform/base/change/git" entitygithub "github.com/uber/submitqueue/platform/base/change/github" - "github.com/uber/submitqueue/runway/extension/merger" + "github.com/uber/submitqueue/runway/extension/lander" ) -// changeRef is everything the merger needs from a change URI, reduced to a +// changeRef is everything the lander needs from a change URI, reduced to a // form that does not depend on which provider minted it. Adding a provider // means adding one case to resolveChange, not touching the apply paths. type changeRef struct { @@ -39,23 +39,23 @@ type changeRef struct { // which case both are skipped. Ref string // Label is a short human-readable identifier for the change, used in - // commit messages the merger synthesizes. + // commit messages the lander synthesizes. Label string } -// resolveChange maps a change URI onto the merger's provider-neutral view of +// resolveChange maps a change URI onto the lander's provider-neutral view of // it. An unrecognized scheme is terminal: no retry produces a parser for it. func resolveChange(uri string) (changeRef, error) { scheme, _, ok := strings.Cut(uri, "://") if !ok { - return changeRef{}, fmt.Errorf("%w: change URI %q has no scheme", merger.ErrInvalidRequest, uri) + return changeRef{}, fmt.Errorf("%w: change URI %q has no scheme", lander.ErrInvalidRequest, uri) } switch scheme { case "github": cid, err := entitygithub.ParseChangeID(uri) if err != nil { - return changeRef{}, fmt.Errorf("%w: invalid change URI %q: %v", merger.ErrInvalidRequest, uri, err) + return changeRef{}, fmt.Errorf("%w: invalid change URI %q: %v", lander.ErrInvalidRequest, uri, err) } return changeRef{ Provider: scheme, @@ -69,7 +69,7 @@ func resolveChange(uri string) (changeRef, error) { case "git": cid, err := entitygit.ParseChangeID(uri) if err != nil { - return changeRef{}, fmt.Errorf("%w: invalid change URI %q: %v", merger.ErrInvalidRequest, uri, err) + return changeRef{}, fmt.Errorf("%w: invalid change URI %q: %v", lander.ErrInvalidRequest, uri, err) } // A git:// URI already names its own fully-qualified ref, so the // staleness check reads exactly the ref the caller pinned. @@ -81,7 +81,7 @@ func resolveChange(uri string) (changeRef, error) { }, nil default: - return changeRef{}, fmt.Errorf("%w: unsupported change URI scheme %q in %q", merger.ErrInvalidRequest, scheme, uri) + return changeRef{}, fmt.Errorf("%w: unsupported change URI scheme %q in %q", lander.ErrInvalidRequest, scheme, uri) } } diff --git a/runway/extension/merger/git/git_merger.go b/runway/extension/lander/git/git_lander.go similarity index 82% rename from runway/extension/merger/git/git_merger.go rename to runway/extension/lander/git/git_lander.go index f0d6575f6..77db46216 100644 --- a/runway/extension/merger/git/git_merger.go +++ b/runway/extension/lander/git/git_lander.go @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package git is a Merger implementation backed by a local git checkout. It -// applies a MergeRequest's ordered steps onto a target branch, honoring each -// step's merge strategy, and (for a committing Merge) pushes the result. +// Package git is a Lander implementation backed by a local git checkout. It +// applies a LandRequest's ordered steps onto a target branch, honoring each +// step's land strategy, and (for a committing Land) pushes the result. // // Strategy → git operation: // @@ -30,17 +30,17 @@ // - DEFAULT: resolved to the instance's configured DefaultStrategy // before any step runs. // -// Authorship: the committer is always the merger's configured identity, since +// Authorship: the committer is always the lander's configured identity, since // it is what applied the change. The author is the person who wrote it — // carried across automatically by cherry-pick under REBASE, and set explicitly // on the commits SQUASH_REBASE and MERGE mint, from the author recorded on the // commit each change's URI pins. See author.go. // -// Atomicity: for a committing merge nothing reaches the remote until every step +// Atomicity: for a committing land nothing reaches the remote until every step // has applied cleanly (PROMOTE excepted, which is itself a single atomic // fast-forward ref update). A step that fails to apply aborts the in-progress // git operation and returns without pushing. With Params.UpdateHeadBranch the -// merge then writes two things rather than one — the changes' head branches +// land then writes two things rather than one — the changes' head branches // first, then the target — and only the target's push is the point of no return. // // Contention: if the push fails because the remote tip moved between reset and @@ -52,11 +52,11 @@ // the push to the target, comparing the change's recorded head against what that // push makes reachable — a comparison REBASE and SQUASH_REBASE break by // rewriting the change's commits. With Params.UpdateHeadBranch, a committing -// merge first moves each change's head branch to the commit it became and then +// land first moves each change's head branch to the commit it became and then // pushes the target, so the provider sees its own recorded head land and marks // the change merged rather than closed. See headbranch.go. // -// Dry-run (CheckMergeability) applies the exact same steps but never pushes; +// Dry-run (CheckLandability) applies the exact same steps but never pushes; // intermediate steps are committed locally so a cumulative multi-step check // sees the same conflict surface, then the checkout is reset to discard them. // Reported StepResults carry no Outputs. @@ -85,12 +85,12 @@ import ( "github.com/uber-go/tally" "go.uber.org/zap" - mergestrategypb "github.com/uber/submitqueue/api/base/mergestrategy/protopb" + landstrategypb "github.com/uber/submitqueue/api/base/landstrategy/protopb" runwaymq "github.com/uber/submitqueue/api/runway/messagequeue" runwaypb "github.com/uber/submitqueue/api/runway/messagequeue/protopb" gitexec "github.com/uber/submitqueue/platform/git/exec" coremetrics "github.com/uber/submitqueue/platform/metrics" - "github.com/uber/submitqueue/runway/extension/merger" + "github.com/uber/submitqueue/runway/extension/lander" ) // defaultMaxPushAttempts caps the retry loop when the remote tip keeps moving @@ -106,7 +106,7 @@ const ( defaultCommitterEmail = "runway@submitqueue.invalid" ) -// GitRuntime identifies the explicitly provided Git runtime used by the Merger. +// GitRuntime identifies the explicitly provided Git runtime used by the Lander. type GitRuntime struct { // Executable is the absolute path to the Git executable. Executable string @@ -118,15 +118,15 @@ type GitRuntime struct { // PassthroughEnv names additional environment variables to inherit from // the parent process, on top of the auth and transport ones always passed // through. For a deployment whose remote needs something unusual; leave - // empty otherwise. Names that could alter merge semantics do not belong - // here — the scrubbed environment is what keeps a merge reproducible. + // empty otherwise. Names that could alter land semantics do not belong + // here — the scrubbed environment is what keeps the land reproducible. PassthroughEnv []string } -// Params holds the dependencies for the git Merger. +// Params holds the dependencies for the git Lander. type Params struct { // CheckoutPath is the absolute path to an existing git checkout that the - // Merger will operate against. The Merger owns this working tree. + // Lander will operate against. The Lander owns this working tree. CheckoutPath string // Remote is the name of the git remote to fetch from and push to // (e.g. "origin"). @@ -135,10 +135,10 @@ type Params struct { Target string // DefaultStrategy resolves a step whose strategy is DEFAULT. Must be a // concrete strategy (REBASE, SQUASH_REBASE, MERGE, or PROMOTE). - DefaultStrategy mergestrategypb.Strategy + DefaultStrategy landstrategypb.Strategy // Runtime is the pinned Git runtime used for every invocation. Runtime GitRuntime - // MaxPushAttempts caps how many times a committing merge retries the full + // MaxPushAttempts caps how many times a committing land retries the full // reset/apply/push cycle when the remote tip moves under it. Defaults to // defaultMaxPushAttempts when zero or negative. MaxPushAttempts int @@ -156,26 +156,26 @@ type Params struct { // is pushed. A provider decides merged-versus-closed while processing the // push to the target, against the head it has recorded at that moment, so // this is what makes a rewriting strategy have the change marked merged — - // without the merger having to know the provider or call its API. + // without the lander having to know the provider or call its API. // // The ordering is the whole mechanism, not an implementation detail: moving // the branch after the target has been pushed, or in the same atomic push, - // both leave the provider comparing against the pre-merge head, and it + // both leave the provider comparing against the pre-land head, and it // records the change as closed instead. // // Only REBASE and SQUASH_REBASE need it: MERGE keeps the change's own head // reachable through second-parent history, and PROMOTE fast-forwards the // target to that head directly. Off by default, since moving a branch the - // merger was not asked to move is a surprise unless a deployment opted in. + // lander was not asked to move is a surprise unless a deployment opted in. // - // A head branch that cannot be moved fails the merge before the target is + // A head branch that cannot be moved fails the land before the target is // pushed. A change with no branch of ours to move — proposed from a fork, or // with a head several branches share — is skipped and lands normally. UpdateHeadBranch bool // AllowUnrelatedHistories lets a MERGE step integrate a change that shares // no ancestry with the target — importing one repository's history into // another. Off by default: the refusal it lifts is a real safeguard, since - // without it a merge of the wrong object fails loudly instead of quietly + // without it a land of the wrong object fails loudly instead of quietly // producing a nonsense result. Enable it on a queue that exists to perform // such an import. AllowUnrelatedHistories bool @@ -189,13 +189,13 @@ type Params struct { MetricsScope tally.Scope } -// gitMerger implements merger.Merger by shelling out to the `git` CLI against a +// gitLander implements lander.Lander by shelling out to the `git` CLI against a // local checkout. -type gitMerger struct { +type gitLander struct { checkoutPath string remote string target string - defaultStrategy mergestrategypb.Strategy + defaultStrategy landstrategypb.Strategy runtime GitRuntime maxPushAttempts int fetchRefspecs []string @@ -214,24 +214,24 @@ type gitMerger struct { mu sync.Mutex } -// Verify gitMerger implements merger.Merger at compile time. -var _ merger.Merger = (*gitMerger)(nil) +// Verify gitLander implements lander.Lander at compile time. +var _ lander.Lander = (*gitLander)(nil) // resolvedStep pairs a request step with its resolved (non-DEFAULT) strategy // and the changes it names, already parsed. type resolvedStep struct { - step *runwaymq.MergeStep - strategy mergestrategypb.Strategy + step *runwaymq.LandStep + strategy landstrategypb.Strategy // refs holds one changeRef per URI of the step's change, in application // order. Resolution happens once, during validation, so the apply paths // work from parsed refs rather than re-parsing the URIs they came from. refs []changeRef } -// NewMerger constructs a git-backed Merger operating against the given checkout. +// NewLander constructs a git-backed Lander operating against the given checkout. // The checkout must already exist and have the configured remote. Runtime paths // must be absolute and DefaultStrategy must be a concrete strategy. -func NewMerger(params Params) (merger.Merger, error) { +func NewLander(params Params) (lander.Lander, error) { if err := params.Runtime.validate(); err != nil { return nil, err } @@ -250,7 +250,7 @@ func NewMerger(params Params) (merger.Merger, error) { if committerEmail == "" { committerEmail = defaultCommitterEmail } - return &gitMerger{ + return &gitLander{ checkoutPath: params.CheckoutPath, remote: params.Remote, target: params.Target, @@ -264,8 +264,8 @@ func NewMerger(params Params) (merger.Merger, error) { allowUnrelatedHistories: params.AllowUnrelatedHistories, committerName: committerName, committerEmail: committerEmail, - logger: params.Logger.Named("git_merger"), - metricsScope: params.MetricsScope.SubScope("git_merger"), + logger: params.Logger.Named("git_lander"), + metricsScope: params.MetricsScope.SubScope("git_lander"), }, nil } @@ -285,26 +285,26 @@ func (r GitRuntime) validate() error { return nil } -// CheckMergeability applies the request's steps as a dry run: it verifies each +// CheckLandability applies the request's steps as a dry run: it verifies each // step applies cleanly without committing to the remote, and returns per-step // results with empty Outputs. -func (m *gitMerger) CheckMergeability(ctx context.Context, req *runwaymq.MergeRequest) (*runwaymq.MergeResult, error) { +func (m *gitLander) CheckLandability(ctx context.Context, req *runwaymq.LandRequest) (*runwaymq.LandResult, error) { return m.process(ctx, req, false) } -// Merge applies the request's steps and commits the result to the remote, +// Land applies the request's steps and commits the result to the remote, // returning per-step Outputs (the revisions produced on the target). -func (m *gitMerger) Merge(ctx context.Context, req *runwaymq.MergeRequest) (*runwaymq.MergeResult, error) { +func (m *gitLander) Land(ctx context.Context, req *runwaymq.LandRequest) (*runwaymq.LandResult, error) { return m.process(ctx, req, true) } -// process is the shared entry point for both Merge and CheckMergeability. The -// commit flag selects whether the result is pushed to the remote (Merge) or -// applied locally and discarded (CheckMergeability). -func (m *gitMerger) process(ctx context.Context, req *runwaymq.MergeRequest, commit bool) (ret *runwaymq.MergeResult, retErr error) { +// process is the shared entry point for both Land and CheckLandability. The +// commit flag selects whether the result is pushed to the remote (Land) or +// applied locally and discarded (CheckLandability). +func (m *gitLander) process(ctx context.Context, req *runwaymq.LandRequest, commit bool) (ret *runwaymq.LandResult, retErr error) { opName := "check" if commit { - opName = "merge" + opName = "land" } op := coremetrics.Begin(m.metricsScope, opName, coremetrics.LongLatencyBuckets) defer func() { op.Complete(retErr) }() @@ -317,7 +317,7 @@ func (m *gitMerger) process(ctx context.Context, req *runwaymq.MergeRequest, com m.mu.Lock() defer m.mu.Unlock() - m.logger.Debugw("starting merge", + m.logger.Debugw("starting land", "id", req.GetId(), "target", m.target, "remote", m.remote, @@ -326,7 +326,7 @@ func (m *gitMerger) process(ctx context.Context, req *runwaymq.MergeRequest, com ) // PROMOTE is exclusive: validated to be the entire request (one step). - if steps[0].strategy == mergestrategypb.Strategy_PROMOTE { + if steps[0].strategy == landstrategypb.Strategy_PROMOTE { return m.promote(ctx, req, steps[0], commit) } return m.applyTransforming(ctx, req, steps, commit) @@ -335,12 +335,12 @@ func (m *gitMerger) process(ctx context.Context, req *runwaymq.MergeRequest, com // resolveAndValidate normalizes DEFAULT strategies to the configured default, // resolves every change URI once for the apply paths to work from, checks that // the request's changes agree on a provider, and enforces the PROMOTE -// composition rule. All failures here are terminal (merger.ErrInvalidRequest): +// composition rule. All failures here are terminal (lander.ErrInvalidRequest): // retrying never succeeds, so the controller publishes a FAILED result rather // than nacking. -func (m *gitMerger) resolveAndValidate(req *runwaymq.MergeRequest) ([]resolvedStep, error) { +func (m *gitLander) resolveAndValidate(req *runwaymq.LandRequest) ([]resolvedStep, error) { if len(req.GetSteps()) == 0 { - return nil, fmt.Errorf("%w: request has no steps", merger.ErrInvalidRequest) + return nil, fmt.Errorf("%w: request has no steps", lander.ErrInvalidRequest) } resolved := make([]resolvedStep, 0, len(req.GetSteps())) @@ -348,19 +348,19 @@ func (m *gitMerger) resolveAndValidate(req *runwaymq.MergeRequest) ([]resolvedSt promoteSeen := false for _, step := range req.GetSteps() { strategy := step.GetStrategy() - if strategy == mergestrategypb.Strategy_DEFAULT { + if strategy == landstrategypb.Strategy_DEFAULT { strategy = m.defaultStrategy } if !isConcreteStrategy(strategy) { - return nil, fmt.Errorf("%w: unsupported strategy %v", merger.ErrInvalidRequest, step.GetStrategy()) + return nil, fmt.Errorf("%w: unsupported strategy %v", lander.ErrInvalidRequest, step.GetStrategy()) } - if strategy == mergestrategypb.Strategy_PROMOTE { + if strategy == landstrategypb.Strategy_PROMOTE { promoteSeen = true } ch := step.GetChange() if ch == nil || len(ch.GetUris()) == 0 { - return nil, fmt.Errorf("%w: step %q has no change URIs", merger.ErrInvalidRequest, step.GetStepId()) + return nil, fmt.Errorf("%w: step %q has no change URIs", lander.ErrInvalidRequest, step.GetStepId()) } refs := make([]changeRef, 0, len(ch.GetUris())) for _, uri := range ch.GetUris() { @@ -383,7 +383,7 @@ func (m *gitMerger) resolveAndValidate(req *runwaymq.MergeRequest) ([]resolvedSt if promoteSeen { if len(resolved) != 1 || len(resolved[0].step.GetChange().GetUris()) != 1 { - return nil, fmt.Errorf("%w: PROMOTE must be the entire request (one step, one change, one URI)", merger.ErrInvalidRequest) + return nil, fmt.Errorf("%w: PROMOTE must be the entire request (one step, one change, one URI)", lander.ErrInvalidRequest) } } @@ -393,7 +393,7 @@ func (m *gitMerger) resolveAndValidate(req *runwaymq.MergeRequest) ([]resolvedSt // providerCheck holds the provider the first change in a request established, // and rejects any later change addressed through a different one. // -// There is no sense in one merge being addressed through two providers, and +// There is no sense in one land being addressed through two providers, and // SubmitQueue already refuses it within a single change. Catching it here costs // nothing and keeps the apply paths from having to reason about a request whose // changes were resolved by different parsers. @@ -411,7 +411,7 @@ func (o *providerCheck) check(ref changeRef, stepID string) error { } if ref.Provider != o.provider { return fmt.Errorf("%w: request mixes change providers: step %q uses %q, step %q uses %q", - merger.ErrInvalidRequest, o.stepID, o.provider, stepID, ref.Provider) + lander.ErrInvalidRequest, o.stepID, o.provider, stepID, ref.Provider) } return nil } @@ -419,7 +419,7 @@ func (o *providerCheck) check(ref changeRef, stepID string) error { // applyTransforming runs the reset/apply/push cycle for the transforming // strategies (REBASE, SQUASH_REBASE, MERGE), retrying on remote contention when // committing. For a dry run it applies the steps locally then discards them. -func (m *gitMerger) applyTransforming(ctx context.Context, req *runwaymq.MergeRequest, steps []resolvedStep, commit bool) (*runwaymq.MergeResult, error) { +func (m *gitLander) applyTransforming(ctx context.Context, req *runwaymq.LandRequest, steps []resolvedStep, commit bool) (*runwaymq.LandResult, error) { // Fetch and vet every change the request names before the first attempt, so // an unusable request fails without having touched the checkout. This sits // outside the retry loop deliberately: the refs are fixed for the whole @@ -449,12 +449,12 @@ func (m *gitMerger) applyTransforming(ctx context.Context, req *runwaymq.MergeRe } stripOutputs(stepResults) } - m.logger.Debugw("merge complete", "id", req.GetId(), "target", m.target, "commit", commit) + m.logger.Debugw("land complete", "id", req.GetId(), "target", m.target, "commit", commit) return successResult(req, stepResults), nil } // A conflict is terminal — no retry. Discard any partial dry-run state. - if errors.Is(err, merger.ErrConflict) { + if errors.Is(err, lander.ErrConflict) { if !commit { _ = m.resetToRemote(ctx) } @@ -475,8 +475,8 @@ func (m *gitMerger) applyTransforming(ctx context.Context, req *runwaymq.MergeRe return nil, err } - coremetrics.NamedCounter(m.metricsScope, "merge", "stale_base_retries", 1) - m.logger.Warnw("remote tip moved during merge, retrying", + coremetrics.NamedCounter(m.metricsScope, "land", "stale_base_retries", 1) + m.logger.Warnw("remote tip moved during land, retrying", "attempt", attempt, "max_attempts", m.maxPushAttempts, "base_sha", baseSHA, @@ -486,17 +486,17 @@ func (m *gitMerger) applyTransforming(ctx context.Context, req *runwaymq.MergeRe lastErr = err } - coremetrics.NamedCounter(m.metricsScope, "merge", "stale_base_giveup", 1) - return nil, fmt.Errorf("exceeded %d merge attempts due to remote contention: %w", m.maxPushAttempts, lastErr) + coremetrics.NamedCounter(m.metricsScope, "land", "stale_base_giveup", 1) + return nil, fmt.Errorf("exceeded %d land attempts due to remote contention: %w", m.maxPushAttempts, lastErr) } // tryApply runs one full reset+apply(+push) cycle. The returned baseSHA is the // SHA the cycle was based on (set as soon as resetToRemote completes) so the // caller can distinguish concurrent-push contention from other failures. The // tracker carries head-branch state across attempts; see headBranchTracker. -func (m *gitMerger) tryApply(ctx context.Context, steps []resolvedStep, commit bool, tracked headBranchTracker) (string, []*runwaymq.StepResult, error) { +func (m *gitLander) tryApply(ctx context.Context, steps []resolvedStep, commit bool, tracked headBranchTracker) (string, []*runwaymq.StepResult, error) { if err := m.resetToRemote(ctx); err != nil { - coremetrics.NamedCounter(m.metricsScope, "merge", "reset_errors", 1) + coremetrics.NamedCounter(m.metricsScope, "land", "reset_errors", 1) return "", nil, err } baseSHA, err := m.headSHA(ctx) @@ -522,7 +522,7 @@ func (m *gitMerger) tryApply(ctx context.Context, steps []resolvedStep, commit b } } if err := m.push(ctx); err != nil { - coremetrics.NamedCounter(m.metricsScope, "merge", "git_push_errors", 1) + coremetrics.NamedCounter(m.metricsScope, "land", "git_push_errors", 1) return baseSHA, nil, err } } @@ -540,7 +540,7 @@ type applied struct { // applySteps dispatches each step by its resolved strategy, in order, building // up local HEAD and collecting one StepResult per step. -func (m *gitMerger) applySteps(ctx context.Context, steps []resolvedStep) ([]*runwaymq.StepResult, []headUpdate, error) { +func (m *gitLander) applySteps(ctx context.Context, steps []resolvedStep) ([]*runwaymq.StepResult, []headUpdate, error) { results := make([]*runwaymq.StepResult, 0, len(steps)) var heads []headUpdate for _, rs := range steps { @@ -549,15 +549,15 @@ func (m *gitMerger) applySteps(ctx context.Context, steps []resolvedStep) ([]*ru err error ) switch rs.strategy { - case mergestrategypb.Strategy_REBASE: + case landstrategypb.Strategy_REBASE: out, err = m.applyRebase(ctx, rs) - case mergestrategypb.Strategy_SQUASH_REBASE: + case landstrategypb.Strategy_SQUASH_REBASE: out, err = m.applySquashRebase(ctx, rs) - case mergestrategypb.Strategy_MERGE: - out, err = m.applyMerge(ctx, rs) + case landstrategypb.Strategy_MERGE: + out, err = m.applyMergeCommit(ctx, rs) default: // resolveAndValidate rejects anything else; defensive. - return nil, nil, fmt.Errorf("%w: unsupported strategy %v", merger.ErrInvalidRequest, rs.strategy) + return nil, nil, fmt.Errorf("%w: unsupported strategy %v", lander.ErrInvalidRequest, rs.strategy) } if err != nil { return nil, nil, err @@ -570,7 +570,7 @@ func (m *gitMerger) applySteps(ctx context.Context, steps []resolvedStep) ([]*ru // applyRebase cherry-picks the head SHA of every URI of every change in the // step, in order, returning one StepOutput per newly-created commit. -func (m *gitMerger) applyRebase(ctx context.Context, rs resolvedStep) (applied, error) { +func (m *gitLander) applyRebase(ctx context.Context, rs resolvedStep) (applied, error) { picked, heads, err := m.pickStepChanges(ctx, rs) if err != nil { return applied{}, err @@ -586,7 +586,7 @@ func (m *gitMerger) applyRebase(ctx context.Context, rs resolvedStep) (applied, // commit would erase the per-PR boundary the stack exists to express. So each // URI is picked as its own range and squashed on its own, in order, yielding // one commit — and one output — per change that had anything to contribute. -func (m *gitMerger) applySquashRebase(ctx context.Context, rs resolvedStep) (applied, error) { +func (m *gitLander) applySquashRebase(ctx context.Context, rs resolvedStep) (applied, error) { var out applied for _, ref := range rs.refs { sha, squashed, err := m.squashChange(ctx, rs.step, ref) @@ -610,7 +610,7 @@ func (m *gitMerger) applySquashRebase(ctx context.Context, rs resolvedStep) (app // base — its effect was already there, spread differently — would squash to an // empty commit, so the intermediates are dropped instead. Both keep redelivery // idempotent. -func (m *gitMerger) squashChange(ctx context.Context, step *runwaymq.MergeStep, ref changeRef) (string, bool, error) { +func (m *gitLander) squashChange(ctx context.Context, step *runwaymq.LandStep, ref changeRef) (string, bool, error) { preSHA, err := m.headSHA(ctx) if err != nil { return "", false, err @@ -643,7 +643,7 @@ func (m *gitMerger) squashChange(ctx context.Context, step *runwaymq.MergeStep, return "", false, fmt.Errorf("git reset --soft %s: %w", preSHA, err) } // The squash is a new commit, so it carries no authorship of its own. - // Credit it to the change's own author rather than to the merger. + // Credit it to the change's own author rather than to the lander. author, err := m.commitAuthor(ctx, ref.SHA) if err != nil { return "", false, err @@ -658,7 +658,7 @@ func (m *gitMerger) squashChange(ctx context.Context, step *runwaymq.MergeStep, return sha, true, nil } -// applyMerge creates a --no-ff merge commit for every change in the step, +// applyMergeCommit creates a --no-ff merge commit for every change in the step, // keeping the change's original commits reachable through second-parent // history — the property that distinguishes MERGE from the picking strategies, // which rewrite those commits. A change already contained in HEAD produces no @@ -666,7 +666,7 @@ func (m *gitMerger) squashChange(ctx context.Context, step *runwaymq.MergeStep, // // It reports no head updates: the change's own head is already reachable from // the target, so its branch needs no moving for a provider to call it merged. -func (m *gitMerger) applyMerge(ctx context.Context, rs resolvedStep) (applied, error) { +func (m *gitLander) applyMergeCommit(ctx context.Context, rs resolvedStep) (applied, error) { var out applied for _, ref := range rs.refs { contained, err := m.isAncestor(ctx, ref.SHA, "HEAD") @@ -681,7 +681,7 @@ func (m *gitMerger) applyMerge(ctx context.Context, rs resolvedStep) (applied, e if m.allowUnrelatedHistories { args = append(args, "--allow-unrelated-histories") } - // The merge commit is minted by the merger, so it would otherwise be + // The merge commit is minted by the lander, so it would otherwise be // authored by the service. Credit it to the change being merged; the // change's own commits keep their authors through the second parent. author, err := m.commitAuthor(ctx, ref.SHA) @@ -694,7 +694,7 @@ func (m *gitMerger) applyMerge(ctx context.Context, rs resolvedStep) (applied, e // does not establish that anything collided. conflicted := m.hasUnmergedPaths(ctx) _, _ = m.run(ctx, nil, "merge", "--abort") - return applied{}, m.classifyMergeFailure(ref, o, conflicted) + return applied{}, m.classifyLandFailure(ref, o, conflicted) } mergeSHA, err := m.headSHA(ctx) if err != nil { @@ -707,7 +707,7 @@ func (m *gitMerger) applyMerge(ctx context.Context, rs resolvedStep) (applied, e // promote fast-forwards the target to an already-existing commit. It is only // reachable for a validated single-step/single-change/single-URI request. -func (m *gitMerger) promote(ctx context.Context, req *runwaymq.MergeRequest, rs resolvedStep, commit bool) (*runwaymq.MergeResult, error) { +func (m *gitLander) promote(ctx context.Context, req *runwaymq.LandRequest, rs resolvedStep, commit bool) (*runwaymq.LandResult, error) { // Validated to be one step with one change of one URI, so the request names // exactly one commit. ref := rs.refs[0] @@ -754,7 +754,7 @@ func (m *gitMerger) promote(ctx context.Context, req *runwaymq.MergeRequest, rs return nil, err } if !fastForward { - return nil, fmt.Errorf("%w: promote target %s is not a fast-forward of %s", merger.ErrConflict, sha, tip) + return nil, fmt.Errorf("%w: promote target %s is not a fast-forward of %s", lander.ErrConflict, sha, tip) } if !commit { @@ -776,7 +776,7 @@ func (m *gitMerger) promote(ctx context.Context, req *runwaymq.MergeRequest, rs return nil, fmt.Errorf("exceeded %d promote attempts due to remote contention: %w", m.maxPushAttempts, lastErr) } -// classifyMergeFailure decides what a failed `git merge` actually means, given +// classifyLandFailure decides what a failed `git merge` actually means, given // whether the index was left holding conflicted entries. // // Not every refusal is a conflict, and reporting one as such tells the client @@ -785,26 +785,26 @@ func (m *gitMerger) promote(ctx context.Context, req *runwaymq.MergeRequest, rs // and fixed by configuration rather than by rebasing, and any other way git // can exit non-zero — a missing object, an unreadable repository, a killed // process — which is infrastructure and should be retried, not made terminal. -func (m *gitMerger) classifyMergeFailure(ref changeRef, out []byte, conflicted bool) error { +func (m *gitLander) classifyLandFailure(ref changeRef, out []byte, conflicted bool) error { detail := strings.TrimSpace(string(out)) if strings.Contains(detail, "refusing to merge unrelated histories") { - coremetrics.NamedCounter(m.metricsScope, "merge", "unrelated_histories", 1) - return fmt.Errorf("%w: %s shares no history with the merge target; integrating an imported history requires the merger to allow unrelated histories", - merger.ErrInvalidRequest, ref.Label) + coremetrics.NamedCounter(m.metricsScope, "land", "unrelated_histories", 1) + return fmt.Errorf("%w: %s shares no history with the land target; integrating an imported history requires the lander to allow unrelated histories", + lander.ErrInvalidRequest, ref.Label) } if !conflicted { - coremetrics.NamedCounter(m.metricsScope, "merge", "merge_errors", 1) + coremetrics.NamedCounter(m.metricsScope, "land", "land_errors", 1) return fmt.Errorf("git merge %s: %s", ref.SHA, detail) } - coremetrics.NamedCounter(m.metricsScope, "merge", "merge_conflicts", 1) - return fmt.Errorf("%w: git merge %s: %s", merger.ErrConflict, ref.SHA, detail) + coremetrics.NamedCounter(m.metricsScope, "land", "land_conflicts", 1) + return fmt.Errorf("%w: git merge %s: %s", lander.ErrConflict, ref.SHA, detail) } // pickStepChanges applies every change in the step, in order, returning the // SHAs of the commits created on the target (empty for a change whose content // was already present) and, per change that produced any, the last of those // commits — the one that now represents the change on the target. -func (m *gitMerger) pickStepChanges(ctx context.Context, rs resolvedStep) ([]string, []headUpdate, error) { +func (m *gitLander) pickStepChanges(ctx context.Context, rs resolvedStep) ([]string, []headUpdate, error) { var picked []string var heads []headUpdate for _, ref := range rs.refs { @@ -829,7 +829,7 @@ func (m *gitMerger) pickStepChanges(ctx context.Context, rs resolvedStep) ([]str // commits touch different files — succeeds while silently dropping everything // before the head. The unit to replay is therefore the range from the change's // merge base with the target up to its head. -func (m *gitMerger) pickRange(ctx context.Context, ref changeRef) ([]string, error) { +func (m *gitLander) pickRange(ctx context.Context, ref changeRef) ([]string, error) { base, err := m.mergeBase(ctx, "HEAD", ref.SHA) if err != nil { return nil, err @@ -838,8 +838,8 @@ func (m *gitMerger) pickRange(ctx context.Context, ref changeRef) ([]string, err // No common ancestor: there is no range to compute, and replaying an // imported history commit-by-commit would rewrite every SHA in it, // which is the opposite of what such a request wants. - return nil, fmt.Errorf("%w: change %s shares no history with the merge target; use the MERGE strategy to integrate an unrelated history", - merger.ErrInvalidRequest, ref.Label) + return nil, fmt.Errorf("%w: change %s shares no history with the land target; use the MERGE strategy to integrate an unrelated history", + lander.ErrInvalidRequest, ref.Label) } // A change wholly contained in the target has an empty range — its merge @@ -874,7 +874,7 @@ func (m *gitMerger) pickRange(ctx context.Context, ref changeRef) ([]string, err // both should be skipped rather than treated as failures. Anything else ends // the attempt, and the in-progress pick is aborted so the checkout stays // usable for the next request. -func (m *gitMerger) cherryPickRange(ctx context.Context, base, head string) error { +func (m *gitLander) cherryPickRange(ctx context.Context, base, head string) error { out, err := m.runCombined(ctx, nil, "cherry-pick", base+".."+head) for err != nil { if isRedundantCherryPick(out) { @@ -891,11 +891,11 @@ func (m *gitMerger) cherryPickRange(ctx context.Context, base, head string) erro _, _ = m.run(ctx, nil, "cherry-pick", "--abort") detail := strings.TrimSpace(string(out)) if !conflicted { - coremetrics.NamedCounter(m.metricsScope, "merge", "cherry_pick_errors", 1) + coremetrics.NamedCounter(m.metricsScope, "land", "cherry_pick_errors", 1) return fmt.Errorf("git cherry-pick %s..%s: %w: %s", base, head, err, detail) } - coremetrics.NamedCounter(m.metricsScope, "merge", "cherry_pick_conflicts", 1) - return fmt.Errorf("%w: git cherry-pick %s..%s: %s", merger.ErrConflict, base, head, detail) + coremetrics.NamedCounter(m.metricsScope, "land", "cherry_pick_conflicts", 1) + return fmt.Errorf("%w: git cherry-pick %s..%s: %s", lander.ErrConflict, base, head, detail) } return nil } @@ -904,7 +904,7 @@ func (m *gitMerger) cherryPickRange(ctx context.Context, base, head string) erro // what separates a genuine content conflict from any other reason a git // invocation exited non-zero, and it has to be read before the operation is // aborted, since aborting clears the index. -func (m *gitMerger) hasUnmergedPaths(ctx context.Context) bool { +func (m *gitLander) hasUnmergedPaths(ctx context.Context) bool { out, err := m.run(ctx, nil, "ls-files", "--unmerged") return err == nil && len(bytes.TrimSpace(out)) > 0 } @@ -912,7 +912,7 @@ func (m *gitMerger) hasUnmergedPaths(ctx context.Context) bool { // mergeBase returns the best common ancestor of two commits, or "" when they // share no history. git exits 1 for the latter, which is an answer rather than // a failure. -func (m *gitMerger) mergeBase(ctx context.Context, a, b string) (string, error) { +func (m *gitLander) mergeBase(ctx context.Context, a, b string) (string, error) { out, err := m.run(ctx, nil, "merge-base", a, b) if err == nil { return strings.TrimSpace(string(out)), nil @@ -928,7 +928,7 @@ func (m *gitMerger) mergeBase(ctx context.Context, a, b string) (string, error) // first. The commits an apply produced are derived from the checkout this way // rather than tracked per-invocation, which stays correct when the sequencer // drops some of them. -func (m *gitMerger) revList(ctx context.Context, from, to string) ([]string, error) { +func (m *gitLander) revList(ctx context.Context, from, to string) ([]string, error) { out, err := m.run(ctx, nil, "rev-list", "--reverse", from+".."+to) if err != nil { return nil, fmt.Errorf("git rev-list %s..%s: %w", from, to, err) @@ -939,7 +939,7 @@ func (m *gitMerger) revList(ctx context.Context, from, to string) ([]string, err // resetToRemote fetches the configured remote and hard-resets the checkout's // HEAD to refs/remotes//, producing a clean working tree pinned // to the latest remote tip. -func (m *gitMerger) resetToRemote(ctx context.Context) error { +func (m *gitLander) resetToRemote(ctx context.Context) error { if _, err := m.run(ctx, nil, "fetch", m.remote); err != nil { return fmt.Errorf("git fetch %s: %w", m.remote, err) } @@ -962,7 +962,7 @@ func (m *gitMerger) resetToRemote(ctx context.Context) error { } // headSHA returns the SHA at HEAD in the local checkout. -func (m *gitMerger) headSHA(ctx context.Context) (string, error) { +func (m *gitLander) headSHA(ctx context.Context) (string, error) { out, err := m.run(ctx, nil, "rev-parse", "HEAD") if err != nil { return "", fmt.Errorf("git rev-parse HEAD: %w", err) @@ -973,7 +973,7 @@ func (m *gitMerger) headSHA(ctx context.Context) (string, error) { // refetchTipSHA fetches the remote and returns the current SHA at // refs/remotes//. Used after a push failure to detect whether // the remote tip moved under us. -func (m *gitMerger) refetchTipSHA(ctx context.Context) (string, error) { +func (m *gitLander) refetchTipSHA(ctx context.Context) (string, error) { if _, err := m.run(ctx, nil, "fetch", m.remote); err != nil { return "", fmt.Errorf("git fetch %s: %w", m.remote, err) } @@ -988,7 +988,7 @@ func (m *gitMerger) refetchTipSHA(ctx context.Context) (string, error) { // isAncestor reports whether ancestor is an ancestor of (or equal to) // descendant. `git merge-base --is-ancestor` exits 0 for true, 1 for false; // any other exit is a real error. -func (m *gitMerger) isAncestor(ctx context.Context, ancestor, descendant string) (bool, error) { +func (m *gitLander) isAncestor(ctx context.Context, ancestor, descendant string) (bool, error) { cmd := m.command(ctx, "merge-base", "--is-ancestor", ancestor, descendant) var stderr bytes.Buffer cmd.Stderr = &stderr @@ -1004,7 +1004,7 @@ func (m *gitMerger) isAncestor(ctx context.Context, ancestor, descendant string) } // commitTreeSHA returns the tree SHA recorded in the commit object at ref. -func (m *gitMerger) commitTreeSHA(ctx context.Context, ref string) (string, error) { +func (m *gitLander) commitTreeSHA(ctx context.Context, ref string) (string, error) { out, err := m.run(ctx, nil, "rev-parse", ref+"^{tree}") if err != nil { return "", fmt.Errorf("git rev-parse %s^{tree}: %w", ref, err) @@ -1013,7 +1013,7 @@ func (m *gitMerger) commitTreeSHA(ctx context.Context, ref string) (string, erro } // push pushes the current HEAD to refs/heads/ on the remote. -func (m *gitMerger) push(ctx context.Context) error { +func (m *gitLander) push(ctx context.Context) error { refspec := "HEAD:refs/heads/" + m.target if _, err := m.run(ctx, nil, "push", m.remote, refspec); err != nil { return fmt.Errorf("git push %s %s: %w", m.remote, refspec, err) @@ -1023,13 +1023,13 @@ func (m *gitMerger) push(ctx context.Context) error { // run executes a `git` command in the checkout. Returns captured stdout and an // error that includes captured stderr for diagnostics. -func (m *gitMerger) run(ctx context.Context, stdin []byte, args ...string) ([]byte, error) { +func (m *gitLander) run(ctx context.Context, stdin []byte, args ...string) ([]byte, error) { return m.runAs(ctx, authorIdent{}, stdin, args...) } // runAs is run with any commit the command creates credited to the given // author. A zero author leaves the committer identity to author it too. -func (m *gitMerger) runAs(ctx context.Context, author authorIdent, stdin []byte, args ...string) ([]byte, error) { +func (m *gitLander) runAs(ctx context.Context, author authorIdent, stdin []byte, args ...string) ([]byte, error) { cmd := m.commandAs(ctx, author, args...) if stdin != nil { cmd.Stdin = bytes.NewReader(stdin) @@ -1045,13 +1045,13 @@ func (m *gitMerger) runAs(ctx context.Context, author authorIdent, stdin []byte, // runCombined is like run but returns combined stdout+stderr both on success // and failure. Used when the caller needs to inspect git's diagnostic output. -func (m *gitMerger) runCombined(ctx context.Context, stdin []byte, args ...string) ([]byte, error) { +func (m *gitLander) runCombined(ctx context.Context, stdin []byte, args ...string) ([]byte, error) { return m.runCombinedAs(ctx, authorIdent{}, stdin, args...) } // runCombinedAs is runCombined with any commit the command creates credited to // the given author. -func (m *gitMerger) runCombinedAs(ctx context.Context, author authorIdent, stdin []byte, args ...string) ([]byte, error) { +func (m *gitLander) runCombinedAs(ctx context.Context, author authorIdent, stdin []byte, args ...string) ([]byte, error) { cmd := m.commandAs(ctx, author, args...) if stdin != nil { cmd.Stdin = bytes.NewReader(stdin) @@ -1061,14 +1061,14 @@ func (m *gitMerger) runCombinedAs(ctx context.Context, author authorIdent, stdin // command builds a git command with the committer identity injected via -c // flags, on top of the pinned runtime and scrubbed environment. -func (m *gitMerger) command(ctx context.Context, args ...string) *exec.Cmd { +func (m *gitLander) command(ctx context.Context, args ...string) *exec.Cmd { return m.commandAs(ctx, authorIdent{}, args...) } // commandAs is command with the author supplied through the environment. The // committer identity is injected either way, so the author overrides only who // the commit is credited to, never who is recorded as having applied it. -func (m *gitMerger) commandAs(ctx context.Context, author authorIdent, args ...string) *exec.Cmd { +func (m *gitLander) commandAs(ctx context.Context, author authorIdent, args ...string) *exec.Cmd { withIdentity := make([]string, 0, len(args)+6) withIdentity = append(withIdentity, "-c", "user.name="+m.committerName, @@ -1115,12 +1115,12 @@ func newGitCommand(ctx context.Context, runtime GitRuntime, dir string, args ... // isConcreteStrategy reports whether s names a concrete integration strategy // (i.e. not DEFAULT and not an unknown value). -func isConcreteStrategy(s mergestrategypb.Strategy) bool { +func isConcreteStrategy(s landstrategypb.Strategy) bool { switch s { - case mergestrategypb.Strategy_REBASE, - mergestrategypb.Strategy_SQUASH_REBASE, - mergestrategypb.Strategy_MERGE, - mergestrategypb.Strategy_PROMOTE: + case landstrategypb.Strategy_REBASE, + landstrategypb.Strategy_SQUASH_REBASE, + landstrategypb.Strategy_MERGE, + landstrategypb.Strategy_PROMOTE: return true default: return false @@ -1140,7 +1140,7 @@ func isRedundantCherryPick(out []byte) bool { // label — which is why this goes through the resolver rather than one // provider's fields, so a non-GitHub change is named too instead of being // silently omitted. -func squashMessage(step *runwaymq.MergeStep, ref changeRef) string { +func squashMessage(step *runwaymq.LandStep, ref changeRef) string { if step.GetStepId() != "" { return fmt.Sprintf("squash: %s (%s)", step.GetStepId(), ref.Label) } @@ -1160,31 +1160,31 @@ func toOutputs(shas []string) []*runwaymq.StepOutput { } // stripOutputs clears the Outputs of every step result — used for a dry-run -// check, which reports mergeability only. +// check, which reports landability only. func stripOutputs(steps []*runwaymq.StepResult) { for _, s := range steps { s.Outputs = nil } } -// successResult builds a SUCCEEDED MergeResult echoing the request id. -func successResult(req *runwaymq.MergeRequest, steps []*runwaymq.StepResult) *runwaymq.MergeResult { - return &runwaymq.MergeResult{ +// successResult builds a SUCCEEDED LandResult echoing the request id. +func successResult(req *runwaymq.LandRequest, steps []*runwaymq.StepResult) *runwaymq.LandResult { + return &runwaymq.LandResult{ Id: req.GetId(), Outcome: runwaypb.Outcome_SUCCEEDED, Steps: steps, } } -// promoteResult builds a SUCCEEDED MergeResult for a promote. A committing +// promoteResult builds a SUCCEEDED LandResult for a promote. A committing // promote reports the promoted SHA as the step's single output; a dry-run check // reports no output. -func promoteResult(req *runwaymq.MergeRequest, rs resolvedStep, sha string, commit bool) *runwaymq.MergeResult { +func promoteResult(req *runwaymq.LandRequest, rs resolvedStep, sha string, commit bool) *runwaymq.LandResult { var outputs []*runwaymq.StepOutput if commit { outputs = []*runwaymq.StepOutput{{Id: sha}} } - return &runwaymq.MergeResult{ + return &runwaymq.LandResult{ Id: req.GetId(), Outcome: runwaypb.Outcome_SUCCEEDED, Steps: []*runwaymq.StepResult{{StepId: rs.step.GetStepId(), Outputs: outputs}}, diff --git a/runway/extension/merger/git/git_merger_test.go b/runway/extension/lander/git/git_lander_test.go similarity index 76% rename from runway/extension/merger/git/git_merger_test.go rename to runway/extension/lander/git/git_lander_test.go index b1e6e2224..f5d9587a7 100644 --- a/runway/extension/merger/git/git_merger_test.go +++ b/runway/extension/lander/git/git_lander_test.go @@ -32,11 +32,11 @@ import ( "go.uber.org/zap/zaptest" changepb "github.com/uber/submitqueue/api/base/change/protopb" - mergestrategypb "github.com/uber/submitqueue/api/base/mergestrategy/protopb" + landstrategypb "github.com/uber/submitqueue/api/base/landstrategy/protopb" runwaymq "github.com/uber/submitqueue/api/runway/messagequeue" runwaypb "github.com/uber/submitqueue/api/runway/messagequeue/protopb" gitexectest "github.com/uber/submitqueue/platform/git/exectest" - "github.com/uber/submitqueue/runway/extension/merger" + "github.com/uber/submitqueue/runway/extension/lander" ) const pinnedGitVersion = "2.55.0" @@ -46,11 +46,11 @@ const pinnedGitVersion = "2.55.0" const fakeSHA = "0123456789abcdef0123456789abcdef01234567" // gitFixture provides a bare "remote" repository plus a working checkout that -// the Merger operates on. Tests run real `git` commands so they exercise the +// the Lander operates on. Tests run real `git` commands so they exercise the // same code path as production. // // The fixture also exposes helpers for pushing additional commits to side -// branches on the remote, so each test can build the SHAs it needs the Merger +// branches on the remote, so each test can build the SHAs it needs the Lander // to apply. type gitFixture struct { root string @@ -69,7 +69,7 @@ func setupGitFixture(t *testing.T) gitFixture { mustGit(t, root, "init", "--bare", "-b", "main", remoteDir) - // Seed main with one initial commit so the Merger's reset/fetch flow has + // Seed main with one initial commit so the Lander's reset/fetch flow has // something to land on. mustGit(t, root, "init", "-b", "main", authorDir) configRepo(t, authorDir, "author", "author@example.com") @@ -204,12 +204,12 @@ func TestCherryPickRange_NonConflictFailureIsRetryable(t *testing.T) { // object or an unreadable repository. Reporting those as ErrConflict would // permanently fail a request that a retry could complete. f := setupGitFixture(t) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE).(*gitMerger) + m := f.newLander(t, landstrategypb.Strategy_REBASE).(*gitLander) err := m.cherryPickRange(context.Background(), "notarevision", "alsonotarevision") require.Error(t, err) - assert.False(t, errors.Is(err, merger.ErrConflict), "a non-conflict failure must stay retryable") - assert.False(t, errors.Is(err, merger.ErrInvalidRequest)) + assert.False(t, errors.Is(err, lander.ErrConflict), "a non-conflict failure must stay retryable") + assert.False(t, errors.Is(err, lander.ErrInvalidRequest)) } func TestCherryPickRange_RealConflictIsErrConflict(t *testing.T) { @@ -217,24 +217,24 @@ func TestCherryPickRange_RealConflictIsErrConflict(t *testing.T) { mainBefore, conflictingSHA := f.setupConflict(t) require.NotEmpty(t, mainBefore) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE).(*gitMerger) + m := f.newLander(t, landstrategypb.Strategy_REBASE).(*gitLander) require.NoError(t, m.resetToRemote(context.Background())) require.NoError(t, m.ensureObjects(context.Background(), []changeRef{{SHA: conflictingSHA}})) err := m.cherryPickRange(context.Background(), conflictingSHA+"^", conflictingSHA) require.Error(t, err) - assert.True(t, errors.Is(err, merger.ErrConflict)) + assert.True(t, errors.Is(err, lander.ErrConflict)) assert.True(t, f.checkoutClean(t), "the aborted pick must leave no conflicted index behind") } // --- REBASE --- -func TestMerge_Rebase_SingleChangeSingleURI(t *testing.T) { +func TestLand_Rebase_SingleChangeSingleURI(t *testing.T) { f := setupGitFixture(t) sha := f.pushPRCommit(t, "feature/a", "hello.txt", "hello\nearth\n", "tweak hello") - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) + m := f.newLander(t, landstrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(sha)))) + res, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(sha)))) require.NoError(t, err) assert.Equal(t, runwaypb.Outcome_SUCCEEDED, res.GetOutcome()) require.Len(t, res.GetSteps(), 1) @@ -245,13 +245,13 @@ func TestMerge_Rebase_SingleChangeSingleURI(t *testing.T) { assert.Equal(t, "hello\nearth\n", f.remoteFile(t, "hello.txt")) } -func TestMerge_Rebase_StackedURIsInOneChange(t *testing.T) { +func TestLand_Rebase_StackedURIsInOneChange(t *testing.T) { f := setupGitFixture(t) sha1, sha2 := f.pushStack(t) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", - stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(sha1), uri(sha2)), + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", + stepOf(landstrategypb.Strategy_REBASE, "s1", uri(sha1), uri(sha2)), )) require.NoError(t, err) require.Len(t, res.GetSteps(), 1) @@ -262,36 +262,36 @@ func TestMerge_Rebase_StackedURIsInOneChange(t *testing.T) { assert.Equal(t, "hello\nearth\ngoodbye\n", f.remoteFile(t, "hello.txt")) } -func TestMerge_Rebase_AlreadyLandedChange(t *testing.T) { +func TestLand_Rebase_AlreadyLandedChange(t *testing.T) { f := setupGitFixture(t) sha := f.pushPRCommit(t, "feature/a", "hello.txt", "hello\nearth\n", "tweak hello") - // Land the same content on main outside the Merger so the cherry-pick + // Land the same content on main outside the Lander so the cherry-pick // finds nothing new to add. f.landOnMain(t, sha) mainBefore := f.remoteHEAD(t) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(sha)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(sha)))) require.NoError(t, err) assert.Equal(t, runwaypb.Outcome_SUCCEEDED, res.GetOutcome()) require.Len(t, res.GetSteps(), 1) assert.Empty(t, res.GetSteps()[0].GetOutputs()) assert.Equal(t, mainBefore, f.remoteHEAD(t), - "rebased-out merge should not advance the remote tip") + "rebased-out land should not advance the remote tip") } -func TestMerge_Rebase_MixedChanges(t *testing.T) { +func TestLand_Rebase_MixedChanges(t *testing.T) { t.Run("two steps", func(t *testing.T) { f := setupGitFixture(t) subsumedSHA := f.pushPRCommit(t, "feature/a", "hello.txt", "hello\nearth\n", "tweak hello") f.landOnMain(t, subsumedSHA) freshSHA := f.pushPRCommit(t, "feature/b", "extra.txt", "extra\n", "add extra") - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", - stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(subsumedSHA)), - stepOf(mergestrategypb.Strategy_REBASE, "s2", uri(freshSHA)), + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", + stepOf(landstrategypb.Strategy_REBASE, "s1", uri(subsumedSHA)), + stepOf(landstrategypb.Strategy_REBASE, "s2", uri(freshSHA)), )) require.NoError(t, err) require.Len(t, res.GetSteps(), 2) @@ -306,9 +306,9 @@ func TestMerge_Rebase_MixedChanges(t *testing.T) { f.landOnMain(t, subsumedSHA) freshSHA := f.pushPRCommit(t, "feature/b", "extra.txt", "extra\n", "add extra") - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", - stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(subsumedSHA), uri(freshSHA)), + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", + stepOf(landstrategypb.Strategy_REBASE, "s1", uri(subsumedSHA), uri(freshSHA)), )) require.NoError(t, err) require.Len(t, res.GetSteps(), 1) @@ -318,27 +318,27 @@ func TestMerge_Rebase_MixedChanges(t *testing.T) { }) } -func TestMerge_Rebase_Conflict(t *testing.T) { +func TestLand_Rebase_Conflict(t *testing.T) { f := setupGitFixture(t) mainBefore, conflictingSHA := f.setupConflict(t) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - _, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(conflictingSHA)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + _, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(conflictingSHA)))) require.Error(t, err) - assert.True(t, errors.Is(err, merger.ErrConflict)) + assert.True(t, errors.Is(err, lander.ErrConflict)) assert.Equal(t, mainBefore, f.remoteHEAD(t), "on conflict the remote tip must not move") } -func TestMerge_Rebase_ResetsBetweenCalls(t *testing.T) { +func TestLand_Rebase_ResetsBetweenCalls(t *testing.T) { f := setupGitFixture(t) sha := f.pushPRCommit(t, "feature/a", "hello.txt", "hello\nearth\n", "tweak hello") - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) + m := f.newLander(t, landstrategypb.Strategy_REBASE) // Dirty the checkout so that, without a reset, subsequent operations would // fail or include unrelated changes. require.NoError(t, writeFile(filepath.Join(f.checkoutDir, "stray.txt"), "leftover\n")) - res, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(sha)))) + res, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(sha)))) require.NoError(t, err) require.Len(t, res.GetSteps()[0].GetOutputs(), 1) @@ -348,24 +348,24 @@ func TestMerge_Rebase_ResetsBetweenCalls(t *testing.T) { assert.Contains(t, string(out), "hello.txt") } -func TestMerge_Rebase_RecoversAfterPriorConflict(t *testing.T) { +func TestLand_Rebase_RecoversAfterPriorConflict(t *testing.T) { f := setupGitFixture(t) _, conflictingSHA := f.setupConflict(t) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - _, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(conflictingSHA)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + _, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(conflictingSHA)))) require.Error(t, err) - // A subsequent, clean merge must succeed even though the prior call left a + // A subsequent, clean land must succeed even though the prior call left a // cherry-pick in progress before its rollback. freshSHA := f.pushPRCommit(t, "feature/c", "extra.txt", "extra\n", "add extra") - res, err := m.Merge(context.Background(), req("b2", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(freshSHA)))) + res, err := m.Land(context.Background(), req("b2", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(freshSHA)))) require.NoError(t, err) assert.Equal(t, runwaypb.Outcome_SUCCEEDED, res.GetOutcome()) assert.Equal(t, "extra\n", f.remoteFile(t, "extra.txt")) } -func TestMerge_Rebase_RetriesWhenRemoteMovesUnderUs(t *testing.T) { +func TestLand_Rebase_RetriesWhenRemoteMovesUnderUs(t *testing.T) { f := setupGitFixture(t) // Pre-stage one race commit, install the hook, then build the feature // commit. Order matters: pushPRCommit also goes through the hook, so race + @@ -374,8 +374,8 @@ func TestMerge_Rebase_RetriesWhenRemoteMovesUnderUs(t *testing.T) { featureSHA := f.pushPRCommit(t, "feature/a", "hello.txt", "hello\nearth\n", "tweak hello") f.installRaceHook(t, []string{raceSHA}) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(featureSHA)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(featureSHA)))) require.NoError(t, err) require.Len(t, res.GetSteps(), 1) require.Len(t, res.GetSteps()[0].GetOutputs(), 1) @@ -391,7 +391,7 @@ func TestMerge_Rebase_RetriesWhenRemoteMovesUnderUs(t *testing.T) { assert.Equal(t, "hello\nearth\n", f.remoteFile(t, "hello.txt")) } -func TestMerge_Rebase_GivesUpAfterMaxAttempts(t *testing.T) { +func TestLand_Rebase_GivesUpAfterMaxAttempts(t *testing.T) { f := setupGitFixture(t) raceSHAs := []string{ f.pushPRCommit(t, "race1", "r1.txt", "1\n", "r1"), @@ -400,11 +400,11 @@ func TestMerge_Rebase_GivesUpAfterMaxAttempts(t *testing.T) { featureSHA := f.pushPRCommit(t, "feature/a", "hello.txt", "hello\nearth\n", "tweak hello") f.installRaceHook(t, raceSHAs) - m, err := NewMerger(Params{ + m, err := NewLander(Params{ CheckoutPath: f.checkoutDir, Remote: "origin", Target: "main", - DefaultStrategy: mergestrategypb.Strategy_REBASE, + DefaultStrategy: landstrategypb.Strategy_REBASE, Runtime: testGitRuntime(t), MaxPushAttempts: 2, Logger: zaptest.NewLogger(t).Sugar(), @@ -414,9 +414,9 @@ func TestMerge_Rebase_GivesUpAfterMaxAttempts(t *testing.T) { }) require.NoError(t, err) - _, err = m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(featureSHA)))) + _, err = m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(featureSHA)))) require.Error(t, err) - assert.False(t, errors.Is(err, merger.ErrConflict)) + assert.False(t, errors.Is(err, lander.ErrConflict)) assert.Equal(t, 2, f.hookInvocations(t), "both attempts hit the hook before the cap kicked in") // The remote ended up at race2 (the last hook injection), and our feature // commit never landed. @@ -425,16 +425,16 @@ func TestMerge_Rebase_GivesUpAfterMaxAttempts(t *testing.T) { // --- SQUASH_REBASE --- -func TestMerge_SquashRebase_OneCommitPerChange(t *testing.T) { +func TestLand_SquashRebase_OneCommitPerChange(t *testing.T) { // Two URIs in one change are a stack of two pull requests. Each squashes on // its own — collapsing them together would erase the boundary the stack // exists to express. f := setupGitFixture(t) sha1, sha2 := f.pushStack(t) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", - stepOf(mergestrategypb.Strategy_SQUASH_REBASE, "s1", uri(sha1), uri(sha2)), + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", + stepOf(landstrategypb.Strategy_SQUASH_REBASE, "s1", uri(sha1), uri(sha2)), )) require.NoError(t, err) assert.Equal(t, runwaypb.Outcome_SUCCEEDED, res.GetOutcome()) @@ -448,14 +448,14 @@ func TestMerge_SquashRebase_OneCommitPerChange(t *testing.T) { assert.Equal(t, "hello\nearth\ngoodbye\n", f.remoteFile(t, "hello.txt")) } -func TestMerge_SquashRebase_AlreadyLanded(t *testing.T) { +func TestLand_SquashRebase_AlreadyLanded(t *testing.T) { f := setupGitFixture(t) sha := f.pushPRCommit(t, "feature/a", "hello.txt", "hello\nearth\n", "tweak hello") f.landOnMain(t, sha) mainBefore := f.remoteHEAD(t) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_SQUASH_REBASE, "s1", uri(sha)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_SQUASH_REBASE, "s1", uri(sha)))) require.NoError(t, err) require.Len(t, res.GetSteps(), 1) assert.Empty(t, res.GetSteps()[0].GetOutputs()) @@ -464,12 +464,12 @@ func TestMerge_SquashRebase_AlreadyLanded(t *testing.T) { // --- MERGE --- -func TestMerge_Merge_FreshChangeCreatesMergeCommit(t *testing.T) { +func TestLand_Merge_FreshChangeCreatesMergeCommit(t *testing.T) { f := setupGitFixture(t) freshSHA := f.pushPRCommit(t, "feature/a", "hello.txt", "hello\nearth\n", "tweak hello") - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_MERGE, "s1", uri(freshSHA)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_MERGE, "s1", uri(freshSHA)))) require.NoError(t, err) assert.Equal(t, runwaypb.Outcome_SUCCEEDED, res.GetOutcome()) require.Len(t, res.GetSteps(), 1) @@ -482,14 +482,14 @@ func TestMerge_Merge_FreshChangeCreatesMergeCommit(t *testing.T) { "the original head commit is preserved as the merge's second parent") } -func TestMerge_Merge_AlreadyAncestor(t *testing.T) { +func TestLand_Merge_AlreadyAncestor(t *testing.T) { f := setupGitFixture(t) freshSHA := f.pushPRCommit(t, "feature/a", "hello.txt", "hello\nearth\n", "tweak hello") f.advanceMain(t, freshSHA) // fast-forward main directly to freshSHA mainBefore := f.remoteHEAD(t) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_MERGE, "s1", uri(freshSHA)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_MERGE, "s1", uri(freshSHA)))) require.NoError(t, err) require.Len(t, res.GetSteps(), 1) assert.Empty(t, res.GetSteps()[0].GetOutputs()) @@ -498,7 +498,7 @@ func TestMerge_Merge_AlreadyAncestor(t *testing.T) { // --- PROMOTE --- -func TestMerge_Promote_MultiCommitChange(t *testing.T) { +func TestLand_Promote_MultiCommitChange(t *testing.T) { // PROMOTE moves the ref to the named commit, so a change of any size // arrives whole by construction — its ancestry comes along with it. f := setupGitFixture(t) @@ -508,8 +508,8 @@ func TestMerge_Promote_MultiCommitChange(t *testing.T) { commitSpec{"c.txt", "c\n", "add c"}, ) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_PROMOTE, "s1", uri(head)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_PROMOTE, "s1", uri(head)))) require.NoError(t, err) assert.Equal(t, head, f.remoteHEAD(t), "promote fast-forwards to the exact named commit") assert.Equal(t, "a\n", f.remoteFile(t, "a.txt")) @@ -519,25 +519,25 @@ func TestMerge_Promote_MultiCommitChange(t *testing.T) { assert.Equal(t, head, res.GetSteps()[0].GetOutputs()[0].GetId()) } -func TestMerge_Promote_UnavailableCommitIsInvalidNotRetryable(t *testing.T) { +func TestLand_Promote_UnavailableCommitIsInvalidNotRetryable(t *testing.T) { // promote does not run through tryApply, so it needs its own availability // check; otherwise the containment queries fail with a plain error and the // consumer retries a request that can never succeed. f := setupGitFixture(t) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) + m := f.newLander(t, landstrategypb.Strategy_REBASE) - _, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_PROMOTE, "s1", uri(fakeSHA)))) + _, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_PROMOTE, "s1", uri(fakeSHA)))) require.Error(t, err) - assert.True(t, errors.Is(err, merger.ErrInvalidRequest)) - assert.False(t, errors.Is(err, merger.ErrConflict)) + assert.True(t, errors.Is(err, lander.ErrInvalidRequest)) + assert.False(t, errors.Is(err, lander.ErrConflict)) } -func TestMerge_Promote_FastForward(t *testing.T) { +func TestLand_Promote_FastForward(t *testing.T) { f := setupGitFixture(t) ffSHA := f.pushPRCommit(t, "feature/ff", "hello.txt", "hello\nearth\n", "ff") - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_PROMOTE, "s1", uri(ffSHA)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_PROMOTE, "s1", uri(ffSHA)))) require.NoError(t, err) assert.Equal(t, runwaypb.Outcome_SUCCEEDED, res.GetOutcome()) require.Len(t, res.GetSteps(), 1) @@ -546,21 +546,21 @@ func TestMerge_Promote_FastForward(t *testing.T) { assert.Equal(t, ffSHA, f.remoteHEAD(t)) } -func TestMerge_Promote_AlreadyContained(t *testing.T) { +func TestLand_Promote_AlreadyContained(t *testing.T) { f := setupGitFixture(t) seedSHA := f.remoteSHA(t, "main") advSHA := f.pushPRCommit(t, "feature/adv", "adv.txt", "adv\n", "adv") f.advanceMain(t, advSHA) mainBefore := f.remoteHEAD(t) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_PROMOTE, "s1", uri(seedSHA)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_PROMOTE, "s1", uri(seedSHA)))) require.NoError(t, err) assert.Equal(t, runwaypb.Outcome_SUCCEEDED, res.GetOutcome()) assert.Equal(t, mainBefore, f.remoteHEAD(t), "promoting an already-contained SHA does not move the tip") } -func TestMerge_Promote_Divergent(t *testing.T) { +func TestLand_Promote_Divergent(t *testing.T) { f := setupGitFixture(t) seedSHA := f.remoteSHA(t, "main") divSHA := f.pushPRCommitFrom(t, seedSHA, "feature/div", "div.txt", "div\n", "div") @@ -568,21 +568,21 @@ func TestMerge_Promote_Divergent(t *testing.T) { f.advanceMain(t, otherSHA) mainBefore := f.remoteHEAD(t) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - _, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_PROMOTE, "s1", uri(divSHA)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + _, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_PROMOTE, "s1", uri(divSHA)))) require.Error(t, err) - assert.True(t, errors.Is(err, merger.ErrConflict)) + assert.True(t, errors.Is(err, lander.ErrConflict)) assert.Equal(t, mainBefore, f.remoteHEAD(t)) } // --- DEFAULT --- -func TestMerge_Default_ResolvesToRebase(t *testing.T) { +func TestLand_Default_ResolvesToRebase(t *testing.T) { f := setupGitFixture(t) sha := f.pushPRCommit(t, "feature/a", "hello.txt", "hello\nearth\n", "tweak hello") - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_DEFAULT, "s1", uri(sha)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_DEFAULT, "s1", uri(sha)))) require.NoError(t, err) assert.Equal(t, runwaypb.Outcome_SUCCEEDED, res.GetOutcome()) require.Len(t, res.GetSteps(), 1) @@ -590,9 +590,9 @@ func TestMerge_Default_ResolvesToRebase(t *testing.T) { assert.Equal(t, []string{res.GetSteps()[0].GetOutputs()[0].GetId()}, f.remoteCommitsSinceSeed(t)) } -func TestClassifyMergeFailure(t *testing.T) { +func TestClassifyLandFailure(t *testing.T) { f := setupGitFixture(t) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE).(*gitMerger) + m := f.newLander(t, landstrategypb.Strategy_REBASE).(*gitLander) ref := changeRef{SHA: fakeSHA, Label: "uber/repo#1"} tests := []struct { @@ -623,69 +623,69 @@ func TestClassifyMergeFailure(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := m.classifyMergeFailure(ref, []byte(tt.out), tt.conflicted) + err := m.classifyLandFailure(ref, []byte(tt.out), tt.conflicted) require.Error(t, err) - assert.Equal(t, tt.wantConflict, errors.Is(err, merger.ErrConflict)) - assert.Equal(t, tt.wantInvalid, errors.Is(err, merger.ErrInvalidRequest)) + assert.Equal(t, tt.wantConflict, errors.Is(err, lander.ErrConflict)) + assert.Equal(t, tt.wantInvalid, errors.Is(err, lander.ErrInvalidRequest)) }) } } // --- change provider consistency --- -// newUnrunnableMerger builds a Merger whose git executable does not exist, so -// any git invocation fails loudly. A request rejected by this Merger with +// newUnrunnableLander builds a Lander whose git executable does not exist, so +// any git invocation fails loudly. A request rejected by this Lander with // ErrInvalidRequest was therefore rejected before it reached for git — a // stronger claim than observing that the remote did not move. -func (f gitFixture) newUnrunnableMerger(t *testing.T) merger.Merger { +func (f gitFixture) newUnrunnableLander(t *testing.T) lander.Lander { t.Helper() - return f.newMergerWith(t, func(p *Params) { + return f.newLanderWith(t, func(p *Params) { p.Runtime.Executable = filepath.Join(t.TempDir(), "no-such-git") }) } -func TestMerge_RejectsInconsistentProvider(t *testing.T) { +func TestLand_RejectsInconsistentProvider(t *testing.T) { const otherSHA = "89abcdef0123456789abcdef0123456789abcdef" tests := []struct { name string - req *runwaymq.MergeRequest + req *runwaymq.LandRequest }{ { name: "two steps using different providers", req: req("b", - stepOf(mergestrategypb.Strategy_REBASE, "s1", "github://github.example.com/uber/one/pull/1/"+fakeSHA), - stepOf(mergestrategypb.Strategy_REBASE, "s2", "git://git.example.com/uber/one/refs%2Fheads%2Fmain/"+otherSHA), + stepOf(landstrategypb.Strategy_REBASE, "s1", "github://github.example.com/uber/one/pull/1/"+fakeSHA), + stepOf(landstrategypb.Strategy_REBASE, "s2", "git://git.example.com/uber/one/refs%2Fheads%2Fmain/"+otherSHA), ), }, { name: "one change spanning two providers", - req: req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", + req: req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", "github://github.example.com/uber/one/pull/1/"+fakeSHA, "git://git.example.com/uber/one/refs%2Fheads%2Fmain/"+otherSHA, )), }, { name: "unsupported provider", - req: req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", "phab://phab.example.com/D123/456")), + req: req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", "phab://phab.example.com/D123/456")), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { f := setupGitFixture(t) - m := f.newUnrunnableMerger(t) + m := f.newUnrunnableLander(t) - _, err := m.Merge(context.Background(), tt.req) + _, err := m.Land(context.Background(), tt.req) require.Error(t, err) - assert.True(t, errors.Is(err, merger.ErrInvalidRequest), + assert.True(t, errors.Is(err, lander.ErrInvalidRequest), "want ErrInvalidRequest before any git runs, got %v", err) - assert.False(t, errors.Is(err, merger.ErrConflict)) + assert.False(t, errors.Is(err, lander.ErrConflict)) }) } } -func TestMerge_AcceptsMultipleChangesFromOneProvider(t *testing.T) { +func TestLand_AcceptsMultipleChangesFromOneProvider(t *testing.T) { // Guard against over-rejecting: several steps and several URIs are normal, // so long as they are all addressed through one provider. f := setupGitFixture(t) @@ -693,10 +693,10 @@ func TestMerge_AcceptsMultipleChangesFromOneProvider(t *testing.T) { b := f.pushPRCommit(t, "feature/b", "b.txt", "b\n", "add b") c := f.pushPRCommit(t, "feature/c", "c.txt", "c\n", "add c") - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", - stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(a), uri(b)), - stepOf(mergestrategypb.Strategy_REBASE, "s2", uri(c)), + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", + stepOf(landstrategypb.Strategy_REBASE, "s1", uri(a), uri(b)), + stepOf(landstrategypb.Strategy_REBASE, "s2", uri(c)), )) require.NoError(t, err) assert.Equal(t, runwaypb.Outcome_SUCCEEDED, res.GetOutcome()) @@ -732,7 +732,7 @@ func (f gitFixture) pushImportedHistory(t *testing.T, prNumber int, commits ...c return head, strings.Fields(string(out)) } -func TestMerge_Merge_ImportsUnrelatedHistory(t *testing.T) { +func TestLand_Merge_ImportsUnrelatedHistory(t *testing.T) { f := setupGitFixture(t) head, imported := f.pushImportedHistory(t, 1, commitSpec{"src/lib.go", "v1\n", "micro: initial"}, @@ -741,8 +741,8 @@ func TestMerge_Merge_ImportsUnrelatedHistory(t *testing.T) { ) require.Len(t, imported, 3) - m := f.newMergerWith(t, func(p *Params) { p.AllowUnrelatedHistories = true }) - res, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_MERGE, "s1", uri(head)))) + m := f.newLanderWith(t, func(p *Params) { p.AllowUnrelatedHistories = true }) + res, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_MERGE, "s1", uri(head)))) require.NoError(t, err) assert.Equal(t, runwaypb.Outcome_SUCCEEDED, res.GetOutcome()) @@ -763,44 +763,44 @@ func TestMerge_Merge_ImportsUnrelatedHistory(t *testing.T) { assert.Len(t, f.remoteParents(t, res.GetSteps()[0].GetOutputs()[0].GetId()), 2) } -func TestMerge_Merge_ImportedHistoryRedeliveryIsNoOp(t *testing.T) { +func TestLand_Merge_ImportedHistoryRedeliveryIsNoOp(t *testing.T) { f := setupGitFixture(t) head, _ := f.pushImportedHistory(t, 1, commitSpec{"src/lib.go", "v1\n", "micro: initial"}, commitSpec{"src/lib.go", "v2\n", "micro: feature A"}, ) - m := f.newMergerWith(t, func(p *Params) { p.AllowUnrelatedHistories = true }) - request := req("b", stepOf(mergestrategypb.Strategy_MERGE, "s1", uri(head))) - _, err := m.Merge(context.Background(), request) + m := f.newLanderWith(t, func(p *Params) { p.AllowUnrelatedHistories = true }) + request := req("b", stepOf(landstrategypb.Strategy_MERGE, "s1", uri(head))) + _, err := m.Land(context.Background(), request) require.NoError(t, err) afterFirst := f.remoteHEAD(t) - res, err := m.Merge(context.Background(), request) + res, err := m.Land(context.Background(), request) require.NoError(t, err) assert.Equal(t, afterFirst, f.remoteHEAD(t), "redelivery must not merge the import twice") require.Len(t, res.GetSteps(), 1) assert.Empty(t, res.GetSteps()[0].GetOutputs()) } -func TestMerge_Merge_UnrelatedHistoryRejectedByDefault(t *testing.T) { +func TestLand_Merge_UnrelatedHistoryRejectedByDefault(t *testing.T) { // Without the option the merge is refused. It must not be reported as a - // conflict: nothing collided, the merger simply declined to join two + // conflict: nothing collided, the lander simply declined to join two // unrelated graphs. f := setupGitFixture(t) head, _ := f.pushImportedHistory(t, 1, commitSpec{"src/lib.go", "v1\n", "micro: initial"}) mainBefore := f.remoteHEAD(t) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - _, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_MERGE, "s1", uri(head)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + _, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_MERGE, "s1", uri(head)))) require.Error(t, err) - assert.True(t, errors.Is(err, merger.ErrInvalidRequest)) - assert.False(t, errors.Is(err, merger.ErrConflict)) + assert.True(t, errors.Is(err, lander.ErrInvalidRequest)) + assert.False(t, errors.Is(err, lander.ErrConflict)) assert.Equal(t, mainBefore, f.remoteHEAD(t)) assert.True(t, f.checkoutClean(t)) } -func TestMerge_Rebase_UnrelatedHistoryRejected(t *testing.T) { +func TestLand_Rebase_UnrelatedHistoryRejected(t *testing.T) { // A picking strategy cannot serve a migration at all: there is no range to // compute, and replaying the import commit-by-commit would rewrite every // hash in it — the opposite of preserving the history. @@ -810,23 +810,23 @@ func TestMerge_Rebase_UnrelatedHistoryRejected(t *testing.T) { commitSpec{"src/lib.go", "v2\n", "micro: feature A"}, ) - for _, strategy := range []mergestrategypb.Strategy{ - mergestrategypb.Strategy_REBASE, - mergestrategypb.Strategy_SQUASH_REBASE, + for _, strategy := range []landstrategypb.Strategy{ + landstrategypb.Strategy_REBASE, + landstrategypb.Strategy_SQUASH_REBASE, } { t.Run(strategy.String(), func(t *testing.T) { - m := f.newMergerWith(t, func(p *Params) { p.AllowUnrelatedHistories = true }) - _, err := m.Merge(context.Background(), req("b", stepOf(strategy, "s1", uri(head)))) + m := f.newLanderWith(t, func(p *Params) { p.AllowUnrelatedHistories = true }) + _, err := m.Land(context.Background(), req("b", stepOf(strategy, "s1", uri(head)))) require.Error(t, err) - assert.True(t, errors.Is(err, merger.ErrInvalidRequest)) - assert.False(t, errors.Is(err, merger.ErrConflict)) + assert.True(t, errors.Is(err, lander.ErrInvalidRequest)) + assert.False(t, errors.Is(err, lander.ErrConflict)) }) } } // --- multi-commit changes under the remaining strategies --- -func TestMerge_SquashRebase_MultiCommitChangeSingleURI(t *testing.T) { +func TestLand_SquashRebase_MultiCommitChangeSingleURI(t *testing.T) { f := setupGitFixture(t) head := f.pushMultiCommitPR(t, "feature/multi", commitSpec{"a.txt", "a\n", "add a"}, @@ -834,8 +834,8 @@ func TestMerge_SquashRebase_MultiCommitChangeSingleURI(t *testing.T) { commitSpec{"c.txt", "c\n", "add c"}, ) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_SQUASH_REBASE, "s1", uri(head)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_SQUASH_REBASE, "s1", uri(head)))) require.NoError(t, err) require.Len(t, res.GetSteps(), 1) assert.Len(t, res.GetSteps()[0].GetOutputs(), 1, "a change of any length collapses into one commit") @@ -846,15 +846,15 @@ func TestMerge_SquashRebase_MultiCommitChangeSingleURI(t *testing.T) { assert.Len(t, f.remoteCommitsSinceSeed(t), 1) } -func TestMerge_Merge_MultiCommitChangeSingleURI(t *testing.T) { +func TestLand_Merge_MultiCommitChangeSingleURI(t *testing.T) { f := setupGitFixture(t) head := f.pushMultiCommitPR(t, "feature/multi", commitSpec{"a.txt", "a\n", "add a"}, commitSpec{"b.txt", "b\n", "add b"}, ) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_MERGE, "s1", uri(head)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_MERGE, "s1", uri(head)))) require.NoError(t, err) assert.Equal(t, "a\n", f.remoteFile(t, "a.txt")) assert.Equal(t, "b\n", f.remoteFile(t, "b.txt")) @@ -869,7 +869,7 @@ func TestMerge_Merge_MultiCommitChangeSingleURI(t *testing.T) { // contains, the request names one. Passing the intermediate SHAs as extra URIs // would be testing a shape production cannot produce. -func TestMerge_Rebase_MultiCommitChangeSingleURI(t *testing.T) { +func TestLand_Rebase_MultiCommitChangeSingleURI(t *testing.T) { f := setupGitFixture(t) head := f.pushMultiCommitPR(t, "feature/multi", commitSpec{"a.txt", "a\n", "add a"}, @@ -877,8 +877,8 @@ func TestMerge_Rebase_MultiCommitChangeSingleURI(t *testing.T) { commitSpec{"c.txt", "c\n", "add c"}, ) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(head)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(head)))) require.NoError(t, err) assert.Equal(t, runwaypb.Outcome_SUCCEEDED, res.GetOutcome()) @@ -890,7 +890,7 @@ func TestMerge_Rebase_MultiCommitChangeSingleURI(t *testing.T) { assert.Equal(t, "c\n", f.remoteFile(t, "c.txt")) } -func TestMerge_Rebase_MultiCommitChangeSharedFile(t *testing.T) { +func TestLand_Rebase_MultiCommitChangeSharedFile(t *testing.T) { // Commits that build on each other in the same region: applying only the // head would conflict against context its predecessors establish. f := setupGitFixture(t) @@ -899,14 +899,14 @@ func TestMerge_Rebase_MultiCommitChangeSharedFile(t *testing.T) { commitSpec{"hello.txt", "hello\nworld\nearth\nmars\n", "add mars"}, ) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(head)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(head)))) require.NoError(t, err) assert.Equal(t, runwaypb.Outcome_SUCCEEDED, res.GetOutcome()) assert.Equal(t, "hello\nworld\nearth\nmars\n", f.remoteFile(t, "hello.txt")) } -func TestMerge_Rebase_PartiallyLandedChange(t *testing.T) { +func TestLand_Rebase_PartiallyLandedChange(t *testing.T) { // The change's first commit is already on the target. The range still // covers it; git declines it as redundant and the rest is applied. f := setupGitFixture(t) @@ -917,8 +917,8 @@ func TestMerge_Rebase_PartiallyLandedChange(t *testing.T) { first := strings.TrimSpace(string(mustGitOutput(t, f.authorDir, "rev-parse", head+"^"))) f.landOnMain(t, first) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(head)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(head)))) require.NoError(t, err) require.Len(t, res.GetSteps(), 1) assert.Len(t, res.GetSteps()[0].GetOutputs(), 1, "only the not-yet-landed commit produces output") @@ -926,7 +926,7 @@ func TestMerge_Rebase_PartiallyLandedChange(t *testing.T) { assert.Equal(t, "b\n", f.remoteFile(t, "b.txt")) } -func TestMerge_Rebase_EmptyCommitInRange(t *testing.T) { +func TestLand_Rebase_EmptyCommitInRange(t *testing.T) { f := setupGitFixture(t) head := f.pushMultiCommitPR(t, "feature/empty", commitSpec{"a.txt", "a\n", "add a"}, @@ -934,8 +934,8 @@ func TestMerge_Rebase_EmptyCommitInRange(t *testing.T) { commitSpec{"b.txt", "b\n", "add b"}, ) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(head)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(head)))) require.NoError(t, err) require.Len(t, res.GetSteps(), 1) assert.Len(t, res.GetSteps()[0].GetOutputs(), 2, "the empty commit is skipped, not landed") @@ -943,31 +943,31 @@ func TestMerge_Rebase_EmptyCommitInRange(t *testing.T) { assert.Equal(t, "b\n", f.remoteFile(t, "b.txt")) } -func TestMerge_Rebase_ConflictInRangeLeavesCheckoutUsable(t *testing.T) { +func TestLand_Rebase_ConflictInRangeLeavesCheckoutUsable(t *testing.T) { f := setupGitFixture(t) head := f.pushMultiCommitPR(t, "feature/conflicting", commitSpec{"a.txt", "a\n", "add a"}, commitSpec{"hello.txt", "hello\nmars\n", "diverge hello"}, ) - // Land a conflicting edit to the same file outside the merger. + // Land a conflicting edit to the same file outside the lander. other := f.pushPRCommit(t, "feature/other", "hello.txt", "hello\nvenus\n", "venus") f.landOnMain(t, other) mainBefore := f.remoteHEAD(t) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - _, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(head)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + _, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(head)))) require.Error(t, err) - assert.True(t, errors.Is(err, merger.ErrConflict)) + assert.True(t, errors.Is(err, lander.ErrConflict)) assert.Equal(t, mainBefore, f.remoteHEAD(t), "a conflicting range must not advance the remote") assert.True(t, f.checkoutClean(t), "the aborted range must leave no sequencer state behind") // The checkout is still usable for the next request. clean := f.pushPRCommit(t, "feature/clean", "z.txt", "z\n", "add z") - _, err = m.Merge(context.Background(), req("c", stepOf(mergestrategypb.Strategy_REBASE, "s2", uri(clean)))) + _, err = m.Land(context.Background(), req("c", stepOf(landstrategypb.Strategy_REBASE, "s2", uri(clean)))) require.NoError(t, err) } -func TestMerge_Rebase_StackedMultiCommitChanges(t *testing.T) { +func TestLand_Rebase_StackedMultiCommitChanges(t *testing.T) { // Change B is authored on top of change A, and both carry several commits. // B's range reaches back over A's commits; they are redundant by the time B // applies, so they must be skipped rather than duplicated. @@ -981,10 +981,10 @@ func TestMerge_Rebase_StackedMultiCommitChanges(t *testing.T) { commitSpec{"b2.txt", "b2\n", "b2"}, ) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", - stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(aHead)), - stepOf(mergestrategypb.Strategy_REBASE, "s2", uri(bHead)), + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", + stepOf(landstrategypb.Strategy_REBASE, "s1", uri(aHead)), + stepOf(landstrategypb.Strategy_REBASE, "s2", uri(bHead)), )) require.NoError(t, err) require.Len(t, res.GetSteps(), 2) @@ -1041,16 +1041,16 @@ func TestAuthorIdent(t *testing.T) { } } -func TestMerge_SquashRebase_CreditsChangeAuthor(t *testing.T) { +func TestLand_SquashRebase_CreditsChangeAuthor(t *testing.T) { // A squash mints a fresh commit, so without attribution it would be - // authored by the merger. The person who wrote the change should be its - // author; the merger stays the committer, since it is what applied it. + // authored by the lander. The person who wrote the change should be its + // author; the lander stays the committer, since it is what applied it. f := setupGitFixture(t) sha := f.pushPRCommit(t, "feature/a", "hello.txt", "hello\nearth\n", "tweak hello") - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", - stepOf(mergestrategypb.Strategy_SQUASH_REBASE, "s1", uri(sha)), + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", + stepOf(landstrategypb.Strategy_SQUASH_REBASE, "s1", uri(sha)), )) require.NoError(t, err) require.Equal(t, runwaypb.Outcome_SUCCEEDED, res.GetOutcome()) @@ -1060,15 +1060,15 @@ func TestMerge_SquashRebase_CreditsChangeAuthor(t *testing.T) { assert.Equal(t, "Test ", committer) } -func TestMerge_Merge_CreditsChangeAuthorOnMergeCommit(t *testing.T) { +func TestLand_Merge_CreditsChangeAuthorOnMergeCommit(t *testing.T) { // `git merge` has no --author flag, so the merge commit is the case that // only the environment can attribute. f := setupGitFixture(t) sha := f.pushPRCommit(t, "feature/a", "hello.txt", "hello\nearth\n", "tweak hello") - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", - stepOf(mergestrategypb.Strategy_MERGE, "s1", uri(sha)), + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", + stepOf(landstrategypb.Strategy_MERGE, "s1", uri(sha)), )) require.NoError(t, err) require.Equal(t, runwaypb.Outcome_SUCCEEDED, res.GetOutcome()) @@ -1080,7 +1080,7 @@ func TestMerge_Merge_CreditsChangeAuthorOnMergeCommit(t *testing.T) { assert.Equal(t, "Test ", committer) } -func TestMerge_SquashRebase_CreditsAuthorOfHeadCommit(t *testing.T) { +func TestLand_SquashRebase_CreditsAuthorOfHeadCommit(t *testing.T) { // The URI pins a head SHA, and that commit's author is the one the request // names. A change whose commits have different authors is credited to the // head's, not to whoever happened to start it. @@ -1092,9 +1092,9 @@ func TestMerge_SquashRebase_CreditsAuthorOfHeadCommit(t *testing.T) { name: "Head Author", email: "head@example.com"}, ) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", - stepOf(mergestrategypb.Strategy_SQUASH_REBASE, "s1", uri(head)), + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", + stepOf(landstrategypb.Strategy_SQUASH_REBASE, "s1", uri(head)), )) require.NoError(t, err) require.Equal(t, runwaypb.Outcome_SUCCEEDED, res.GetOutcome()) @@ -1108,7 +1108,7 @@ func TestMerge_SquashRebase_CreditsAuthorOfHeadCommit(t *testing.T) { assert.Equal(t, "package b\n", f.remoteFile(t, "b.go")) } -func TestMerge_Rebase_PreservesEachCommitsOwnAuthor(t *testing.T) { +func TestLand_Rebase_PreservesEachCommitsOwnAuthor(t *testing.T) { // REBASE cherry-picks, which carries the author across on its own. Nothing // collapses, so each commit keeps the person who wrote it rather than // everything being credited to the head's author. @@ -1120,9 +1120,9 @@ func TestMerge_Rebase_PreservesEachCommitsOwnAuthor(t *testing.T) { name: "Head Author", email: "head@example.com"}, ) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", - stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(head)), + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", + stepOf(landstrategypb.Strategy_REBASE, "s1", uri(head)), )) require.NoError(t, err) require.Equal(t, runwaypb.Outcome_SUCCEEDED, res.GetOutcome()) @@ -1139,20 +1139,20 @@ func TestMerge_Rebase_PreservesEachCommitsOwnAuthor(t *testing.T) { // --- object availability and staleness --- -func TestMerge_UnavailableCommitIsInvalidRequestNotConflict(t *testing.T) { +func TestLand_UnavailableCommitIsInvalidRequestNotConflict(t *testing.T) { // A commit the remote cannot supply is a request we can never satisfy. It - // must not be reported as a merge conflict: that would tell the client its + // must not be reported as a land conflict: that would tell the client its // change conflicts when nothing was ever applied. f := setupGitFixture(t) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) + m := f.newLander(t, landstrategypb.Strategy_REBASE) - _, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(fakeSHA)))) + _, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(fakeSHA)))) require.Error(t, err) - assert.True(t, errors.Is(err, merger.ErrInvalidRequest)) - assert.False(t, errors.Is(err, merger.ErrConflict)) + assert.True(t, errors.Is(err, lander.ErrInvalidRequest)) + assert.False(t, errors.Is(err, lander.ErrConflict)) } -func TestMerge_ChangeReachableOnlyViaPRRef(t *testing.T) { +func TestLand_ChangeReachableOnlyViaPRRef(t *testing.T) { // The head is published as a pull-request ref and never as a branch, the // normal shape for a fork PR. The default fetch refspec does not cover it. f := setupGitFixture(t) @@ -1163,14 +1163,14 @@ func TestMerge_ChangeReachableOnlyViaPRRef(t *testing.T) { f.publishPRRef(t, 1, head) mustGit(t, f.authorDir, "push", "origin", "--delete", "feature/forked") - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(head)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(head)))) require.NoError(t, err) require.Len(t, res.GetSteps(), 1) assert.Len(t, res.GetSteps()[0].GetOutputs(), 2) } -func TestMerge_StaleChangeRejected(t *testing.T) { +func TestLand_StaleChangeRejected(t *testing.T) { f := setupGitFixture(t) stale := f.pushPRCommit(t, "feature/s", "s.txt", "v1\n", "v1") f.publishPRRef(t, 1, stale) @@ -1179,25 +1179,25 @@ func TestMerge_StaleChangeRejected(t *testing.T) { current := f.pushPRCommit(t, "feature/s2", "s.txt", "v2\n", "v2") f.publishPRRef(t, 1, current) - m := f.newMergerWith(t, func(p *Params) { p.CheckStaleness = true }) - _, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(stale)))) + m := f.newLanderWith(t, func(p *Params) { p.CheckStaleness = true }) + _, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(stale)))) require.Error(t, err) - assert.True(t, errors.Is(err, merger.ErrInvalidRequest)) + assert.True(t, errors.Is(err, lander.ErrInvalidRequest)) // The same request is accepted when the URI names the current head. - _, err = m.Merge(context.Background(), req("c", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(current)))) + _, err = m.Land(context.Background(), req("c", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(current)))) require.NoError(t, err) } -func TestMerge_StalenessCheckOffByDefault(t *testing.T) { +func TestLand_StalenessCheckOffByDefault(t *testing.T) { f := setupGitFixture(t) stale := f.pushPRCommit(t, "feature/s", "s.txt", "v1\n", "v1") f.publishPRRef(t, 1, stale) current := f.pushPRCommit(t, "feature/s2", "t.txt", "v2\n", "v2") f.publishPRRef(t, 1, current) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - _, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(stale)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + _, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(stale)))) require.NoError(t, err) } @@ -1239,7 +1239,7 @@ func TestResolveChange(t *testing.T) { got, err := resolveChange(tt.uri) if tt.wantErr { require.Error(t, err) - assert.True(t, errors.Is(err, merger.ErrInvalidRequest)) + assert.True(t, errors.Is(err, lander.ErrInvalidRequest)) return } require.NoError(t, err) @@ -1253,10 +1253,10 @@ func TestResolveChange(t *testing.T) { // --- invalid requests --- -func TestMerge_InvalidRequests(t *testing.T) { +func TestLand_InvalidRequests(t *testing.T) { tests := []struct { name string - req *runwaymq.MergeRequest + req *runwaymq.LandRequest }{ { name: "no steps", @@ -1265,40 +1265,40 @@ func TestMerge_InvalidRequests(t *testing.T) { { name: "promote with two steps", req: req("b", - stepOf(mergestrategypb.Strategy_PROMOTE, "s1", uri(fakeSHA)), - stepOf(mergestrategypb.Strategy_PROMOTE, "s2", uri(fakeSHA)), + stepOf(landstrategypb.Strategy_PROMOTE, "s1", uri(fakeSHA)), + stepOf(landstrategypb.Strategy_PROMOTE, "s2", uri(fakeSHA)), ), }, { name: "promote step with two URIs", - req: req("b", stepOf(mergestrategypb.Strategy_PROMOTE, "s1", uri(fakeSHA), uri(fakeSHA))), + req: req("b", stepOf(landstrategypb.Strategy_PROMOTE, "s1", uri(fakeSHA), uri(fakeSHA))), }, { name: "malformed URI", - req: req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", "not a uri")), + req: req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", "not a uri")), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { f := setupGitFixture(t) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - _, err := m.Merge(context.Background(), tt.req) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + _, err := m.Land(context.Background(), tt.req) require.Error(t, err) - assert.True(t, errors.Is(err, merger.ErrInvalidRequest)) + assert.True(t, errors.Is(err, lander.ErrInvalidRequest)) }) } } -// --- CheckMergeability (dry-run) --- +// --- CheckLandability (dry-run) --- -func TestCheckMergeability_RebaseMergeable(t *testing.T) { +func TestCheckLandability_RebaseMergeable(t *testing.T) { f := setupGitFixture(t) sha := f.pushPRCommit(t, "feature/a", "hello.txt", "hello\nearth\n", "tweak hello") mainBefore := f.remoteHEAD(t) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.CheckMergeability(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(sha)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.CheckLandability(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(sha)))) require.NoError(t, err) assert.Equal(t, runwaypb.Outcome_SUCCEEDED, res.GetOutcome()) require.Len(t, res.GetSteps(), 1) @@ -1306,13 +1306,13 @@ func TestCheckMergeability_RebaseMergeable(t *testing.T) { assert.Equal(t, mainBefore, f.remoteHEAD(t), "a dry run does not advance the remote tip") } -func TestCheckMergeability_MergeMergeable(t *testing.T) { +func TestCheckLandability_MergeMergeable(t *testing.T) { f := setupGitFixture(t) sha := f.pushPRCommit(t, "feature/a", "hello.txt", "hello\nearth\n", "tweak hello") mainBefore := f.remoteHEAD(t) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.CheckMergeability(context.Background(), req("b", stepOf(mergestrategypb.Strategy_MERGE, "s1", uri(sha)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.CheckLandability(context.Background(), req("b", stepOf(landstrategypb.Strategy_MERGE, "s1", uri(sha)))) require.NoError(t, err) assert.Equal(t, runwaypb.Outcome_SUCCEEDED, res.GetOutcome()) require.Len(t, res.GetSteps(), 1) @@ -1320,24 +1320,24 @@ func TestCheckMergeability_MergeMergeable(t *testing.T) { assert.Equal(t, mainBefore, f.remoteHEAD(t)) } -func TestCheckMergeability_Conflict(t *testing.T) { +func TestCheckLandability_Conflict(t *testing.T) { f := setupGitFixture(t) mainBefore, conflictingSHA := f.setupConflict(t) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - _, err := m.CheckMergeability(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(conflictingSHA)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + _, err := m.CheckLandability(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(conflictingSHA)))) require.Error(t, err) - assert.True(t, errors.Is(err, merger.ErrConflict)) + assert.True(t, errors.Is(err, lander.ErrConflict)) assert.Equal(t, mainBefore, f.remoteHEAD(t)) } -func TestCheckMergeability_PromoteFastForward(t *testing.T) { +func TestCheckLandability_PromoteFastForward(t *testing.T) { f := setupGitFixture(t) ffSHA := f.pushPRCommit(t, "feature/ff", "hello.txt", "hello\nearth\n", "ff") mainBefore := f.remoteHEAD(t) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - res, err := m.CheckMergeability(context.Background(), req("b", stepOf(mergestrategypb.Strategy_PROMOTE, "s1", uri(ffSHA)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + res, err := m.CheckLandability(context.Background(), req("b", stepOf(landstrategypb.Strategy_PROMOTE, "s1", uri(ffSHA)))) require.NoError(t, err) assert.Equal(t, runwaypb.Outcome_SUCCEEDED, res.GetOutcome()) require.Len(t, res.GetSteps(), 1) @@ -1345,7 +1345,7 @@ func TestCheckMergeability_PromoteFastForward(t *testing.T) { assert.Equal(t, mainBefore, f.remoteHEAD(t)) } -func TestCheckMergeability_PromoteDivergent(t *testing.T) { +func TestCheckLandability_PromoteDivergent(t *testing.T) { f := setupGitFixture(t) seedSHA := f.remoteSHA(t, "main") divSHA := f.pushPRCommitFrom(t, seedSHA, "feature/div", "div.txt", "div\n", "div") @@ -1353,10 +1353,10 @@ func TestCheckMergeability_PromoteDivergent(t *testing.T) { f.advanceMain(t, otherSHA) mainBefore := f.remoteHEAD(t) - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - _, err := m.CheckMergeability(context.Background(), req("b", stepOf(mergestrategypb.Strategy_PROMOTE, "s1", uri(divSHA)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + _, err := m.CheckLandability(context.Background(), req("b", stepOf(landstrategypb.Strategy_PROMOTE, "s1", uri(divSHA)))) require.Error(t, err) - assert.True(t, errors.Is(err, merger.ErrConflict)) + assert.True(t, errors.Is(err, lander.ErrConflict)) assert.Equal(t, mainBefore, f.remoteHEAD(t)) } @@ -1367,21 +1367,21 @@ func TestPinnedGitVersion(t *testing.T) { // --- request builders --- -// req builds a MergeRequest with the given correlation id and ordered steps. -func req(id string, steps ...*runwaymq.MergeStep) *runwaymq.MergeRequest { - return &runwaymq.MergeRequest{Id: id, Steps: steps} +// req builds a LandRequest with the given correlation id and ordered steps. +func req(id string, steps ...*runwaymq.LandStep) *runwaymq.LandRequest { + return &runwaymq.LandRequest{Id: id, Steps: steps} } // stepOf builds a step whose change carries the given URIs. -func stepOf(strategy mergestrategypb.Strategy, stepID string, uris ...string) *runwaymq.MergeStep { - return &runwaymq.MergeStep{ +func stepOf(strategy landstrategypb.Strategy, stepID string, uris ...string) *runwaymq.LandStep { + return &runwaymq.LandStep{ StepId: stepID, Strategy: strategy, Change: &changepb.Change{Uris: uris}, } } -// --- merger + fixture helpers --- +// --- lander + fixture helpers --- // remoteHasCommit reports whether sha is reachable in the remote repository. func (f gitFixture) remoteHasCommit(t *testing.T, sha string) bool { @@ -1445,15 +1445,15 @@ func (f gitFixture) publishPRRef(t *testing.T, n int, sha string) { fmt.Sprintf("%s:refs/pull/%d/head", sha, n)) } -// newMergerWith builds a Merger with non-default Params, for the options that +// newLanderWith builds a Lander with non-default Params, for the options that // only some tests exercise. -func (f gitFixture) newMergerWith(t *testing.T, mutate func(*Params)) merger.Merger { +func (f gitFixture) newLanderWith(t *testing.T, mutate func(*Params)) lander.Lander { t.Helper() p := Params{ CheckoutPath: f.checkoutDir, Remote: "origin", Target: "main", - DefaultStrategy: mergestrategypb.Strategy_REBASE, + DefaultStrategy: landstrategypb.Strategy_REBASE, Runtime: testGitRuntime(t), Logger: zaptest.NewLogger(t).Sugar(), MetricsScope: tally.NoopScope, @@ -1461,12 +1461,12 @@ func (f gitFixture) newMergerWith(t *testing.T, mutate func(*Params)) merger.Mer CommitterEmail: "test@example.com", } mutate(&p) - m, err := NewMerger(p) + m, err := NewLander(p) require.NoError(t, err) return m } -// checkoutClean reports whether the merger's checkout has no in-progress +// checkoutClean reports whether the lander's checkout has no in-progress // operation and no dirty state — what must hold after any failed apply. func (f gitFixture) checkoutClean(t *testing.T) bool { t.Helper() @@ -1482,9 +1482,9 @@ func (f gitFixture) checkoutClean(t *testing.T) bool { return true } -func (f gitFixture) newMerger(t *testing.T, defaultStrategy mergestrategypb.Strategy) merger.Merger { +func (f gitFixture) newLander(t *testing.T, defaultStrategy landstrategypb.Strategy) lander.Lander { t.Helper() - m, err := NewMerger(Params{ + m, err := NewLander(Params{ CheckoutPath: f.checkoutDir, Remote: "origin", Target: "main", @@ -1559,7 +1559,7 @@ func (f gitFixture) pushStack(t *testing.T) (string, string) { return sha1, sha2 } -// setupConflict lands "earth" on main and returns the pre-merge main SHA plus a +// setupConflict lands "earth" on main and returns the pre-land main SHA plus a // divergent "mars" SHA that conflicts when applied onto the new main. func (f gitFixture) setupConflict(t *testing.T) (mainBefore, conflictingSHA string) { t.Helper() @@ -1597,7 +1597,7 @@ func (f gitFixture) advanceMain(t *testing.T, sha string) { mustGit(t, f.authorDir, "push", "origin", sha+":main") } -// uri builds a github-format URI ending in `sha` so the Merger's parser resolves +// uri builds a github-format URI ending in `sha` so the Lander's parser resolves // it to that SHA. func uri(sha string) string { return fmt.Sprintf("github://github.example.com/uber/submitqueue/pull/1/%s", sha) @@ -1609,7 +1609,7 @@ func uri(sha string) string { // current push). Once race-shas is exhausted it exits 0 and the push goes // through. // -// Combined with the gitMerger's contention-detection (refetch + compare base +// Combined with the gitLander's contention-detection (refetch + compare base // SHA), this lets a test drive the full retry loop using only real git // mechanics — the second-attempt's reset picks up the moved tip and proceeds // from there. @@ -1626,7 +1626,7 @@ func (f gitFixture) installRaceHook(t *testing.T, raceSHAs []string) { strings.Join(raceSHAs, "\n")+"\n", )) const script = `#!/bin/sh -# Contend only on the target branch. A merge that moves change head branches +# Contend only on the target branch. A land that moves change head branches # pushes those first, and they are not what this hook simulates a race for. target_pushed=0 while read -r _old _new ref; do @@ -1730,7 +1730,7 @@ func (f gitFixture) parents(t *testing.T, sha string) []string { } // remoteIdent returns the author and committer recorded on a commit, each -// formatted "Name ". The two differ whenever the merger creates a commit +// formatted "Name ". The two differ whenever the lander creates a commit // for someone else's change. func (f gitFixture) remoteIdent(t *testing.T, sha string) (author, committer string) { t.Helper() diff --git a/runway/extension/merger/git/headbranch.go b/runway/extension/lander/git/headbranch.go similarity index 93% rename from runway/extension/merger/git/headbranch.go rename to runway/extension/lander/git/headbranch.go index 49c54c456..4df1a0c44 100644 --- a/runway/extension/merger/git/headbranch.go +++ b/runway/extension/lander/git/headbranch.go @@ -39,7 +39,7 @@ type headUpdate struct { } // headBranchTracker records, per change, the branch its head was found on and -// the commit this merger last pushed that branch to. It is keyed by the commit +// the commit this lander last pushed that branch to. It is keyed by the commit // the change's URI pins, which is fixed for the whole request and so survives // the retries the value has to cross. // @@ -51,7 +51,7 @@ type headUpdate struct { // the next lease must name — is only available from here. type headBranchTracker map[string]trackedBranch -// trackedBranch is one change's resolved head branch and the commit this merger +// trackedBranch is one change's resolved head branch and the commit this lander // last pushed it to. type trackedBranch struct { // branch is the fully-qualified ref resolved on the first attempt. @@ -87,20 +87,20 @@ func (t headBranchTracker) record(pinnedSHA, branch, sha string) { // became restores it — but only if the provider has already recorded the new // head by the time it sees the target move. That is why this is a separate, // earlier push: moving the branch after the target has been pushed, or in the -// same atomic push, both leave the provider comparing against the pre-merge head +// same atomic push, both leave the provider comparing against the pre-land head // and it records the change as closed rather than merged. // // This is deliberately provider-neutral. It matches a SHA against the remote's // branch tips rather than parsing a change number or calling an API, so it works // the same for a GitHub pull request, a GitLab merge request, or a plain branch. // -// A failure to move a branch fails the merge, before the target is pushed. +// A failure to move a branch fails the land, before the target is pushed. // Landing a change while knowing its head could not be moved produces exactly -// the half-merged state the option exists to prevent, so the merge stops instead +// the partially landed state the option exists to prevent, so the land stops instead // of completing into it. Having nothing to move is not a failure: a change // proposed from a fork, or one whose head several branches share, is skipped and -// the merge carries on. -func (m *gitMerger) updateHeadBranches(ctx context.Context, updates []headUpdate, tracked headBranchTracker) error { +// the land carries on. +func (m *gitLander) updateHeadBranches(ctx context.Context, updates []headUpdate, tracked headBranchTracker) error { // Resolved lazily: an attempt whose changes were all resolved by an earlier // one needs no advertisement, and asking for one only adds a way to fail. var tips map[string][]string @@ -125,7 +125,7 @@ func (m *gitMerger) updateHeadBranches(ctx context.Context, updates []headUpdate // updateHeadBranchFor moves one change's head branch. It reports an error only // when a move was attempted and failed; a change with no branch of ours to move // is a skip, not a failure. -func (m *gitMerger) updateHeadBranchFor(ctx context.Context, u headUpdate, tips map[string][]string, tracked headBranchTracker) error { +func (m *gitLander) updateHeadBranchFor(ctx context.Context, u headUpdate, tips map[string][]string, tracked headBranchTracker) error { if u.newSHA == "" || u.newSHA == u.ref.SHA { return nil } @@ -161,7 +161,7 @@ func (m *gitMerger) updateHeadBranchFor(ctx context.Context, u headUpdate, tips // --force-with-lease names the remote ref and the value it must still hold — // the SHA the change's URI pinned, or, once an earlier attempt has moved the // branch, whatever that attempt left it at. An author who pushed while the - // batch was building fails this check, and the merge stops rather than + // batch was building fails this check, and the land stops rather than // discarding their push. The explicit form is what makes the check // independent of whatever stale remote-tracking refs this checkout holds. leaseArg := fmt.Sprintf("--force-with-lease=%s:%s", branch, lease) @@ -184,8 +184,8 @@ func (m *gitMerger) updateHeadBranchFor(ctx context.Context, u headUpdate, tips // // The target branch is excluded. It is not a change's head branch, and a change // whose head happens to equal the target tip would otherwise make this rewrite -// the very branch the merge just landed on. -func (m *gitMerger) remoteBranchTips(ctx context.Context) (map[string][]string, error) { +// the very branch the operation just landed on. +func (m *gitLander) remoteBranchTips(ctx context.Context) (map[string][]string, error) { out, err := m.run(ctx, nil, "ls-remote", "--heads", m.remote) if err != nil { return nil, fmt.Errorf("git ls-remote --heads %s: %w", m.remote, err) diff --git a/runway/extension/merger/git/headbranch_test.go b/runway/extension/lander/git/headbranch_test.go similarity index 73% rename from runway/extension/merger/git/headbranch_test.go rename to runway/extension/lander/git/headbranch_test.go index c5233e136..ed5c953b4 100644 --- a/runway/extension/merger/git/headbranch_test.go +++ b/runway/extension/lander/git/headbranch_test.go @@ -24,7 +24,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - mergestrategypb "github.com/uber/submitqueue/api/base/mergestrategy/protopb" + landstrategypb "github.com/uber/submitqueue/api/base/landstrategy/protopb" ) // uriPR builds a change URI for a specific pull request number, so a test can @@ -46,26 +46,26 @@ func (f gitFixture) branchSHA(t *testing.T, branch string) (string, bool) { return strings.TrimSpace(string(out)), true } -// mergerWithHeadBranchUpdates builds a merger that moves change head branches -// after a committing merge. -func (f gitFixture) mergerWithHeadBranchUpdates(t *testing.T, strategy mergestrategypb.Strategy) *gitMerger { +// landerWithHeadBranchUpdates builds a lander that moves change head branches +// after a committing land. +func (f gitFixture) landerWithHeadBranchUpdates(t *testing.T, strategy landstrategypb.Strategy) *gitLander { t.Helper() - m := f.newMergerWith(t, func(p *Params) { + m := f.newLanderWith(t, func(p *Params) { p.DefaultStrategy = strategy p.UpdateHeadBranch = true }) - return m.(*gitMerger) + return m.(*gitLander) } -func TestMerge_Rebase_MovesHeadBranchToLandedCommit(t *testing.T) { +func TestLand_Rebase_MovesHeadBranchToLandedCommit(t *testing.T) { // The rebased commit is a new object, so nothing on the target names the // change's original head. Moving the branch to the commit the change became // is what lets a provider see it as merged. f := setupGitFixture(t) head := f.pushPRCommit(t, "feature/a", "a.txt", "a\n", "add a") - m := f.mergerWithHeadBranchUpdates(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(head)))) + m := f.landerWithHeadBranchUpdates(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(head)))) require.NoError(t, err) landed := res.GetSteps()[0].GetOutputs()[0].GetId() @@ -77,12 +77,12 @@ func TestMerge_Rebase_MovesHeadBranchToLandedCommit(t *testing.T) { assert.Equal(t, landed, got) } -func TestMerge_HeadBranchUntouchedWhenDisabled(t *testing.T) { +func TestLand_HeadBranchUntouchedWhenDisabled(t *testing.T) { f := setupGitFixture(t) head := f.pushPRCommit(t, "feature/a", "a.txt", "a\n", "add a") - m := f.newMerger(t, mergestrategypb.Strategy_REBASE) - _, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(head)))) + m := f.newLander(t, landstrategypb.Strategy_REBASE) + _, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(head)))) require.NoError(t, err) got, ok := f.branchSHA(t, "feature/a") @@ -90,15 +90,15 @@ func TestMerge_HeadBranchUntouchedWhenDisabled(t *testing.T) { assert.Equal(t, head, got, "branch must not move unless the deployment opted in") } -func TestMerge_SquashRebase_MovesHeadBranchToSquashedCommit(t *testing.T) { +func TestLand_SquashRebase_MovesHeadBranchToSquashedCommit(t *testing.T) { f := setupGitFixture(t) head := f.pushMultiCommitPR(t, "feature/sq", commitSpec{"a.txt", "a\n", "add a"}, commitSpec{"b.txt", "b\n", "add b"}, ) - m := f.mergerWithHeadBranchUpdates(t, mergestrategypb.Strategy_SQUASH_REBASE) - res, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_SQUASH_REBASE, "s1", uri(head)))) + m := f.landerWithHeadBranchUpdates(t, landstrategypb.Strategy_SQUASH_REBASE) + res, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_SQUASH_REBASE, "s1", uri(head)))) require.NoError(t, err) outputs := res.GetSteps()[0].GetOutputs() @@ -110,16 +110,16 @@ func TestMerge_SquashRebase_MovesHeadBranchToSquashedCommit(t *testing.T) { assert.Equal(t, squashed, got) } -func TestMerge_Rebase_MovesEachHeadBranchOfAStack(t *testing.T) { +func TestLand_Rebase_MovesEachHeadBranchOfAStack(t *testing.T) { // Each change in a stack keeps its own identity on the target, so each head // branch has to land on its own commit rather than all on the final tip. f := setupGitFixture(t) first := f.pushPRCommit(t, "feature/one", "one.txt", "one\n", "add one") second := f.pushPRCommit(t, "feature/two", "two.txt", "two\n", "add two") - m := f.mergerWithHeadBranchUpdates(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", - stepOf(mergestrategypb.Strategy_REBASE, "s1", uriPR(1, first), uriPR(2, second)), + m := f.landerWithHeadBranchUpdates(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", + stepOf(landstrategypb.Strategy_REBASE, "s1", uriPR(1, first), uriPR(2, second)), )) require.NoError(t, err) @@ -139,7 +139,7 @@ func TestMerge_Rebase_MovesEachHeadBranchOfAStack(t *testing.T) { assert.Equal(t, landedSecond, f.remoteHEAD(t)) } -func TestMerge_HeadBranchSkippedForForkChange(t *testing.T) { +func TestLand_HeadBranchSkippedForForkChange(t *testing.T) { // A change proposed from a fork has no branch in this repository — only the // provider's read-only change ref. The land must still succeed, and nothing // here is ours to move. @@ -148,8 +148,8 @@ func TestMerge_HeadBranchSkippedForForkChange(t *testing.T) { f.publishPRRef(t, 1, head) mustGit(t, f.authorDir, "push", "origin", "--delete", "feature/forked") - m := f.mergerWithHeadBranchUpdates(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(head)))) + m := f.landerWithHeadBranchUpdates(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(head)))) require.NoError(t, err) assert.Equal(t, res.GetSteps()[0].GetOutputs()[0].GetId(), f.remoteHEAD(t)) @@ -157,15 +157,15 @@ func TestMerge_HeadBranchSkippedForForkChange(t *testing.T) { assert.False(t, ok, "no branch should be resurrected for a fork change") } -func TestMerge_HeadBranchSkippedWhenAmbiguous(t *testing.T) { +func TestLand_HeadBranchSkippedWhenAmbiguous(t *testing.T) { // Two branches sit on the change's head and the URI does not say which one // it was proposed from. Rewriting a guess could clobber an unrelated branch. f := setupGitFixture(t) head := f.pushPRCommit(t, "feature/a", "a.txt", "a\n", "add a") mustGit(t, f.authorDir, "push", "origin", head+":refs/heads/feature/a-copy") - m := f.mergerWithHeadBranchUpdates(t, mergestrategypb.Strategy_REBASE) - _, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(head)))) + m := f.landerWithHeadBranchUpdates(t, landstrategypb.Strategy_REBASE) + _, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(head)))) require.NoError(t, err) for _, branch := range []string{"feature/a", "feature/a-copy"} { @@ -175,14 +175,14 @@ func TestMerge_HeadBranchSkippedWhenAmbiguous(t *testing.T) { } } -func TestMerge_Merge_LeavesHeadBranchAlone(t *testing.T) { +func TestLand_Merge_LeavesHeadBranchAlone(t *testing.T) { // MERGE keeps the change's own head reachable from the target through // second-parent history, so the branch already satisfies the provider. f := setupGitFixture(t) head := f.pushPRCommit(t, "feature/a", "a.txt", "a\n", "add a") - m := f.mergerWithHeadBranchUpdates(t, mergestrategypb.Strategy_MERGE) - _, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_MERGE, "s1", uri(head)))) + m := f.landerWithHeadBranchUpdates(t, landstrategypb.Strategy_MERGE) + _, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_MERGE, "s1", uri(head)))) require.NoError(t, err) got, ok := f.branchSHA(t, "feature/a") @@ -190,15 +190,15 @@ func TestMerge_Merge_LeavesHeadBranchAlone(t *testing.T) { assert.Equal(t, head, got) } -func TestMerge_HeadBranchUntouchedForAlreadyLandedChange(t *testing.T) { +func TestLand_HeadBranchUntouchedForAlreadyLandedChange(t *testing.T) { // The change contributed no commits, so there is nothing for its branch to // move to. Redelivery must not disturb it. f := setupGitFixture(t) head := f.pushPRCommit(t, "feature/a", "a.txt", "a\n", "add a") f.advanceMain(t, head) - m := f.mergerWithHeadBranchUpdates(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(head)))) + m := f.landerWithHeadBranchUpdates(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(head)))) require.NoError(t, err) assert.Empty(t, res.GetSteps()[0].GetOutputs()) @@ -207,14 +207,14 @@ func TestMerge_HeadBranchUntouchedForAlreadyLandedChange(t *testing.T) { assert.Equal(t, head, got) } -func TestCheckMergeability_DoesNotMoveHeadBranch(t *testing.T) { +func TestCheckLandability_DoesNotMoveHeadBranch(t *testing.T) { // A dry run commits nothing, so it has nothing to point a branch at. f := setupGitFixture(t) head := f.pushPRCommit(t, "feature/a", "a.txt", "a\n", "add a") mainBefore := f.remoteHEAD(t) - m := f.mergerWithHeadBranchUpdates(t, mergestrategypb.Strategy_REBASE) - _, err := m.CheckMergeability(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(head)))) + m := f.landerWithHeadBranchUpdates(t, landstrategypb.Strategy_REBASE) + _, err := m.CheckLandability(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(head)))) require.NoError(t, err) assert.Equal(t, mainBefore, f.remoteHEAD(t)) @@ -227,7 +227,7 @@ func TestUpdateHeadBranchFor_StaleLeaseFailsAndLeavesBranchAlone(t *testing.T) { // The guard against the window between reading the remote's branches and // pushing: if the author moved the branch in between, the lease must refuse // rather than discard their push. Driven directly, since the race is not - // reproducible through Merge. + // reproducible through Land. f := setupGitFixture(t) pinned := f.pushPRCommit(t, "feature/a", "a.txt", "a\n", "add a") moved := f.pushPRCommit(t, "feature/a", "a.txt", "a2\n", "author pushed again") @@ -235,7 +235,7 @@ func TestUpdateHeadBranchFor_StaleLeaseFailsAndLeavesBranchAlone(t *testing.T) { landed := f.pushPRCommit(t, "feature/other", "z.txt", "z\n", "some landed commit") - m := f.mergerWithHeadBranchUpdates(t, mergestrategypb.Strategy_REBASE) + m := f.landerWithHeadBranchUpdates(t, landstrategypb.Strategy_REBASE) // A stale view of the remote: it claims feature/a is still at the pinned // SHA, which is exactly what a racing push invalidates. stale := map[string][]string{pinned: {"refs/heads/feature/a"}} @@ -244,14 +244,14 @@ func TestUpdateHeadBranchFor_StaleLeaseFailsAndLeavesBranchAlone(t *testing.T) { stale, nil, ) - require.Error(t, err, "a refused lease must fail the merge, not be swallowed") + require.Error(t, err, "a refused lease must fail the land, not be swallowed") got, ok := f.branchSHA(t, "feature/a") require.True(t, ok) assert.Equal(t, moved, got, "the author's push must survive") } -func TestMerge_HeadBranchMovesBeforeTheTargetIsPushed(t *testing.T) { +func TestLand_HeadBranchMovesBeforeTheTargetIsPushed(t *testing.T) { // Ordering is the whole mechanism: a provider compares a change's recorded // head against the target push while processing it, so the head has to be // recorded first. Observed by failing the target push and checking the head @@ -262,13 +262,13 @@ func TestMerge_HeadBranchMovesBeforeTheTargetIsPushed(t *testing.T) { race := f.pushPRCommit(t, "race", "race.txt", "race\n", "race commit") f.installRaceHook(t, []string{race}) - m := f.newMergerWith(t, func(p *Params) { - p.DefaultStrategy = mergestrategypb.Strategy_REBASE + m := f.newLanderWith(t, func(p *Params) { + p.DefaultStrategy = landstrategypb.Strategy_REBASE p.UpdateHeadBranch = true p.MaxPushAttempts = 1 }) - _, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(head)))) - require.Error(t, err, "the target push was rejected, so the merge failed") + _, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(head)))) + require.Error(t, err, "the target push was rejected, so the land failed") got, ok := f.branchSHA(t, "feature/a") require.True(t, ok) @@ -276,7 +276,7 @@ func TestMerge_HeadBranchMovesBeforeTheTargetIsPushed(t *testing.T) { assert.NotEqual(t, mainBefore, f.remoteHEAD(t), "the hook moved the target out from under us") } -func TestMerge_HeadBranchMovedAgainAfterTargetContention(t *testing.T) { +func TestLand_HeadBranchMovedAgainAfterTargetContention(t *testing.T) { // An attempt that moved the branch and then lost the target push leaves it on // a commit that never landed. The retry has to move it on to the commit that // did — it can no longer be found by matching the SHA the URI pinned. @@ -285,8 +285,8 @@ func TestMerge_HeadBranchMovedAgainAfterTargetContention(t *testing.T) { head := f.pushPRCommit(t, "feature/a", "a.txt", "a\n", "add a") f.installRaceHook(t, []string{race}) - m := f.mergerWithHeadBranchUpdates(t, mergestrategypb.Strategy_REBASE) - res, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(head)))) + m := f.landerWithHeadBranchUpdates(t, landstrategypb.Strategy_REBASE) + res, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(head)))) require.NoError(t, err) assert.Equal(t, 2, f.hookInvocations(t), "first target push rejected, second allowed through") @@ -298,17 +298,17 @@ func TestMerge_HeadBranchMovedAgainAfterTargetContention(t *testing.T) { assert.Equal(t, landed, got, "the branch follows the commit that actually landed") } -func TestMerge_HeadBranchPushFailureFailsTheLand(t *testing.T) { +func TestLand_HeadBranchPushFailureFailsTheLand(t *testing.T) { // Landing a change while knowing its head could not be moved produces exactly - // the half-merged state the option exists to prevent, so the merge stops + // the half-landed state the option exists to prevent, so the land stops // before the target is pushed rather than completing into it. f := setupGitFixture(t) head := f.pushPRCommit(t, "feature/a", "a.txt", "a\n", "add a") mainBefore := f.remoteHEAD(t) f.installRefRejectHook(t, "refs/heads/feature/a") - m := f.mergerWithHeadBranchUpdates(t, mergestrategypb.Strategy_REBASE) - _, err := m.Merge(context.Background(), req("b", stepOf(mergestrategypb.Strategy_REBASE, "s1", uri(head)))) + m := f.landerWithHeadBranchUpdates(t, landstrategypb.Strategy_REBASE) + _, err := m.Land(context.Background(), req("b", stepOf(landstrategypb.Strategy_REBASE, "s1", uri(head)))) require.Error(t, err) assert.Equal(t, mainBefore, f.remoteHEAD(t), "the target must not have been pushed") @@ -323,7 +323,7 @@ func TestRemoteBranchTips(t *testing.T) { mustGit(t, f.authorDir, "push", "origin", head+":refs/heads/feature/a-copy") f.publishPRRef(t, 7, head) - m := f.mergerWithHeadBranchUpdates(t, mergestrategypb.Strategy_REBASE) + m := f.landerWithHeadBranchUpdates(t, landstrategypb.Strategy_REBASE) tips, err := m.remoteBranchTips(context.Background()) require.NoError(t, err) @@ -334,7 +334,7 @@ func TestRemoteBranchTips(t *testing.T) { ) // The target is excluded: a change whose head happened to equal the target - // tip would otherwise make the merger rewrite the branch it just landed on. + // tip would otherwise make the lander rewrite the branch it just landed on. for sha, refs := range tips { assert.NotContains(t, refs, "refs/heads/main", "target must never be a candidate (sha %s)", sha) } diff --git a/runway/extension/merger/git/objects.go b/runway/extension/lander/git/objects.go similarity index 85% rename from runway/extension/merger/git/objects.go rename to runway/extension/lander/git/objects.go index 3a919e5c5..d1eea2ccb 100644 --- a/runway/extension/merger/git/objects.go +++ b/runway/extension/lander/git/objects.go @@ -20,7 +20,7 @@ import ( "strings" coremetrics "github.com/uber/submitqueue/platform/metrics" - "github.com/uber/submitqueue/runway/extension/merger" + "github.com/uber/submitqueue/runway/extension/lander" ) // ensureObjects makes every commit the request references available locally, @@ -31,8 +31,8 @@ import ( // cover a provider's change refs: a pull request head that was never also // pushed as a branch (the normal case for a fork) is simply absent locally, and // the apply would then fail with git's "bad object" — indistinguishable, to the -// apply paths, from a merge conflict. -func (m *gitMerger) ensureObjects(ctx context.Context, refs []changeRef) error { +// apply paths, from a land conflict. +func (m *gitLander) ensureObjects(ctx context.Context, refs []changeRef) error { for _, ref := range refs { if err := m.ensureObject(ctx, ref); err != nil { return err @@ -48,7 +48,7 @@ func (m *gitMerger) ensureObjects(ctx context.Context, refs []changeRef) error { // .allowReachableSHA1InWant, which github.com enables). The provider's // canonical ref is the fallback for a server that does not. Neither fetch is // shallow: the apply paths need the commit's ancestry, not just the commit. -func (m *gitMerger) ensureObject(ctx context.Context, ref changeRef) error { +func (m *gitLander) ensureObject(ctx context.Context, ref changeRef) error { if m.hasCommit(ctx, ref.SHA) { return nil } @@ -69,22 +69,22 @@ func (m *gitMerger) ensureObject(ctx context.Context, ref changeRef) error { return fmt.Errorf("remote %s unreachable while resolving commit %s: %w", m.remote, ref.SHA, err) } - coremetrics.NamedCounter(m.metricsScope, "merge", "object_unavailable", 1) + coremetrics.NamedCounter(m.metricsScope, "land", "object_unavailable", 1) // Name the change, not just the commit. A bare "commit not available" // sends the reader looking for a deleted commit, when the likelier cause is // a change this remote was never going to be able to serve. return fmt.Errorf("%w: commit %s of %s is not available from remote %s (tried by SHA and via %q)", - merger.ErrInvalidRequest, ref.SHA, ref.Label, m.remote, ref.Ref) + lander.ErrInvalidRequest, ref.SHA, ref.Label, m.remote, ref.Ref) } // hasCommit reports whether sha names a commit object in the local checkout. -func (m *gitMerger) hasCommit(ctx context.Context, sha string) bool { +func (m *gitLander) hasCommit(ctx context.Context, sha string) bool { _, err := m.run(ctx, nil, "cat-file", "-e", sha+"^{commit}") return err == nil } // checkStale reports whether any change has been superseded since its URI was -// minted. Fetching by SHA guarantees the merger applies exactly the commit the +// minted. Fetching by SHA guarantees the lander applies exactly the commit the // URI names, but not that the commit is still the change's head — a force-push // leaves the old commit fetchable for some time on most hosts, so a successful // fetch says nothing about freshness. @@ -93,7 +93,7 @@ func (m *gitMerger) hasCommit(ctx context.Context, sha string) bool { // that no longer exists yields no verdict rather than a failure: the change may // legitimately have been closed, and ensureObjects has already established that // the commit itself is present. -func (m *gitMerger) checkStale(ctx context.Context, refs []changeRef) error { +func (m *gitLander) checkStale(ctx context.Context, refs []changeRef) error { if !m.checkStaleness { return nil } @@ -110,9 +110,9 @@ func (m *gitMerger) checkStale(ctx context.Context, refs []changeRef) error { continue } if current := fields[0]; current != ref.SHA { - coremetrics.NamedCounter(m.metricsScope, "merge", "stale_changes", 1) + coremetrics.NamedCounter(m.metricsScope, "land", "stale_changes", 1) return fmt.Errorf("%w: change is stale: %s names commit %s but %s now points at %s", - merger.ErrInvalidRequest, ref.Label, ref.SHA, ref.Ref, current) + lander.ErrInvalidRequest, ref.Label, ref.SHA, ref.Ref, current) } } return nil diff --git a/runway/extension/merger/merger.go b/runway/extension/lander/lander.go similarity index 55% rename from runway/extension/merger/merger.go rename to runway/extension/lander/lander.go index 8371e0f74..716fbfb27 100644 --- a/runway/extension/merger/merger.go +++ b/runway/extension/lander/lander.go @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package merger defines the pluggable interface for version-control merge +// Package lander defines the pluggable interface for version-control land // operations that Runway performs on behalf of its callers. Implementations -// resolve change URIs, apply changes to a merge target, and (for a committing -// merge) push the result and finalize the change lifecycle (e.g. close PRs). -package merger +// resolve change URIs, apply changes to a land target, and (for a committing +// land) push the result and finalize the change lifecycle (e.g. close PRs). +package lander -//go:generate mockgen -source=merger.go -destination=mock/merger_mock.go -package=mock +//go:generate mockgen -source=lander.go -destination=mock/lander_mock.go -package=mock import ( "context" @@ -30,43 +30,43 @@ import ( // ErrConflict signals that the ordered steps could not be applied cleanly. // Controllers treat this as an expected outcome (ack + publish a failure // result), not an infrastructure error. -var ErrConflict = errors.New("merge conflict") +var ErrConflict = errors.New("land conflict") // ErrInvalidRequest signals that the request can never be applied as written — -// an unknown merge strategy, a malformed change URI, or an invalid strategy +// an unknown land strategy, a malformed change URI, or an invalid strategy // composition (e.g. PROMOTE mixed with other steps). Like ErrConflict it is a // terminal outcome: retrying never succeeds, so controllers ack and publish a // failure result rather than nacking into an infinite retry / dead-letter. -var ErrInvalidRequest = errors.New("invalid merge request") +var ErrInvalidRequest = errors.New("invalid land request") -// IsTerminal reports whether err is a terminal merge outcome — one the client +// IsTerminal reports whether err is a terminal land outcome — one the client // must be told about via a FAILED result rather than retried. Controllers use // it to decide between publishing a FAILED result (ack) and nacking for retry. func IsTerminal(err error) bool { return errors.Is(err, ErrConflict) || errors.Is(err, ErrInvalidRequest) } -// Merger performs version-control operations against a single merge target. -// Both methods accept the same MergeRequest payload; the behavioral difference +// Lander performs version-control operations against a single land target. +// Both methods accept the same LandRequest payload; the behavioral difference // is whether the result is committed to the remote. -type Merger interface { - // CheckMergeability performs a dry-run merge without committing. The - // returned MergeResult reports per-step mergeability; Outputs are empty. - CheckMergeability(ctx context.Context, req *runwaymq.MergeRequest) (*runwaymq.MergeResult, error) - // Merge applies the ordered steps, commits the result to the remote, and +type Lander interface { + // CheckLandability performs a dry-run land without committing. The + // returned LandResult reports per-step landability; Outputs are empty. + CheckLandability(ctx context.Context, req *runwaymq.LandRequest) (*runwaymq.LandResult, error) + // Land applies the ordered steps, commits the result to the remote, and // reports per-step Outputs (the VCS-neutral revision identifiers produced). - Merge(ctx context.Context, req *runwaymq.MergeRequest) (*runwaymq.MergeResult, error) + Land(ctx context.Context, req *runwaymq.LandRequest) (*runwaymq.LandResult, error) } -// Config identifies the merge target a Merger instance operates on. The factory +// Config identifies the land target a Lander instance operates on. The factory // resolves deployment-specific details (remote URL, credentials) from this. type Config struct { - // QueueName is the caller-provided queue name from the MergeRequest. + // QueueName is the caller-provided queue name from the LandRequest. QueueName string } -// Factory creates Merger instances bound to a merge target. +// Factory creates Lander instances bound to a land target. type Factory interface { - // For returns a Merger instance configured for the given merge target. - For(cfg Config) (Merger, error) + // For returns a Lander instance configured for the given land target. + For(cfg Config) (Lander, error) } diff --git a/runway/extension/merger/mock/BUILD.bazel b/runway/extension/lander/mock/BUILD.bazel similarity index 61% rename from runway/extension/merger/mock/BUILD.bazel rename to runway/extension/lander/mock/BUILD.bazel index 737462e9b..048276703 100644 --- a/runway/extension/merger/mock/BUILD.bazel +++ b/runway/extension/lander/mock/BUILD.bazel @@ -2,12 +2,12 @@ load("@rules_go//go:def.bzl", "go_library") go_library( name = "go_default_library", - srcs = ["merger_mock.go"], - importpath = "github.com/uber/submitqueue/runway/extension/merger/mock", + srcs = ["lander_mock.go"], + importpath = "github.com/uber/submitqueue/runway/extension/lander/mock", visibility = ["//visibility:public"], deps = [ "//api/runway/messagequeue:go_default_library", - "//runway/extension/merger:go_default_library", + "//runway/extension/lander:go_default_library", "@org_uber_go_mock//gomock:go_default_library", ], ) diff --git a/runway/extension/lander/mock/lander_mock.go b/runway/extension/lander/mock/lander_mock.go new file mode 100644 index 000000000..87cdf6537 --- /dev/null +++ b/runway/extension/lander/mock/lander_mock.go @@ -0,0 +1,112 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: lander.go +// +// Generated by this command: +// +// mockgen -source=lander.go -destination=mock/lander_mock.go -package=mock +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + messagequeue "github.com/uber/submitqueue/api/runway/messagequeue" + lander "github.com/uber/submitqueue/runway/extension/lander" + gomock "go.uber.org/mock/gomock" +) + +// MockLander is a mock of Lander interface. +type MockLander struct { + ctrl *gomock.Controller + recorder *MockLanderMockRecorder + isgomock struct{} +} + +// MockLanderMockRecorder is the mock recorder for MockLander. +type MockLanderMockRecorder struct { + mock *MockLander +} + +// NewMockLander creates a new mock instance. +func NewMockLander(ctrl *gomock.Controller) *MockLander { + mock := &MockLander{ctrl: ctrl} + mock.recorder = &MockLanderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLander) EXPECT() *MockLanderMockRecorder { + return m.recorder +} + +// CheckLandability mocks base method. +func (m *MockLander) CheckLandability(ctx context.Context, req *messagequeue.LandRequest) (*messagequeue.LandResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CheckLandability", ctx, req) + ret0, _ := ret[0].(*messagequeue.LandResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CheckLandability indicates an expected call of CheckLandability. +func (mr *MockLanderMockRecorder) CheckLandability(ctx, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckLandability", reflect.TypeOf((*MockLander)(nil).CheckLandability), ctx, req) +} + +// Land mocks base method. +func (m *MockLander) Land(ctx context.Context, req *messagequeue.LandRequest) (*messagequeue.LandResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Land", ctx, req) + ret0, _ := ret[0].(*messagequeue.LandResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Land indicates an expected call of Land. +func (mr *MockLanderMockRecorder) Land(ctx, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Land", reflect.TypeOf((*MockLander)(nil).Land), ctx, req) +} + +// 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 lander.Config) (lander.Lander, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "For", cfg) + ret0, _ := ret[0].(lander.Lander) + 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) +} diff --git a/runway/extension/merger/noop/BUILD.bazel b/runway/extension/lander/noop/BUILD.bazel similarity index 74% rename from runway/extension/merger/noop/BUILD.bazel rename to runway/extension/lander/noop/BUILD.bazel index 09ba35091..1c4b98de7 100644 --- a/runway/extension/merger/noop/BUILD.bazel +++ b/runway/extension/lander/noop/BUILD.bazel @@ -3,12 +3,12 @@ load("@rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", srcs = ["noop.go"], - importpath = "github.com/uber/submitqueue/runway/extension/merger/noop", + importpath = "github.com/uber/submitqueue/runway/extension/lander/noop", visibility = ["//visibility:public"], deps = [ "//api/runway/messagequeue:go_default_library", "//api/runway/messagequeue/protopb:go_default_library", - "//runway/extension/merger:go_default_library", + "//runway/extension/lander:go_default_library", ], ) @@ -18,10 +18,10 @@ go_test( embed = [":go_default_library"], deps = [ "//api/base/change/protopb:go_default_library", - "//api/base/mergestrategy/protopb:go_default_library", + "//api/base/landstrategy/protopb:go_default_library", "//api/runway/messagequeue:go_default_library", "//api/runway/messagequeue/protopb:go_default_library", - "//runway/extension/merger:go_default_library", + "//runway/extension/lander:go_default_library", "@com_github_stretchr_testify//assert:go_default_library", "@com_github_stretchr_testify//require:go_default_library", ], diff --git a/runway/extension/merger/noop/noop.go b/runway/extension/lander/noop/noop.go similarity index 61% rename from runway/extension/merger/noop/noop.go rename to runway/extension/lander/noop/noop.go index 01f4ceadd..001b2cedb 100644 --- a/runway/extension/merger/noop/noop.go +++ b/runway/extension/lander/noop/noop.go @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package noop provides a no-op Merger implementation for local development and -// testing. CheckMergeability always reports success; Merge produces synthetic +// Package noop provides a no-op Lander implementation for local development and +// testing. CheckLandability always reports success; Land produces synthetic // output IDs from an atomic counter. package noop @@ -24,41 +24,41 @@ import ( runwaymq "github.com/uber/submitqueue/api/runway/messagequeue" runwaypb "github.com/uber/submitqueue/api/runway/messagequeue/protopb" - "github.com/uber/submitqueue/runway/extension/merger" + "github.com/uber/submitqueue/runway/extension/lander" ) -var _ merger.Merger = (*Merger)(nil) +var _ lander.Lander = (*Lander)(nil) -// Merger is a no-op implementation that always succeeds. -type Merger struct { - // cfg is the per-queue identity this merger was built for. - cfg merger.Config - // seq mints the synthetic revision ids returned by Merge. It is supplied by - // the caller so a factory that builds one Merger per queue can still hand +// Lander is a no-op implementation that always succeeds. +type Lander struct { + // cfg is the per-queue identity this lander was built for. + cfg lander.Config + // seq mints the synthetic revision ids returned by Land. It is supplied by + // the caller so a factory that builds one Lander per queue can still hand // out ids that are unique across every queue in the process. seq *atomic.Uint64 } -// New returns a no-op Merger bound to the queue named in cfg. seq must be -// non-nil and is shared, not owned: callers minting ids from more than one -// Merger must pass the same counter to each. -func New(cfg merger.Config, seq *atomic.Uint64) *Merger { - return &Merger{cfg: cfg, seq: seq} +// New returns a no-op Lander bound to the queue named in cfg. seq must be +// non-nil and is shared, not owned: callers minting ids from more than +// Lander must pass the same counter to each. +func New(cfg lander.Config, seq *atomic.Uint64) *Lander { + return &Lander{cfg: cfg, seq: seq} } -func (v *Merger) CheckMergeability(_ context.Context, req *runwaymq.MergeRequest) (*runwaymq.MergeResult, error) { +func (v *Lander) CheckLandability(_ context.Context, req *runwaymq.LandRequest) (*runwaymq.LandResult, error) { steps := make([]*runwaymq.StepResult, len(req.GetSteps())) for i, s := range req.GetSteps() { steps[i] = &runwaymq.StepResult{StepId: s.GetStepId()} } - return &runwaymq.MergeResult{ + return &runwaymq.LandResult{ Id: req.GetId(), Outcome: runwaypb.Outcome_SUCCEEDED, Steps: steps, }, nil } -func (v *Merger) Merge(_ context.Context, req *runwaymq.MergeRequest) (*runwaymq.MergeResult, error) { +func (v *Lander) Land(_ context.Context, req *runwaymq.LandRequest) (*runwaymq.LandResult, error) { steps := make([]*runwaymq.StepResult, len(req.GetSteps())) for i, s := range req.GetSteps() { n := v.seq.Add(1) @@ -69,7 +69,7 @@ func (v *Merger) Merge(_ context.Context, req *runwaymq.MergeRequest) (*runwaymq }, } } - return &runwaymq.MergeResult{ + return &runwaymq.LandResult{ Id: req.GetId(), Outcome: runwaypb.Outcome_SUCCEEDED, Steps: steps, diff --git a/runway/extension/merger/noop/noop_test.go b/runway/extension/lander/noop/noop_test.go similarity index 81% rename from runway/extension/merger/noop/noop_test.go rename to runway/extension/lander/noop/noop_test.go index 876a1cc6f..a093321e1 100644 --- a/runway/extension/merger/noop/noop_test.go +++ b/runway/extension/lander/noop/noop_test.go @@ -19,24 +19,24 @@ import ( "sync/atomic" "testing" - "github.com/uber/submitqueue/runway/extension/merger" + "github.com/uber/submitqueue/runway/extension/lander" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" changepb "github.com/uber/submitqueue/api/base/change/protopb" - strategypb "github.com/uber/submitqueue/api/base/mergestrategy/protopb" + strategypb "github.com/uber/submitqueue/api/base/landstrategy/protopb" runwaymq "github.com/uber/submitqueue/api/runway/messagequeue" runwaypb "github.com/uber/submitqueue/api/runway/messagequeue/protopb" ) // testCfg is the per-queue identity used by every case in this file. -var testCfg = merger.Config{QueueName: "test-queue"} +var testCfg = lander.Config{QueueName: "test-queue"} -func testRequest() *runwaymq.MergeRequest { - return &runwaymq.MergeRequest{ +func testRequest() *runwaymq.LandRequest { + return &runwaymq.LandRequest{ Id: "queue-a/42", QueueName: "queue-a", - Steps: []*runwaymq.MergeStep{ + Steps: []*runwaymq.LandStep{ { StepId: "queue-a/1", Change: &changepb.Change{Uris: []string{"github://github.example.com/uber/repo/pull/1/abcdef0123456789abcdef0123456789abcdef01"}}, @@ -51,11 +51,11 @@ func testRequest() *runwaymq.MergeRequest { } } -func TestCheckMergeability(t *testing.T) { +func TestCheckLandability(t *testing.T) { v := New(testCfg, new(atomic.Uint64)) req := testRequest() - res, err := v.CheckMergeability(context.Background(), req) + res, err := v.CheckLandability(context.Background(), req) require.NoError(t, err) assert.Equal(t, req.GetId(), res.GetId()) @@ -67,11 +67,11 @@ func TestCheckMergeability(t *testing.T) { assert.Empty(t, res.GetSteps()[1].GetOutputs()) } -func TestMerge(t *testing.T) { +func TestLand(t *testing.T) { v := New(testCfg, new(atomic.Uint64)) req := testRequest() - res, err := v.Merge(context.Background(), req) + res, err := v.Land(context.Background(), req) require.NoError(t, err) assert.Equal(t, req.GetId(), res.GetId()) @@ -85,13 +85,13 @@ func TestMerge(t *testing.T) { assert.NotEmpty(t, res.GetSteps()[1].GetOutputs()[0].GetId()) } -func TestMerge_UniqueOutputIDs(t *testing.T) { +func TestLand_UniqueOutputIDs(t *testing.T) { v := New(testCfg, new(atomic.Uint64)) req := testRequest() - res1, err := v.Merge(context.Background(), req) + res1, err := v.Land(context.Background(), req) require.NoError(t, err) - res2, err := v.Merge(context.Background(), req) + res2, err := v.Land(context.Background(), req) require.NoError(t, err) assert.NotEqual(t, res1.GetSteps()[0].GetOutputs()[0].GetId(), res2.GetSteps()[0].GetOutputs()[0].GetId()) diff --git a/runway/extension/merger/mock/merger_mock.go b/runway/extension/merger/mock/merger_mock.go deleted file mode 100644 index 0fdc6393e..000000000 --- a/runway/extension/merger/mock/merger_mock.go +++ /dev/null @@ -1,112 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: merger.go -// -// Generated by this command: -// -// mockgen -source=merger.go -destination=mock/merger_mock.go -package=mock -// - -// Package mock is a generated GoMock package. -package mock - -import ( - context "context" - reflect "reflect" - - messagequeue "github.com/uber/submitqueue/api/runway/messagequeue" - merger "github.com/uber/submitqueue/runway/extension/merger" - gomock "go.uber.org/mock/gomock" -) - -// MockMerger is a mock of Merger interface. -type MockMerger struct { - ctrl *gomock.Controller - recorder *MockMergerMockRecorder - isgomock struct{} -} - -// MockMergerMockRecorder is the mock recorder for MockMerger. -type MockMergerMockRecorder struct { - mock *MockMerger -} - -// NewMockMerger creates a new mock instance. -func NewMockMerger(ctrl *gomock.Controller) *MockMerger { - mock := &MockMerger{ctrl: ctrl} - mock.recorder = &MockMergerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockMerger) EXPECT() *MockMergerMockRecorder { - return m.recorder -} - -// CheckMergeability mocks base method. -func (m *MockMerger) CheckMergeability(ctx context.Context, req *messagequeue.MergeRequest) (*messagequeue.MergeResult, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CheckMergeability", ctx, req) - ret0, _ := ret[0].(*messagequeue.MergeResult) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CheckMergeability indicates an expected call of CheckMergeability. -func (mr *MockMergerMockRecorder) CheckMergeability(ctx, req any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckMergeability", reflect.TypeOf((*MockMerger)(nil).CheckMergeability), ctx, req) -} - -// Merge mocks base method. -func (m *MockMerger) Merge(ctx context.Context, req *messagequeue.MergeRequest) (*messagequeue.MergeResult, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Merge", ctx, req) - ret0, _ := ret[0].(*messagequeue.MergeResult) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Merge indicates an expected call of Merge. -func (mr *MockMergerMockRecorder) Merge(ctx, req any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Merge", reflect.TypeOf((*MockMerger)(nil).Merge), ctx, req) -} - -// 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 merger.Config) (merger.Merger, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "For", cfg) - ret0, _ := ret[0].(merger.Merger) - 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) -} diff --git a/service/README.md b/service/README.md index 72c76bb46..75dda099b 100644 --- a/service/README.md +++ b/service/README.md @@ -6,16 +6,16 @@ Each domain has its own subdirectory with a dedicated README: - [`submitqueue/`](submitqueue/README.md) — the multi-service SubmitQueue domain (Gateway + Orchestrator). - [`stovepipe/`](stovepipe/README.md) — the single-service Stovepipe domain (ingest → process → build → buildsignal → record). -- [`runway/`](runway/README.md) — the single-service Runway landing service (consumes the merge queues). +- [`runway/`](runway/README.md) — the single-service Runway landing service (consumes the land queues). ## Services | Service | Port | Domain | RPCs | Backing stores | |---------|------|--------|------|----------------| | **SubmitQueue Gateway** | 8081 | `submitqueue` | `Ping`, `Land`, `Cancel`, `GetRequestSummaryByID`, `GetRequestSummaryByChangeURI`, `List`, `GetRequestHistoryByID`, `GetRequestHistoryByChangeURI` | MySQL app + queue | -| **SubmitQueue Orchestrator** | 8082 | `submitqueue` | `Ping` (+ consumes start, cancel, validate, merge-conflict-check-signal, batch, dependency-analysis, speculate, build, buildsignal, submitqueue-merge, merge-signal, conclude, submitqueue-hook, and paired DLQ topics) | MySQL app + queue | +| **SubmitQueue Orchestrator** | 8082 | `submitqueue` | `Ping` (+ consumes start, cancel, validate, land-conflict-check-signal, batch, dependency-analysis, speculate, build, buildsignal, submitqueue-land, land-signal, conclude, submitqueue-hook, and paired DLQ topics) | MySQL app + queue | | **Stovepipe** | 8083 | `stovepipe` | `Ping`, `Ingest` (+ consumes process, build, buildsignal, record, stovepipe-hook, and paired DLQ topics) | MySQL storage + queue | -| **Runway** | 8086 | `runway` | `Ping` (+ consumes merge-conflict-check & runway-merge topics) | MySQL queue | +| **Runway** | 8086 | `runway` | `Ping` (+ consumes land-conflict-check & runway-land topics) | MySQL queue | Ports above are the `go run` defaults; under Docker Compose each server listens on `:8080` inside its container and is published on a random ephemeral host port (use `make local-*-ps` / `docker port` to discover it). diff --git a/service/runway/README.md b/service/runway/README.md index 99b152aea..22272c124 100644 --- a/service/runway/README.md +++ b/service/runway/README.md @@ -1,34 +1,34 @@ # Runway Service -Runnable wiring for the **Runway** domain — a single-service, consumer-only landing service. Runway has no gateway/orchestrator split: the domain *is* the service. It exposes a thin `Ping` RPC for health checks and does its real work as a queue consumer, draining the merge pipeline queues that the SubmitQueue orchestrator publishes to. +Runnable wiring for the **Runway** domain — a single-service, consumer-only landing service. Runway has no gateway/orchestrator split: the domain *is* the service. It exposes a thin `Ping` RPC for health checks and does its real work as a queue consumer, draining the land pipeline queues that the SubmitQueue orchestrator publishes to. ## What it does Runway registers two consuming subscriptions against the shared MySQL-backed message queue: -- **merge-conflict-check** (`TopicKeyMergeConflictCheck`) — handled by `runway/controller/mergeconflictcheck`. -- **merge** (`TopicKeyMerge`) — handled by `runway/controller/merge`. +- **land-conflict-check** (`TopicKeyLandConflictCheck`) — handled by `runway/controller/landconflictcheck`. +- **land** (`TopicKeyLand`) — handled by `runway/controller/land`. -Each controller applies the request's ordered steps via a `Merger` and publishes a `MergeResult` to the corresponding signal queue (`merge-conflict-check-signal` / `merge-signal`). A second, DLQ consumer drains the inbound topics' dead-letter queues (`merge-conflict-check_dlq`, `runway-merge_dlq`) and republishes a `FAILED` result to the matching signal queue, so a dead-lettered request still resolves the client's correlation id. +Each controller applies the request's ordered steps via a `Lander` and publishes a `LandResult` to the corresponding signal queue (`land-conflict-check-signal` / `land-signal`). A second, DLQ consumer drains the inbound topics' dead-letter queues (`land-conflict-check_dlq`, `runway-land_dlq`) and republishes a `FAILED` result to the matching signal queue, so a dead-lettered request still resolves the client's correlation id. These topic keys and their wire contracts are owned by the queue's producer side and published under `api/runway/messagequeue/` (the external, cross-domain contract). -### Merger backend +### Lander backend -The merge work is done by the [`merger`](../../runway/extension/merger) extension, resolved **per queue** — so one Runway can serve several repositories by giving each queue its own merge target. By default every queue gets the **noop** merger (always succeeds — for local dev and compose). Point `MERGE_CONFIG_PATH` at a merge configuration file to wire real **git** merge targets, or set `MERGE_CHECKOUT_PATH` to configure a single one from the environment (see Configuration). +The land work is done by the [`lander`](../../runway/extension/lander) extension, resolved **per queue** — so one Runway can serve several repositories by giving each queue its own land target. By default every queue gets the **noop** lander (always succeeds — for local dev and compose). Point `LAND_CONFIG_PATH` at a land configuration file to wire real **git** land targets, or set `LAND_CHECKOUT_PATH` to configure a single one from the environment (see Configuration). -Two queues naming the same checkout resolve to the *same* merger instance, which is what serializes them against each other: a git merger locks the working tree it owns, and two instances over one tree would reset it out from under each other mid-merge. Naming one checkout for two *different* targets is rejected at startup. +Two queues naming the same checkout resolve to the *same* lander instance, which is what serializes them against each other: a git lander locks the working tree it owns, and two instances over one tree would reset it out from under each other mid-land. Naming one checkout for two *different* targets is rejected at startup. -### Merge configuration file +### Land configuration file -`MERGE_CONFIG_PATH` names a YAML file with a `defaults` block and per-queue overrides. A queue's `merger` block replaces the default wholesale rather than merging field by field. +`LAND_CONFIG_PATH` names a YAML file with a `defaults` block and per-queue overrides. A queue's `lander` block replaces the default wholesale rather than merging field by field. ```yaml defaults: - merger: {type: noop} # noop | git + lander: {type: noop} # noop | git queues: - name: demo-queue - merger: + lander: type: git remoteUrl: https://github.com/uber/sq-sandbox.git target: main @@ -42,11 +42,11 @@ The file holds **no secret**: `tokenEnv` names the environment variable carrying ### Checkout provisioning -A git merge target's checkout is created at startup if it is not already there: the repository is initialised, the remote configured, the credential written, and the target branch checked out. It is idempotent, so restarting against a persisted volume costs nothing and a rotated token takes effect. +A git land target's checkout is created at startup if it is not already there: the repository is initialised, the remote configured, the credential written, and the target branch checked out. It is idempotent, so restarting against a persisted volume costs nothing and a rotated token takes effect. -The credential never goes into the remote URL. The merger folds git's stderr into the errors it returns, so a URL-embedded token would be reprinted into logs and dead-letter payloads by any failed fetch. Instead it is written as an HTTP `Authorization` header into a `0600` config fragment inside `.git/`, which the repository config includes — keeping it out of the command line too. +The credential never goes into the remote URL. The lander folds git's stderr into the errors it returns, so a URL-embedded token would be reprinted into logs and dead-letter payloads by any failed fetch. Instead it is written as an HTTP `Authorization` header into a `0600` config fragment inside `.git/`, which the repository config includes — keeping it out of the command line too. -For SSH, leave `tokenEnv` unset and use an `ssh://` remote: the merger already passes `SSH_AUTH_SOCK` and `GIT_SSH_COMMAND` through to git, so an agent or a mounted deploy key works with no further configuration. +For SSH, leave `tokenEnv` unset and use an `ssh://` remote: the lander already passes `SSH_AUTH_SOCK` and `GIT_SSH_COMMAND` through to git, so an agent or a mounted deploy key works with no further configuration. Because Runway only consumes queues and serves `Ping`, it needs a **queue** database but no application/storage database. @@ -55,7 +55,7 @@ Because Runway only consumes queues and serves `Ping`, it needs a **queue** data ``` runway/ ├── server/ -│ ├── main.go # gRPC server (Ping) + merge/DLQ consumer wiring +│ ├── main.go # gRPC server (Ping) + land/DLQ consumer wiring │ ├── Dockerfile │ └── docker-compose.yml # Runway service + queue MySQL └── client/ @@ -71,18 +71,18 @@ The Runway controllers themselves live under [`runway/controller/`](../../runway | `QUEUE_MYSQL_DSN` | yes | Queue database DSN | — | | `PORT` | no | gRPC listen address | `:8086` | | `HOSTNAME` | no | Subscriber name for the queue consumer | `runway-` | -| `MERGE_CONFIG_PATH` | no | Path to the per-queue merge configuration file (see above). Takes precedence over the `MERGE_*` variables below, which configure a single target. | — | -| `MERGE_CHECKOUT_PATH` | no | Absolute path to the git checkout the merger owns. When unset (and no config file), the noop merger is used. | — (noop) | -| `MERGE_REMOTE` | no | Git remote to fetch/push | `origin` | -| `MERGE_TARGET` | no | Destination branch on the remote | `main` | -| `MERGE_DEFAULT_STRATEGY` | no | Strategy a `DEFAULT` step resolves to: `REBASE`, `SQUASH_REBASE`, `MERGE`, or `PROMOTE` | `REBASE` | -| `MERGE_COMMITTER_NAME` | no | Committer name for service-created commits | `SubmitQueue Runway` | -| `MERGE_COMMITTER_EMAIL` | no | Committer email for service-created commits | `runway@submitqueue.invalid` | +| `LAND_CONFIG_PATH` | no | Path to the per-queue land configuration file (see above). Takes precedence over the `LAND_*` variables below, which configure a single target. | — | +| `LAND_CHECKOUT_PATH` | no | Absolute path to the git checkout the lander owns. When unset (and no config file), the noop lander is used. | — (noop) | +| `LAND_REMOTE` | no | Git remote to fetch/push | `origin` | +| `LAND_TARGET` | no | Destination branch on the remote | `main` | +| `LAND_DEFAULT_STRATEGY` | no | Strategy a `DEFAULT` step resolves to: `REBASE`, `SQUASH_REBASE`, `MERGE`, or `PROMOTE` | `REBASE` | +| `LAND_COMMITTER_NAME` | no | Committer name for service-created commits | `SubmitQueue Runway` | +| `LAND_COMMITTER_EMAIL` | no | Committer email for service-created commits | `runway@submitqueue.invalid` | | `GIT_EXECUTABLE` / `GIT_EXEC_PATH` / `GIT_TEMPLATE_DIR` | no | Absolute paths pinning the git runtime. Each is derived from the installed git when unset — the executable from `PATH`, the exec path from `git --exec-path`, the templates from the matching install prefix. | derived | -| `MERGE_CHECK_STALENESS` | no | Verify each change's provider ref still points at the commit its URI names before applying | `true` | -| `MERGE_UPDATE_HEAD_BRANCH` | no | Before pushing the target, move each change's head branch to the commit it landed as, so the provider marks the change merged rather than closed. Only affects `REBASE`/`SQUASH_REBASE`. | `false` | -| `MERGE_ALLOW_UNRELATED_HISTORIES` | no | Let a `MERGE` step integrate a change sharing no ancestry with the target (repository imports). Leave off unless the queue exists to perform imports. | `false` | -| `MERGE_FETCH_REFSPECS` | no | Comma-separated extra refspecs fetched each cycle. Only needed for a remote that refuses to serve an unadvertised commit by SHA. | — | +| `LAND_CHECK_STALENESS` | no | Verify each change's provider ref still points at the commit its URI names before applying | `true` | +| `LAND_UPDATE_HEAD_BRANCH` | no | Before pushing the target, move each change's head branch to the commit it landed as, so the provider marks the change merged rather than closed. Only affects `REBASE`/`SQUASH_REBASE`. | `false` | +| `LAND_ALLOW_UNRELATED_HISTORIES` | no | Let a `MERGE` step integrate a change sharing no ancestry with the target (repository imports). Leave off unless the queue exists to perform imports. | `false` | +| `LAND_FETCH_REFSPECS` | no | Comma-separated extra refspecs fetched each cycle. Only needed for a remote that refuses to serve an unadvertised commit by SHA. | — | ## Running diff --git a/service/runway/server/BUILD.bazel b/service/runway/server/BUILD.bazel index 113cd6e23..12e714b36 100644 --- a/service/runway/server/BUILD.bazel +++ b/service/runway/server/BUILD.bazel @@ -16,7 +16,7 @@ go_library( importpath = "github.com/uber/submitqueue/service/runway/server", visibility = ["//visibility:private"], deps = [ - "//api/base/mergestrategy/protopb:go_default_library", + "//api/base/landstrategy/protopb:go_default_library", "//api/runway/messagequeue:go_default_library", "//api/runway/protopb:go_default_library", "//platform/consumer:go_default_library", @@ -31,12 +31,12 @@ go_library( "//platform/git/exec:go_default_library", "//runway/controller:go_default_library", "//runway/controller/dlq:go_default_library", - "//runway/controller/merge:go_default_library", - "//runway/controller/mergeconflictcheck:go_default_library", - "//runway/extension/merger:go_default_library", - "//runway/extension/merger/fake:go_default_library", - "//runway/extension/merger/git:go_default_library", - "//runway/extension/merger/noop:go_default_library", + "//runway/controller/land:go_default_library", + "//runway/controller/landconflictcheck:go_default_library", + "//runway/extension/lander:go_default_library", + "//runway/extension/lander/fake:go_default_library", + "//runway/extension/lander/git:go_default_library", + "//runway/extension/lander/noop:go_default_library", "@com_github_go_sql_driver_mysql//:go_default_library", "@com_github_uber_go_tally//:go_default_library", "@in_gopkg_yaml_v3//:go_default_library", @@ -81,7 +81,7 @@ go_test( "config_test.go", ], # Checkout provisioning runs real git, so the test uses the same pinned - # runtime the merger does rather than whatever git the host happens to have. + # runtime the lander does rather than whatever git the host happens to have. data = [ "@git", "@git//:git_receive_pack", @@ -96,9 +96,9 @@ go_test( "SUBMITQUEUE_TEST_GIT_TEMPLATE_DESCRIPTION": "$(location @git//:templates/description)", }, deps = [ - "//api/base/mergestrategy/protopb:go_default_library", + "//api/base/landstrategy/protopb:go_default_library", "//platform/git/exectest:go_default_library", - "//runway/extension/merger/git:go_default_library", + "//runway/extension/lander/git:go_default_library", "@com_github_stretchr_testify//assert:go_default_library", "@com_github_stretchr_testify//require:go_default_library", "@org_uber_go_zap//zaptest:go_default_library", diff --git a/service/runway/server/Dockerfile b/service/runway/server/Dockerfile index a4dd19fba..c072f1ffc 100644 --- a/service/runway/server/Dockerfile +++ b/service/runway/server/Dockerfile @@ -1,11 +1,11 @@ FROM debian:bookworm-slim -# git is a runtime dependency, not a build one: the git merger shells out to it +# git is a runtime dependency, not a build one: the git lander shells out to it # to fetch, apply, and push. Without it the service still starts, but only the -# noop merger works — a git merge target fails at startup instead. +# noop lander works — a git land target fails at startup instead. RUN apt-get update && apt-get install -y ca-certificates git && rm -rf /var/lib/apt/lists/* \ && mkdir -p /app && chmod 0755 /app \ - # The working trees the git merger owns. Created here so that a named volume + # The working trees the git lander owns. Created here so that a named volume # mounted over it starts with a mode the service can write: Docker seeds an # empty volume from the image, and the container's user is deployment- # configurable (SQ_CONTAINER_USER), so a root-owned directory would leave a diff --git a/service/runway/server/checkout.go b/service/runway/server/checkout.go index 4c12992a5..b444945b5 100644 --- a/service/runway/server/checkout.go +++ b/service/runway/server/checkout.go @@ -27,7 +27,7 @@ import ( "go.uber.org/zap" gitexec "github.com/uber/submitqueue/platform/git/exec" - gitmerger "github.com/uber/submitqueue/runway/extension/merger/git" + gitlander "github.com/uber/submitqueue/runway/extension/lander/git" ) // credentialFile is the name, inside the checkout's .git directory, of the @@ -37,20 +37,20 @@ import ( // repository does not print it back out. const credentialFile = "submitqueue-credentials.config" -// provisionCheckout makes the working tree the git merger requires: a +// provisionCheckout makes the working tree the git lander requires: a // repository at CheckoutPath with the configured remote, its credential in // place, and the target branch checked out. // -// The merger deliberately does none of this — it declares that the checkout +// The lander deliberately does none of this — it declares that the checkout // must already exist, so that it never has to decide whether a surprising // working tree is one it should repair or one it should refuse. Creating it is -// the wiring layer's job, and doing it here keeps the decision out of the merge +// the wiring layer's job, and doing it here keeps the decision out of the land // path. // // It is idempotent: an existing checkout has its remote URL and credential // brought back in line rather than being recreated, so a restart against a // persisted volume costs nothing and a rotated token takes effect. -func provisionCheckout(ctx context.Context, logger *zap.SugaredLogger, runtime gitmerger.GitRuntime, cfg mergerConfig) error { +func provisionCheckout(ctx context.Context, logger *zap.SugaredLogger, runtime gitlander.GitRuntime, cfg landerConfig) error { if err := os.MkdirAll(cfg.CheckoutPath, 0o755); err != nil { return fmt.Errorf("create checkout dir %q: %w", cfg.CheckoutPath, err) } @@ -77,14 +77,14 @@ func provisionCheckout(ctx context.Context, logger *zap.SugaredLogger, runtime g return fmt.Errorf("check out %s: %w", remoteRef, err) } - // The merger points HOME here. Its own `git clean -fdx` may remove it + // The lander points HOME here. Its own `git clean -fdx` may remove it // again, which is harmless — nothing is stored there, and the scrubbed // environment means git reads no configuration from it either way. if err := os.MkdirAll(filepath.Join(cfg.CheckoutPath, ".submitqueue-git-home", "xdg"), 0o700); err != nil { return fmt.Errorf("create git home in %q: %w", cfg.CheckoutPath, err) } - logger.Infow("provisioned merge checkout", + logger.Infow("provisioned land checkout", "checkout", cfg.CheckoutPath, "remote", cfg.Remote, "remote_url", cfg.RemoteURL, @@ -98,7 +98,7 @@ func provisionCheckout(ctx context.Context, logger *zap.SugaredLogger, runtime g // initRepository creates the repository if the path does not already hold one, // reporting whether it had to. An existing repository is left in place: it may // carry fetched objects worth keeping, and re-creating it would discard them. -func initRepository(ctx context.Context, runtime gitmerger.GitRuntime, cfg mergerConfig) (bool, error) { +func initRepository(ctx context.Context, runtime gitlander.GitRuntime, cfg landerConfig) (bool, error) { if info, err := os.Stat(filepath.Join(cfg.CheckoutPath, ".git")); err == nil && info.IsDir() { return false, nil } @@ -110,7 +110,7 @@ func initRepository(ctx context.Context, runtime gitmerger.GitRuntime, cfg merge // configureRemote points the configured remote name at the configured URL, // adding it when absent and correcting it when it has drifted. -func configureRemote(ctx context.Context, runtime gitmerger.GitRuntime, cfg mergerConfig) error { +func configureRemote(ctx context.Context, runtime gitlander.GitRuntime, cfg landerConfig) error { out, err := runGit(ctx, runtime, cfg.CheckoutPath, "remote") if err != nil { return fmt.Errorf("list remotes in %q: %w", cfg.CheckoutPath, err) @@ -133,12 +133,12 @@ func configureRemote(ctx context.Context, runtime gitmerger.GitRuntime, cfg merg // writeCredential stores the remote's token as an HTTP Authorization header in // a config fragment the repository includes. // -// The token deliberately never enters the remote URL. The merger folds git's +// The token deliberately never enters the remote URL. The lander folds git's // stderr into the errors it returns, so a URL-embedded credential would be // reprinted into logs and dead-letter payloads by any failed fetch. It is kept // out of the command line for the same reason — a header written by this // process into a file it owns is visible to neither. -func writeCredential(cfg mergerConfig) error { +func writeCredential(cfg landerConfig) error { path := filepath.Join(cfg.CheckoutPath, ".git", credentialFile) if !cfg.needsHTTPCredential() { @@ -179,7 +179,7 @@ func writeCredential(cfg mergerConfig) error { func setLocalConfig(checkoutPath, key, value string) error { // Uses the ambient git rather than the pinned runtime: this touches only // the repository's own config file, never the remote, and the pinned - // runtime exists to make *merge results* reproducible. + // runtime exists to make *land results* reproducible. cmd := exec.Command("git", "config", "--local", "--replace-all", key, value) cmd.Dir = checkoutPath var stderr bytes.Buffer @@ -192,9 +192,9 @@ func setLocalConfig(checkoutPath, key, value string) error { // runGit invokes the pinned git in dir with an environment scrubbed of ambient // configuration but retaining what is needed to reach a remote. It composes that -// environment through gitexec.Env, the same source the merger uses, so -// provisioning and merging authenticate — and behave — identically. -func runGit(ctx context.Context, runtime gitmerger.GitRuntime, dir string, args ...string) ([]byte, error) { +// environment through gitexec.Env, the same source the lander uses, so +// provisioning and landing authenticate — and behave — identically. +func runGit(ctx context.Context, runtime gitlander.GitRuntime, dir string, args ...string) ([]byte, error) { full := append([]string{ "--exec-path=" + runtime.ExecPath, "-c", "init.templateDir=" + runtime.TemplateDir, diff --git a/service/runway/server/checkout_test.go b/service/runway/server/checkout_test.go index d85de4536..b2e5d8e40 100644 --- a/service/runway/server/checkout_test.go +++ b/service/runway/server/checkout_test.go @@ -27,16 +27,16 @@ import ( "go.uber.org/zap/zaptest" gitexectest "github.com/uber/submitqueue/platform/git/exectest" - gitmerger "github.com/uber/submitqueue/runway/extension/merger/git" + gitlander "github.com/uber/submitqueue/runway/extension/lander/git" ) // testRuntime resolves the pinned git the Bazel target supplies. Provisioning -// runs real git, so these tests use the same runtime the merger will. -func testRuntime(t *testing.T) gitmerger.GitRuntime { +// runs real git, so these tests use the same runtime the lander will. +func testRuntime(t *testing.T) gitlander.GitRuntime { t.Helper() executable := gitexectest.Git(t) templateDescription := gitexectest.Runfile(t, "SUBMITQUEUE_TEST_GIT_TEMPLATE_DESCRIPTION") - return gitmerger.GitRuntime{ + return gitlander.GitRuntime{ Executable: executable, ExecPath: filepath.Dir(executable), TemplateDir: filepath.Dir(templateDescription), @@ -44,7 +44,7 @@ func testRuntime(t *testing.T) gitmerger.GitRuntime { } // seedBareRepo creates a bare repository holding one commit on branch and -// returns its path — the shape of the remote a merge target points at. +// returns its path — the shape of the remote a land target points at. func seedBareRepo(t *testing.T, branch string) string { t.Helper() runtime := testRuntime(t) @@ -68,7 +68,7 @@ func seedBareRepo(t *testing.T, branch string) string { return bare } -func mustRunGit(t *testing.T, ctx context.Context, runtime gitmerger.GitRuntime, dir string, args ...string) string { +func mustRunGit(t *testing.T, ctx context.Context, runtime gitlander.GitRuntime, dir string, args ...string) string { t.Helper() out, err := runGit(ctx, runtime, dir, args...) require.NoError(t, err, "git %s", strings.Join(args, " ")) @@ -76,10 +76,10 @@ func mustRunGit(t *testing.T, ctx context.Context, runtime gitmerger.GitRuntime, } // localCheckout builds a config pointing at a freshly seeded bare repo. -func localCheckout(t *testing.T, bare string) mergerConfig { +func localCheckout(t *testing.T, bare string) landerConfig { t.Helper() - cfg := mergerConfig{ - Type: mergerTypeGit, + cfg := landerConfig{ + Type: landerTypeGit, RemoteURL: "file://" + bare, CheckoutPath: filepath.Join(t.TempDir(), "checkout"), } @@ -94,7 +94,7 @@ func TestProvisionCheckout_CreatesUsableWorkingTree(t *testing.T) { require.NoError(t, provisionCheckout(ctx, zaptest.NewLogger(t).Sugar(), runtime, cfg)) - // The merger resets to refs/remotes// on every cycle, so + // The lander resets to refs/remotes// on every cycle, so // that ref existing is what makes the checkout usable at all. assert.NotEmpty(t, mustRunGit(t, ctx, runtime, cfg.CheckoutPath, "rev-parse", "origin/main")) assert.Equal(t, "main", mustRunGit(t, ctx, runtime, cfg.CheckoutPath, "rev-parse", "--abbrev-ref", "HEAD")) @@ -157,7 +157,7 @@ func TestProvisionCheckout_LocalRemoteCarriesNoCredential(t *testing.T) { } func TestWriteCredential_KeepsTokenOutOfTheRemoteURL(t *testing.T) { - // The merger folds git's stderr into the errors it returns, so a token in + // The lander folds git's stderr into the errors it returns, so a token in // the remote URL would be reprinted by any failed fetch. This is the // property that keeps it out of logs and dead-letter payloads. const token = "s3cret-token-value" @@ -219,8 +219,8 @@ func TestWriteCredential_RemovesFragmentWhenNoLongerNeeded(t *testing.T) { func TestWriteCredential_RejectsUnsetToken(t *testing.T) { // Failing here beats cloning anonymously and reporting a confusing 404 on a // private repository later. - cfg := mergerConfig{ - Type: mergerTypeGit, + cfg := landerConfig{ + Type: landerTypeGit, RemoteURL: "https://example.com/o/r.git", CheckoutPath: t.TempDir(), TokenEnv: "TEST_TOKEN_DEFINITELY_NOT_SET", @@ -244,8 +244,8 @@ func TestResolveGitRuntime_HonorsEnvironmentOverrides(t *testing.T) { } func TestResolveGitRuntime_DerivesExecPathFromTheBinary(t *testing.T) { - // The trap this removes: the merger requires all three paths, so a - // deployment that sets only MERGE_CHECKOUT_PATH used to fail at startup. + // The trap this removes: the lander requires all three paths, so a + // deployment that sets only LAND_CHECKOUT_PATH used to fail at startup. pinned := testRuntime(t) t.Setenv("GIT_EXECUTABLE", pinned.Executable) t.Setenv("GIT_EXEC_PATH", "") diff --git a/service/runway/server/config.go b/service/runway/server/config.go index 94b64a09e..162e0ec97 100644 --- a/service/runway/server/config.go +++ b/service/runway/server/config.go @@ -22,60 +22,60 @@ import ( yamlv3 "gopkg.in/yaml.v3" - mergestrategypb "github.com/uber/submitqueue/api/base/mergestrategy/protopb" + landstrategypb "github.com/uber/submitqueue/api/base/landstrategy/protopb" ) -// Merger types selectable from configuration. +// Lander types selectable from configuration. const ( - mergerTypeNoop = "noop" - mergerTypeGit = "git" + landerTypeNoop = "noop" + landerTypeGit = "git" ) -// mergeConfig is the runway merge configuration file: which merger each queue -// gets, and how its merge target is reached. +// landConfig is the runway land configuration file: which lander each queue +// gets, and how its land target is reached. // // It carries no secret. A git target names the *environment variable* holding // its credential (tokenEnv), so the file stays committable and a deployment // swaps the credential without editing it. -type mergeConfig struct { +type landConfig struct { // Defaults applies to any queue without its own entry. - Defaults queueMergeConfig `yaml:"defaults"` + Defaults queueLandConfig `yaml:"defaults"` // Queues holds per-queue overrides, keyed by queue name. - Queues []namedQueueMergeConfig `yaml:"queues"` + Queues []namedQueueLandConfig `yaml:"queues"` } -// namedQueueMergeConfig is one queue's entry. -type namedQueueMergeConfig struct { +// namedQueueLandConfig is one queue's entry. +type namedQueueLandConfig struct { // Name is the queue this entry configures. Name string `yaml:"name"` - // Merger replaces the default merger wholesale when present. Overriding is + // Lander replaces the default lander wholesale when present. Overriding is // per-block rather than per-field: a half-inherited git target, where the // remote comes from one place and the branch from another, is harder to // read than one stated outright. - Merger *mergerConfig `yaml:"merger"` + Lander *landerConfig `yaml:"lander"` } -// queueMergeConfig is the set of extensions a queue resolves to. -type queueMergeConfig struct { - // Merger performs the mergeability check and the committing merge. - Merger mergerConfig `yaml:"merger"` +// queueLandConfig is the set of extensions a queue resolves to. +type queueLandConfig struct { + // Lander performs the landability check and the committing land. + Lander landerConfig `yaml:"lander"` } -// mergerConfig selects a merger implementation and configures it. Fields other -// than Type apply only to the git merger. -type mergerConfig struct { +// landerConfig selects a lander implementation and configures it. Fields other +// than Type apply only to the git lander. +type landerConfig struct { // Type selects the implementation: "noop" or "git". Type string `yaml:"type"` - // RemoteURL is the repository the merger clones and pushes to. When empty + // RemoteURL is the repository the lander clones and pushes to. When empty // the checkout is taken as provisioned by something else and used as it // stands — which is how an externally-managed working tree, or one mounted // into the container ready to go, is described. RemoteURL string `yaml:"remoteUrl"` // Remote is the git remote name for that URL. Defaults to "origin". Remote string `yaml:"remote"` - // Target is the branch merges land on. Defaults to "main". + // Target is the branch land operations apply to. Defaults to "main". Target string `yaml:"target"` - // CheckoutPath is the working tree the merger owns. It is provisioned at + // CheckoutPath is the working tree the lander owns. It is provisioned at // startup if absent, and must not be shared by two different targets. CheckoutPath string `yaml:"checkoutPath"` // DefaultStrategy resolves a step whose strategy is DEFAULT. Defaults to @@ -94,47 +94,47 @@ type mergerConfig struct { // that will not serve an unadvertised object by SHA. FetchRefspecs []string `yaml:"fetchRefspecs"` // MaxPushAttempts caps the reset/apply/push retry loop under contention. - // Zero uses the merger's own default. + // Zero uses the lander's own default. MaxPushAttempts int `yaml:"maxPushAttempts"` - // CommitterName and CommitterEmail identify commits the merger creates. + // CommitterName and CommitterEmail identify commits the lander creates. CommitterName string `yaml:"committerName"` CommitterEmail string `yaml:"committerEmail"` // TokenEnv names the environment variable holding the credential for an // HTTPS remote — never the credential itself. Empty means the remote needs // no token, which is the case for a local path and for SSH (where the - // merger passes the agent socket through instead). + // lander passes the agent socket through instead). TokenEnv string `yaml:"tokenEnv"` // TokenUser is the username paired with the token in basic auth. Providers // each have their own convention; defaults to "x-access-token". TokenUser string `yaml:"tokenUser"` } -// loadMergeConfig reads and validates the merge configuration at path. -func loadMergeConfig(path string) (mergeConfig, error) { +// loadLandConfig reads and validates the land configuration at path. +func loadLandConfig(path string) (landConfig, error) { data, err := os.ReadFile(path) if err != nil { - return mergeConfig{}, fmt.Errorf("failed to read merge config %q: %w", path, err) + return landConfig{}, fmt.Errorf("failed to read land config %q: %w", path, err) } - var cfg mergeConfig + var cfg landConfig if err := yamlv3.Unmarshal(data, &cfg); err != nil { - return mergeConfig{}, fmt.Errorf("failed to parse merge config %q: %w", path, err) + return landConfig{}, fmt.Errorf("failed to parse land config %q: %w", path, err) } if err := cfg.normalizeAndValidate(); err != nil { - return mergeConfig{}, fmt.Errorf("invalid merge config %q: %w", path, err) + return landConfig{}, fmt.Errorf("invalid land config %q: %w", path, err) } return cfg, nil } -// usesGit reports whether any configured queue resolves to the git merger, and +// usesGit reports whether any configured queue resolves to the git lander, and // so whether the process needs a git runtime at all. -func (c mergeConfig) usesGit() bool { - if c.Defaults.Merger.Type == mergerTypeGit { +func (c landConfig) usesGit() bool { + if c.Defaults.Lander.Type == landerTypeGit { return true } for _, q := range c.Queues { - if q.Merger != nil && q.Merger.Type == mergerTypeGit { + if q.Lander != nil && q.Lander.Type == landerTypeGit { return true } } @@ -142,20 +142,20 @@ func (c mergeConfig) usesGit() bool { } // normalizeAndValidate applies defaults and rejects a configuration that could -// not produce working mergers. -func (c *mergeConfig) normalizeAndValidate() error { - if err := c.Defaults.Merger.normalizeAndValidate("defaults"); err != nil { +// not produce working landers. +func (c *landConfig) normalizeAndValidate() error { + if err := c.Defaults.Lander.normalizeAndValidate("defaults"); err != nil { return err } seen := make(map[string]bool, len(c.Queues)) - // Two queues may legitimately share one merge target, but two *different* - // targets sharing a working tree would have each merger reset and push the + // Two queues may legitimately share one land target, but two *different* + // targets sharing a working tree would have each lander reset and push the // other's work mid-flight. Only the second is an error, so the check is on // conflicting settings rather than on reuse. - byCheckout := make(map[string]mergerConfig) - if c.Defaults.Merger.Type == mergerTypeGit { - byCheckout[c.Defaults.Merger.CheckoutPath] = c.Defaults.Merger + byCheckout := make(map[string]landerConfig) + if c.Defaults.Lander.Type == landerTypeGit { + byCheckout[c.Defaults.Lander.CheckoutPath] = c.Defaults.Lander } for i := range c.Queues { @@ -168,45 +168,45 @@ func (c *mergeConfig) normalizeAndValidate() error { } seen[q.Name] = true - if q.Merger == nil { + if q.Lander == nil { continue } where := fmt.Sprintf("queue %q", q.Name) - if err := q.Merger.normalizeAndValidate(where); err != nil { + if err := q.Lander.normalizeAndValidate(where); err != nil { return err } - if q.Merger.Type != mergerTypeGit { + if q.Lander.Type != landerTypeGit { continue } - if prior, ok := byCheckout[q.Merger.CheckoutPath]; ok { - if diff, differs := prior.firstDifference(*q.Merger); differs { + if prior, ok := byCheckout[q.Lander.CheckoutPath]; ok { + if diff, differs := prior.firstDifference(*q.Lander); differs { return fmt.Errorf( - "%s reuses checkout %q but differs from an earlier queue in %s; queues sharing a checkout share one merger instance, so give each configuration its own checkout", - where, q.Merger.CheckoutPath, diff, + "%s reuses checkout %q but differs from an earlier queue in %s; queues sharing a checkout share one lander instance, so give each configuration its own checkout", + where, q.Lander.CheckoutPath, diff, ) } } - byCheckout[q.Merger.CheckoutPath] = *q.Merger + byCheckout[q.Lander.CheckoutPath] = *q.Lander } return nil } // normalizeAndValidate fills in defaults and checks the fields the selected // type requires. where names the config location for error messages. -func (m *mergerConfig) normalizeAndValidate(where string) error { +func (m *landerConfig) normalizeAndValidate(where string) error { if m.Type == "" { - m.Type = mergerTypeNoop + m.Type = landerTypeNoop } switch m.Type { - case mergerTypeNoop: + case landerTypeNoop: return nil - case mergerTypeGit: + case landerTypeGit: default: - return fmt.Errorf("%s: unknown merger type %q (want %q or %q)", where, m.Type, mergerTypeNoop, mergerTypeGit) + return fmt.Errorf("%s: unknown lander type %q (want %q or %q)", where, m.Type, landerTypeNoop, landerTypeGit) } if m.CheckoutPath == "" { - return fmt.Errorf("%s: git merger requires checkoutPath", where) + return fmt.Errorf("%s: git lander requires checkoutPath", where) } if m.TokenEnv != "" && m.RemoteURL == "" { return fmt.Errorf("%s: tokenEnv is set but remoteUrl is not, so there is no remote to authenticate to", where) @@ -236,21 +236,21 @@ func (m *mergerConfig) normalizeAndValidate(where string) error { return nil } -// firstDifference reports the first field in which two git mergers disagree, +// firstDifference reports the first field in which two git landers disagree, // rendered for an error message, and whether there was one. // // Every field is compared, not only the ones naming the target. Queues sharing -// a checkout share a single merger instance, built from whichever queue reached +// a checkout share a single lander instance, built from whichever queue reached // the builder first, so any field that instance is constructed from has to -// agree — a queue asking for SQUASH_REBASE and silently merging with REBASE +// agree — a queue asking for SQUASH_REBASE and silently landing with REBASE // writes the wrong history and reports nothing. // // The comparison is reflective rather than field by field so that a field added -// to mergerConfig later is covered without anyone remembering to extend this. +// to landerConfig later is covered without anyone remembering to extend this. // That is the failure mode worth designing against: the old three-field -// comparison was correct when the merger consumed three fields, and quietly +// comparison was correct when the lander consumed three fields, and quietly // stopped being correct as it grew to consume eleven. -func (m mergerConfig) firstDifference(other mergerConfig) (string, bool) { +func (m landerConfig) firstDifference(other landerConfig) (string, bool) { mine, theirs := reflect.ValueOf(m), reflect.ValueOf(other) t := mine.Type() for i := 0; i < t.NumField(); i++ { @@ -262,14 +262,14 @@ func (m mergerConfig) firstDifference(other mergerConfig) (string, bool) { if name == "" { name = t.Field(i).Name } - return fmt.Sprintf("%s (%s vs %s)", name, renderMergerField(a), renderMergerField(b)), true + return fmt.Sprintf("%s (%s vs %s)", name, renderLanderField(a), renderLanderField(b)), true } return "", false } -// renderMergerField formats one field for an error message, following a pointer +// renderLanderField formats one field for an error message, following a pointer // so an optional bool reads as its value rather than as an address. -func renderMergerField(v reflect.Value) string { +func renderLanderField(v reflect.Value) string { if v.Kind() == reflect.Ptr { if v.IsNil() { return "unset" @@ -281,12 +281,12 @@ func renderMergerField(v reflect.Value) string { // provisions reports whether this target names a remote to build its checkout // from, as opposed to relying on one that already exists. -func (m mergerConfig) provisions() bool { - return m.Type == mergerTypeGit && m.RemoteURL != "" +func (m landerConfig) provisions() bool { + return m.Type == landerTypeGit && m.RemoteURL != "" } // strategy returns the configured default strategy, already validated. -func (m mergerConfig) strategy() mergestrategypb.Strategy { +func (m landerConfig) strategy() landstrategypb.Strategy { s, _ := parseStrategy(m.DefaultStrategy) return s } @@ -295,7 +295,7 @@ func (m mergerConfig) strategy() mergestrategypb.Strategy { // the variable was set (rather than just returning "") keeps "no credential // configured" distinct from "configured but empty", which is almost always a // deployment mistake worth failing on. -func (m mergerConfig) token() (string, bool) { +func (m landerConfig) token() (string, bool) { if m.TokenEnv == "" { return "", false } @@ -305,7 +305,7 @@ func (m mergerConfig) token() (string, bool) { // needsHTTPCredential reports whether this target is reached over HTTP(S) and // so can carry a token. A local path or an SSH remote authenticates by other // means, and writing an Authorization header for one would be inert at best. -func (m mergerConfig) needsHTTPCredential() bool { +func (m landerConfig) needsHTTPCredential() bool { if m.TokenEnv == "" { return false } diff --git a/service/runway/server/config_test.go b/service/runway/server/config_test.go index 63e3fb186..688b50ff4 100644 --- a/service/runway/server/config_test.go +++ b/service/runway/server/config_test.go @@ -23,51 +23,51 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - mergestrategypb "github.com/uber/submitqueue/api/base/mergestrategy/protopb" + landstrategypb "github.com/uber/submitqueue/api/base/landstrategy/protopb" ) -// writeConfig writes a merge config file and returns its path. +// writeConfig writes a land config file and returns its path. func writeConfig(t *testing.T, contents string) string { t.Helper() - path := filepath.Join(t.TempDir(), "merge.yaml") + path := filepath.Join(t.TempDir(), "land.yaml") require.NoError(t, os.WriteFile(path, []byte(contents), 0o600)) return path } -func TestLoadMergeConfig_Defaults(t *testing.T) { +func TestLoadLandConfig_Defaults(t *testing.T) { path := writeConfig(t, ` defaults: - merger: {type: noop} + lander: {type: noop} queues: - name: demo - merger: + lander: type: git remoteUrl: https://example.com/o/r.git checkoutPath: /var/checkouts/r `) - cfg, err := loadMergeConfig(path) + cfg, err := loadLandConfig(path) require.NoError(t, err) - demo := cfg.Queues[0].Merger + demo := cfg.Queues[0].Lander require.NotNil(t, demo) assert.Equal(t, "origin", demo.Remote) assert.Equal(t, "main", demo.Target) assert.Equal(t, "x-access-token", demo.TokenUser) - assert.Equal(t, mergestrategypb.Strategy_REBASE, demo.strategy()) + assert.Equal(t, landstrategypb.Strategy_REBASE, demo.strategy()) require.NotNil(t, demo.CheckStaleness) assert.True(t, *demo.CheckStaleness, "staleness checking is on unless turned off") assert.False(t, demo.UpdateHeadBranch) assert.True(t, cfg.usesGit()) } -func TestLoadMergeConfig_ExplicitValuesWin(t *testing.T) { +func TestLoadLandConfig_ExplicitValuesWin(t *testing.T) { path := writeConfig(t, ` defaults: - merger: {type: noop} + lander: {type: noop} queues: - name: demo - merger: + lander: type: git remoteUrl: https://example.com/o/r.git remote: upstream @@ -80,68 +80,68 @@ queues: tokenUser: oauth2 `) - cfg, err := loadMergeConfig(path) + cfg, err := loadLandConfig(path) require.NoError(t, err) - demo := cfg.Queues[0].Merger + demo := cfg.Queues[0].Lander assert.Equal(t, "upstream", demo.Remote) assert.Equal(t, "trunk", demo.Target) assert.Equal(t, "oauth2", demo.TokenUser) - assert.Equal(t, mergestrategypb.Strategy_SQUASH_REBASE, demo.strategy()) + assert.Equal(t, landstrategypb.Strategy_SQUASH_REBASE, demo.strategy()) require.NotNil(t, demo.CheckStaleness) assert.False(t, *demo.CheckStaleness) assert.True(t, demo.UpdateHeadBranch) } -func TestLoadMergeConfig_NoopOnlyNeedsNoGit(t *testing.T) { +func TestLoadLandConfig_NoopOnlyNeedsNoGit(t *testing.T) { path := writeConfig(t, ` defaults: - merger: {type: noop} + lander: {type: noop} queues: - name: demo `) - cfg, err := loadMergeConfig(path) + cfg, err := loadLandConfig(path) require.NoError(t, err) assert.False(t, cfg.usesGit(), "a noop-only deployment must not require a git runtime") } -func TestLoadMergeConfig_EmptyFileIsNoop(t *testing.T) { - cfg, err := loadMergeConfig(writeConfig(t, "")) +func TestLoadLandConfig_EmptyFileIsNoop(t *testing.T) { + cfg, err := loadLandConfig(writeConfig(t, "")) require.NoError(t, err) - assert.Equal(t, mergerTypeNoop, cfg.Defaults.Merger.Type) + assert.Equal(t, landerTypeNoop, cfg.Defaults.Lander.Type) assert.False(t, cfg.usesGit()) } -func TestLoadMergeConfig_SharedCheckoutForSameTargetIsAllowed(t *testing.T) { - // Two queues landing on one target must resolve to one merger instance, so +func TestLoadLandConfig_SharedCheckoutForSameTargetIsAllowed(t *testing.T) { + // Two queues landing on one target must resolve to one lander instance, so // sharing a checkout is the correct configuration rather than a mistake. path := writeConfig(t, ` defaults: - merger: {type: noop} + lander: {type: noop} queues: - name: a - merger: {type: git, remoteUrl: https://example.com/o/r.git, checkoutPath: /var/checkouts/r} + lander: {type: git, remoteUrl: https://example.com/o/r.git, checkoutPath: /var/checkouts/r} - name: b - merger: {type: git, remoteUrl: https://example.com/o/r.git, checkoutPath: /var/checkouts/r} + lander: {type: git, remoteUrl: https://example.com/o/r.git, checkoutPath: /var/checkouts/r} `) - _, err := loadMergeConfig(path) + _, err := loadLandConfig(path) require.NoError(t, err) } -func TestLoadMergeConfig_SharedCheckoutToleratesEquivalentSpellings(t *testing.T) { +func TestLoadLandConfig_SharedCheckoutToleratesEquivalentSpellings(t *testing.T) { // Sharing is judged on the normalized configuration, so a queue that omits // a defaulted field and one that states the default explicitly agree. An // empty refspec list and an omitted one likewise mean the same thing. path := writeConfig(t, ` defaults: - merger: {type: noop} + lander: {type: noop} queues: - name: a - merger: {type: git, remoteUrl: https://example.com/o/r.git, checkoutPath: /var/checkouts/r} + lander: {type: git, remoteUrl: https://example.com/o/r.git, checkoutPath: /var/checkouts/r} - name: b - merger: + lander: type: git remoteUrl: https://example.com/o/r.git checkoutPath: /var/checkouts/r @@ -151,12 +151,12 @@ queues: fetchRefspecs: [] `) - _, err := loadMergeConfig(path) + _, err := loadLandConfig(path) require.NoError(t, err) } -func TestLoadMergeConfig_RejectsSharedCheckoutWithDivergentMergerFields(t *testing.T) { - // A shared checkout means a shared merger instance, built from whichever +func TestLoadLandConfig_RejectsSharedCheckoutWithDivergentLanderFields(t *testing.T) { + // A shared checkout means a shared lander instance, built from whichever // queue reached the builder first. Any field that instance is constructed // from must agree, or the second queue silently runs with the first's // value — merging with a strategy it never asked for, and saying nothing. @@ -194,52 +194,52 @@ func TestLoadMergeConfig_RejectsSharedCheckoutWithDivergentMergerFields(t *testi t.Run(tt.name, func(t *testing.T) { path := writeConfig(t, ` defaults: - merger: {type: noop} + lander: {type: noop} queues: - name: a - merger: {type: git, remoteUrl: https://example.com/o/r.git, checkoutPath: /var/checkouts/r, defaultStrategy: SQUASH_REBASE, committerEmail: a@example.com} + lander: {type: git, remoteUrl: https://example.com/o/r.git, checkoutPath: /var/checkouts/r, defaultStrategy: SQUASH_REBASE, committerEmail: a@example.com} - name: b - merger: `+tt.b+` + lander: `+tt.b+` `) - _, err := loadMergeConfig(path) + _, err := loadLandConfig(path) require.Error(t, err) }) } } -func TestMergerConfig_FirstDifference(t *testing.T) { +func TestLanderConfig_FirstDifference(t *testing.T) { // The differing field is named rather than left to the reader, because // "these two queues disagree somewhere" is not enough to act on when a - // merger has fifteen fields. This is asserted on the returned value rather + // lander has fifteen fields. This is asserted on the returned value rather // than on an error string so it stays a test of behavior. - base := mergerConfig{Type: mergerTypeGit, RemoteURL: "https://example.com/o/r.git", Remote: "origin", Target: "main"} + base := landerConfig{Type: landerTypeGit, RemoteURL: "https://example.com/o/r.git", Remote: "origin", Target: "main"} enabled, disabled := true, false tests := []struct { name string - other mergerConfig + other landerConfig wantDiffers bool wantField string }{ {name: "identical", other: base}, { name: "target", - other: func() mergerConfig { c := base; c.Target = "release"; return c }(), + other: func() landerConfig { c := base; c.Target = "release"; return c }(), wantDiffers: true, wantField: "target", }, { name: "default strategy", - other: func() mergerConfig { c := base; c.DefaultStrategy = "REBASE"; return c }(), + other: func() landerConfig { c := base; c.DefaultStrategy = "REBASE"; return c }(), wantDiffers: true, wantField: "defaultStrategy", }, { name: "optional bool set on one side only", - other: func() mergerConfig { c := base; c.CheckStaleness = &enabled; return c }(), + other: func() landerConfig { c := base; c.CheckStaleness = &enabled; return c }(), wantDiffers: true, wantField: "checkStaleness", }, { name: "refspecs", - other: func() mergerConfig { c := base; c.FetchRefspecs = []string{"+refs/pull/*:refs/pull/*"}; return c }(), + other: func() landerConfig { c := base; c.FetchRefspecs = []string{"+refs/pull/*:refs/pull/*"}; return c }(), wantDiffers: true, wantField: "fetchRefspecs", }, } @@ -271,37 +271,37 @@ func TestMergerConfig_FirstDifference(t *testing.T) { "an optional bool reads as its value, not as a pointer address") } -func TestLoadMergeConfig_Rejects(t *testing.T) { +func TestLoadLandConfig_Rejects(t *testing.T) { tests := []struct { name string contents string }{ { - name: "unknown merger type", + name: "unknown lander type", contents: ` defaults: - merger: {type: magic} + lander: {type: magic} `, }, { name: "git without checkout path", contents: ` defaults: - merger: {type: git, remoteUrl: https://example.com/o/r.git} + lander: {type: git, remoteUrl: https://example.com/o/r.git} `, }, { name: "token without a remote to authenticate to", contents: ` defaults: - merger: {type: git, checkoutPath: /var/checkouts/r, tokenEnv: SOME_TOKEN} + lander: {type: git, checkoutPath: /var/checkouts/r, tokenEnv: SOME_TOKEN} `, }, { name: "unknown default strategy", contents: ` defaults: - merger: + lander: type: git remoteUrl: https://example.com/o/r.git checkoutPath: /var/checkouts/r @@ -312,73 +312,73 @@ defaults: name: "queue without a name", contents: ` defaults: - merger: {type: noop} + lander: {type: noop} queues: - - merger: {type: noop} + - lander: {type: noop} `, }, { name: "duplicate queue", contents: ` defaults: - merger: {type: noop} + lander: {type: noop} queues: - name: a - merger: {type: noop} + lander: {type: noop} - name: a - merger: {type: noop} + lander: {type: noop} `, }, { - // Two mergers over one working tree would reset and push it out - // from under each other mid-merge. + // Two landers over one working tree would reset and push it out + // from under each other mid-land. name: "same checkout for different targets", contents: ` defaults: - merger: {type: noop} + lander: {type: noop} queues: - name: a - merger: {type: git, remoteUrl: https://example.com/o/r.git, target: main, checkoutPath: /var/checkouts/r} + lander: {type: git, remoteUrl: https://example.com/o/r.git, target: main, checkoutPath: /var/checkouts/r} - name: b - merger: {type: git, remoteUrl: https://example.com/o/r.git, target: release, checkoutPath: /var/checkouts/r} + lander: {type: git, remoteUrl: https://example.com/o/r.git, target: release, checkoutPath: /var/checkouts/r} `, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, err := loadMergeConfig(writeConfig(t, tt.contents)) + _, err := loadLandConfig(writeConfig(t, tt.contents)) require.Error(t, err) }) } } -func TestMergerConfig_NeedsHTTPCredential(t *testing.T) { +func TestLanderConfig_NeedsHTTPCredential(t *testing.T) { tests := []struct { name string - cfg mergerConfig + cfg landerConfig want bool }{ { name: "https remote with token", - cfg: mergerConfig{RemoteURL: "https://example.com/o/r.git", TokenEnv: "T"}, + cfg: landerConfig{RemoteURL: "https://example.com/o/r.git", TokenEnv: "T"}, want: true, }, { name: "https remote without token", - cfg: mergerConfig{RemoteURL: "https://example.com/o/r.git"}, + cfg: landerConfig{RemoteURL: "https://example.com/o/r.git"}, want: false, }, { // The credential is an HTTP header; over SSH there is nothing to - // attach it to, and the merger passes the agent socket instead. + // attach it to, and the lander passes the agent socket instead. name: "ssh remote", - cfg: mergerConfig{RemoteURL: "ssh://git@example.com/o/r.git", TokenEnv: "T"}, + cfg: landerConfig{RemoteURL: "ssh://git@example.com/o/r.git", TokenEnv: "T"}, want: false, }, { name: "local path used by the hermetic e2e", - cfg: mergerConfig{RemoteURL: "file:///srv/git/sandbox.git", TokenEnv: "T"}, + cfg: landerConfig{RemoteURL: "file:///srv/git/sandbox.git", TokenEnv: "T"}, want: false, }, } @@ -390,12 +390,12 @@ func TestMergerConfig_NeedsHTTPCredential(t *testing.T) { } } -func TestMergerConfig_Provisions(t *testing.T) { +func TestLanderConfig_Provisions(t *testing.T) { // A checkout with no remote named is one something else built; the wiring // must use it as it stands rather than trying to create it. - external := mergerConfig{Type: mergerTypeGit, CheckoutPath: "/var/checkouts/r"} + external := landerConfig{Type: landerTypeGit, CheckoutPath: "/var/checkouts/r"} assert.False(t, external.provisions()) - owned := mergerConfig{Type: mergerTypeGit, CheckoutPath: "/var/checkouts/r", RemoteURL: "https://example.com/o/r.git"} + owned := landerConfig{Type: landerTypeGit, CheckoutPath: "/var/checkouts/r", RemoteURL: "https://example.com/o/r.git"} assert.True(t, owned.provisions()) } diff --git a/service/runway/server/docker-compose.yml b/service/runway/server/docker-compose.yml index 1706886dc..73fa09892 100644 --- a/service/runway/server/docker-compose.yml +++ b/service/runway/server/docker-compose.yml @@ -13,7 +13,7 @@ services: # Queue Database - Messaging infrastructure (messages, offsets, partition leases). - # Runway consumes the merge-conflict-check and merge queues from here. + # Runway consumes the land-conflict-check and land queues from here. mysql-queue: image: mysql:8.0 environment: @@ -43,10 +43,10 @@ services: - "8080" # Random ephemeral port to avoid conflicts environment: - PORT=:8080 - # Merger implementation. Empty (the default) resolves from the merge - # environment: git when MERGE_CHECKOUT_PATH is set, noop otherwise. The - # e2e suite sets SQ_RUNWAY_MERGER=fake to drive outcomes from the payload. - - MERGER=${SQ_RUNWAY_MERGER:-} + # Lander implementation. Empty (the default) resolves from the land + # environment: git when LAND_CHECKOUT_PATH is set, noop otherwise. The + # e2e suite sets SQ_RUNWAY_LANDER=fake to drive outcomes from the payload. + - LANDER=${SQ_RUNWAY_LANDER:-} # Queue infrastructure connection - QUEUE_MYSQL_DSN=root:root@tcp(mysql-queue:3306)/submitqueue?parseTime=true # Level for the queue's own logs; info by default so its per-message diff --git a/service/runway/server/gitruntime.go b/service/runway/server/gitruntime.go index 8742ec677..fb30cfd53 100644 --- a/service/runway/server/gitruntime.go +++ b/service/runway/server/gitruntime.go @@ -23,36 +23,36 @@ import ( "path/filepath" "strings" - gitmerger "github.com/uber/submitqueue/runway/extension/merger/git" + gitlander "github.com/uber/submitqueue/runway/extension/lander/git" ) -// resolveGitRuntime determines the pinned git runtime the merger invokes. +// resolveGitRuntime determines the pinned git runtime the lander invokes. // -// The merger requires all three paths to be absolute and present — it refuses -// to construct otherwise, deliberately, so that a merge can never silently pick +// The lander requires all three paths to be absolute and present — it refuses +// to construct otherwise, deliberately, so that a land can never silently pick // up a different git than the one the deployment intended. Requiring an // operator to supply all three by hand turns that safeguard into a boot-time // trap, so each is derived from the installed git when its environment // override is unset. GIT_EXECUTABLE, GIT_EXEC_PATH, and GIT_TEMPLATE_DIR still // take precedence, which is how a deployment pins a git other than the one on // PATH. -func resolveGitRuntime(ctx context.Context) (gitmerger.GitRuntime, error) { +func resolveGitRuntime(ctx context.Context) (gitlander.GitRuntime, error) { executable, err := resolveGitExecutable() if err != nil { - return gitmerger.GitRuntime{}, err + return gitlander.GitRuntime{}, err } execPath, err := resolveGitExecPath(ctx, executable) if err != nil { - return gitmerger.GitRuntime{}, err + return gitlander.GitRuntime{}, err } templateDir, err := resolveGitTemplateDir(execPath) if err != nil { - return gitmerger.GitRuntime{}, err + return gitlander.GitRuntime{}, err } - return gitmerger.GitRuntime{ + return gitlander.GitRuntime{ Executable: executable, ExecPath: execPath, TemplateDir: templateDir, diff --git a/service/runway/server/main.go b/service/runway/server/main.go index dc9985001..c7d7d50a7 100644 --- a/service/runway/server/main.go +++ b/service/runway/server/main.go @@ -31,7 +31,7 @@ import ( _ "github.com/go-sql-driver/mysql" "github.com/uber-go/tally" - mergestrategypb "github.com/uber/submitqueue/api/base/mergestrategy/protopb" + landstrategypb "github.com/uber/submitqueue/api/base/landstrategy/protopb" runwaymq "github.com/uber/submitqueue/api/runway/messagequeue" pb "github.com/uber/submitqueue/api/runway/protopb" "github.com/uber/submitqueue/platform/consumer" @@ -45,12 +45,12 @@ import ( queueMySQL "github.com/uber/submitqueue/platform/extension/messagequeue/mysql" "github.com/uber/submitqueue/runway/controller" "github.com/uber/submitqueue/runway/controller/dlq" - "github.com/uber/submitqueue/runway/controller/merge" - "github.com/uber/submitqueue/runway/controller/mergeconflictcheck" - "github.com/uber/submitqueue/runway/extension/merger" - "github.com/uber/submitqueue/runway/extension/merger/fake" - gitmerger "github.com/uber/submitqueue/runway/extension/merger/git" - "github.com/uber/submitqueue/runway/extension/merger/noop" + "github.com/uber/submitqueue/runway/controller/land" + "github.com/uber/submitqueue/runway/controller/landconflictcheck" + "github.com/uber/submitqueue/runway/extension/lander" + "github.com/uber/submitqueue/runway/extension/lander/fake" + gitlander "github.com/uber/submitqueue/runway/extension/lander/git" + "github.com/uber/submitqueue/runway/extension/lander/noop" "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/reflection" @@ -171,37 +171,37 @@ func run() error { gate, ) - mergerFactory, err := newMergerFactory(ctx, logger, scope.SubScope("merger")) + landerFactory, err := newLanderFactory(ctx, logger, scope.SubScope("lander")) if err != nil { - return fmt.Errorf("failed to create merger factory: %w", err) + return fmt.Errorf("failed to create lander factory: %w", err) } - mergeConflictCheckController := mergeconflictcheck.NewController(mergeconflictcheck.Params{ + landConflictCheckController := landconflictcheck.NewController(landconflictcheck.Params{ Logger: logger.Sugar(), Scope: scope, - MergerFactory: mergerFactory, + LanderFactory: landerFactory, Registry: registry, - TopicKey: runwaymq.TopicKeyMergeConflictCheck, - ConsumerGroup: "runway-mergeconflictcheck", + TopicKey: runwaymq.TopicKeyLandConflictCheck, + ConsumerGroup: "runway-landconflictcheck", }) - if err := primaryConsumer.Register(mergeConflictCheckController); err != nil { - return fmt.Errorf("failed to register merge-conflict-check controller: %w", err) + if err := primaryConsumer.Register(landConflictCheckController); err != nil { + return fmt.Errorf("failed to register land-conflict-check controller: %w", err) } - mergeController := merge.NewController(merge.Params{ + landController := land.NewController(land.Params{ Logger: logger.Sugar(), Scope: scope, - MergerFactory: mergerFactory, + LanderFactory: landerFactory, Registry: registry, - TopicKey: runwaymq.TopicKeyMerge, - ConsumerGroup: "runway-merge", + TopicKey: runwaymq.TopicKeyLand, + ConsumerGroup: "runway-land", }) - if err := primaryConsumer.Register(mergeController); err != nil { - return fmt.Errorf("failed to register merge controller: %w", err) + if err := primaryConsumer.Register(landController); err != nil { + return fmt.Errorf("failed to register land controller: %w", err) } logger.Info("controllers registered", zap.Int("primary", 2)) - // The DLQ consumer reconciles dead-lettered merge requests: it republishes a + // The DLQ consumer reconciles dead-lettered land requests: it republishes a // terminal FAILED result to the signal topic so the client's correlation id // always resolves. It uses AlwaysRetryableProcessor so a transient publish // failure retries forever rather than dead-lettering again. @@ -210,28 +210,28 @@ func run() error { gate, ) - mergeConflictCheckDLQController := dlq.NewController(dlq.Params{ + landConflictCheckDLQController := dlq.NewController(dlq.Params{ Logger: logger.Sugar(), Scope: scope, Registry: registry, - TopicKey: dlq.TopicKey(runwaymq.TopicKeyMergeConflictCheck), - SignalTopicKey: runwaymq.TopicKeyMergeConflictCheckSignal, - ConsumerGroup: "runway-mergeconflictcheck-dlq", + TopicKey: dlq.TopicKey(runwaymq.TopicKeyLandConflictCheck), + SignalTopicKey: runwaymq.TopicKeyLandConflictCheckSignal, + ConsumerGroup: "runway-landconflictcheck-dlq", }) - if err := dlqConsumer.Register(mergeConflictCheckDLQController); err != nil { - return fmt.Errorf("failed to register merge-conflict-check DLQ controller: %w", err) + if err := dlqConsumer.Register(landConflictCheckDLQController); err != nil { + return fmt.Errorf("failed to register land-conflict-check DLQ controller: %w", err) } - mergeDLQController := dlq.NewController(dlq.Params{ + landDLQController := dlq.NewController(dlq.Params{ Logger: logger.Sugar(), Scope: scope, Registry: registry, - TopicKey: dlq.TopicKey(runwaymq.TopicKeyMerge), - SignalTopicKey: runwaymq.TopicKeyMergeSignal, - ConsumerGroup: "runway-merge-dlq", + TopicKey: dlq.TopicKey(runwaymq.TopicKeyLand), + SignalTopicKey: runwaymq.TopicKeyLandSignal, + ConsumerGroup: "runway-land-dlq", }) - if err := dlqConsumer.Register(mergeDLQController); err != nil { - return fmt.Errorf("failed to register merge DLQ controller: %w", err) + if err := dlqConsumer.Register(landDLQController); err != nil { + return fmt.Errorf("failed to register land DLQ controller: %w", err) } logger.Info("DLQ controllers registered", zap.Int("dlq", 2)) @@ -305,41 +305,41 @@ func run() error { return err } -// newMergerFactory builds the mergers for the server. +// newLanderFactory builds the landers for the server. // -// MERGER pins every queue to one implementation explicitly, which is how a test +// LANDER pins every queue to one implementation explicitly, which is how a test // holds the service to a fake without a git checkout. Left unset, each queue -// resolves its own merge target through the merge configuration, so a +// resolves its own land target through the land configuration, so a // deployment can serve several repositories from one Runway. // -// The fake is reachable only through MERGER, never through the configuration +// The fake is reachable only through LANDER, never through the configuration // file: an implementation whose outcomes are steered by markers in a change URI // has no business being selectable by a production config. -func newMergerFactory(ctx context.Context, logger *zap.Logger, scope tally.Scope) (merger.Factory, error) { - switch impl := strings.ToLower(strings.TrimSpace(os.Getenv("MERGER"))); impl { +func newLanderFactory(ctx context.Context, logger *zap.Logger, scope tally.Scope) (lander.Factory, error) { + switch impl := strings.ToLower(strings.TrimSpace(os.Getenv("LANDER"))); impl { case "fake": // Marker-driven outcomes, for e2e tests that need Runway to fail on // demand without a git checkout. Never production. - logger.Info("MERGER=fake; using marker-driven fake merger for every queue") - return &fakeMergerFactory{seq: new(atomic.Uint64)}, nil + logger.Info("LANDER=fake; using marker-driven fake lander for every queue") + return &fakeLanderFactory{seq: new(atomic.Uint64)}, nil case "noop": - logger.Info("MERGER=noop; using noop merger for every queue") - return &noopMergerFactory{seq: new(atomic.Uint64)}, nil + logger.Info("LANDER=noop; using noop lander for every queue") + return &noopLanderFactory{seq: new(atomic.Uint64)}, nil case "", "git": - // Fall through to the configured per-queue merge targets. + // Fall through to the configured per-queue land targets. default: - return nil, fmt.Errorf("invalid MERGER %q", impl) + return nil, fmt.Errorf("invalid LANDER %q", impl) } - cfg, err := loadMergeConfigFromEnv(logger) + cfg, err := loadLandConfigFromEnv(logger) if err != nil { return nil, err } // The git runtime is resolved only when something actually needs it, so a - // deployment running nothing but the noop merger does not require git to be + // deployment running nothing but the noop lander does not require git to be // installed at all. - var runtime gitmerger.GitRuntime + var runtime gitlander.GitRuntime if cfg.usesGit() { runtime, err = resolveGitRuntime(ctx) if err != nil { @@ -352,115 +352,115 @@ func newMergerFactory(ctx context.Context, logger *zap.Logger, scope tally.Scope ) } - builder := &mergerBuilder{ + builder := &landerBuilder{ ctx: ctx, logger: logger, scope: scope, runtime: runtime, - byTarget: make(map[string]merger.Factory), + byTarget: make(map[string]lander.Factory), seq: new(atomic.Uint64), } - fallback, err := builder.build(cfg.Defaults.Merger, "defaults") + fallback, err := builder.build(cfg.Defaults.Lander, "defaults") if err != nil { return nil, err } - byQueue := make(map[string]merger.Factory, len(cfg.Queues)) + byQueue := make(map[string]lander.Factory, len(cfg.Queues)) for _, q := range cfg.Queues { - if q.Merger == nil { + if q.Lander == nil { continue } - m, err := builder.build(*q.Merger, fmt.Sprintf("queue %q", q.Name)) + m, err := builder.build(*q.Lander, fmt.Sprintf("queue %q", q.Name)) if err != nil { return nil, err } byQueue[q.Name] = m } - logger.Info("mergers configured", - zap.String("default_type", cfg.Defaults.Merger.Type), + logger.Info("landers configured", + zap.String("default_type", cfg.Defaults.Lander.Type), zap.Int("queue_overrides", len(byQueue)), ) - return mergerRegistry{byQueue: byQueue, fallback: fallback}, nil + return landerRegistry{byQueue: byQueue, fallback: fallback}, nil } -// loadMergeConfigFromEnv reads the merge configuration file when one is +// loadLandConfigFromEnv reads the land configuration file when one is // configured, and otherwise reconstructs the equivalent single-queue -// configuration from the MERGE_* environment. +// configuration from the LAND_* environment. // // The environment path predates the file and remains the shortest way to run -// one merge target, so it stays supported rather than being migrated away; +// one land target, so it stays supported rather than being migrated away; // expressing it as the same config type means there is still only one code path -// building mergers. -func loadMergeConfigFromEnv(logger *zap.Logger) (mergeConfig, error) { - if path := os.Getenv("MERGE_CONFIG_PATH"); path != "" { - cfg, err := loadMergeConfig(path) +// building landers. +func loadLandConfigFromEnv(logger *zap.Logger) (landConfig, error) { + if path := os.Getenv("LAND_CONFIG_PATH"); path != "" { + cfg, err := loadLandConfig(path) if err != nil { - return mergeConfig{}, err + return landConfig{}, err } - logger.Info("merge config loaded", zap.String("path", path), zap.Int("queues", len(cfg.Queues))) + logger.Info("land config loaded", zap.String("path", path), zap.Int("queues", len(cfg.Queues))) return cfg, nil } - checkoutPath := os.Getenv("MERGE_CHECKOUT_PATH") + checkoutPath := os.Getenv("LAND_CHECKOUT_PATH") if checkoutPath == "" { - logger.Info("neither MERGE_CONFIG_PATH nor MERGE_CHECKOUT_PATH set; using noop merger") - return mergeConfig{Defaults: queueMergeConfig{Merger: mergerConfig{Type: mergerTypeNoop}}}, nil + logger.Info("neither LAND_CONFIG_PATH nor LAND_CHECKOUT_PATH set; using noop lander") + return landConfig{Defaults: queueLandConfig{Lander: landerConfig{Type: landerTypeNoop}}}, nil } - checkStaleness := envBool("MERGE_CHECK_STALENESS", true) - cfg := mergeConfig{Defaults: queueMergeConfig{Merger: mergerConfig{ - Type: mergerTypeGit, + checkStaleness := envBool("LAND_CHECK_STALENESS", true) + cfg := landConfig{Defaults: queueLandConfig{Lander: landerConfig{ + Type: landerTypeGit, CheckoutPath: checkoutPath, - Remote: envOr("MERGE_REMOTE", "origin"), - Target: envOr("MERGE_TARGET", "main"), - DefaultStrategy: os.Getenv("MERGE_DEFAULT_STRATEGY"), + Remote: envOr("LAND_REMOTE", "origin"), + Target: envOr("LAND_TARGET", "main"), + DefaultStrategy: os.Getenv("LAND_DEFAULT_STRATEGY"), CheckStaleness: &checkStaleness, // Off by default: it lifts git's refusal to join two unrelated history // graphs, which is a safeguard everywhere except a queue whose purpose // is importing one repository's history into another. - AllowUnrelatedHistories: envBool("MERGE_ALLOW_UNRELATED_HISTORIES", false), - UpdateHeadBranch: envBool("MERGE_UPDATE_HEAD_BRANCH", false), - FetchRefspecs: splitRefspecs(os.Getenv("MERGE_FETCH_REFSPECS")), - CommitterName: os.Getenv("MERGE_COMMITTER_NAME"), - CommitterEmail: os.Getenv("MERGE_COMMITTER_EMAIL"), + AllowUnrelatedHistories: envBool("LAND_ALLOW_UNRELATED_HISTORIES", false), + UpdateHeadBranch: envBool("LAND_UPDATE_HEAD_BRANCH", false), + FetchRefspecs: splitRefspecs(os.Getenv("LAND_FETCH_REFSPECS")), + CommitterName: os.Getenv("LAND_COMMITTER_NAME"), + CommitterEmail: os.Getenv("LAND_COMMITTER_EMAIL"), }}} if err := cfg.normalizeAndValidate(); err != nil { - return mergeConfig{}, fmt.Errorf("invalid MERGE_* environment: %w", err) + return landConfig{}, fmt.Errorf("invalid LAND_* environment: %w", err) } return cfg, nil } -// mergerBuilder constructs merger factories, reusing one git instance per merge +// landerBuilder constructs lander factories, reusing one git instance per land // target. // -// Sharing matters: a git merger serializes its own operations against the +// Sharing matters: a git lander serializes its own operations against the // working tree it owns, so two queues landing on the same target must be the // same instance to be serialized against each other. Two instances would hold // separate locks over one working tree and reset it out from under each other -// mid-merge. A noop target has no such constraint and is built per queue, so it +// mid-land. A noop target has no such constraint and is built per queue, so it // carries the queue's own config. -type mergerBuilder struct { +type landerBuilder struct { ctx context.Context logger *zap.Logger scope tally.Scope - runtime gitmerger.GitRuntime - // byTarget caches one merger per checkout path. Keying on the path alone is + runtime gitlander.GitRuntime + // byTarget caches one lander per checkout path. Keying on the path alone is // safe only because validation has already rejected two queues that share a - // checkout and disagree anywhere in their merger config: the cached instance + // checkout and disagree anywhere in their lander config: the cached instance // is built from whichever queue arrived first, so a divergent second queue // would silently run with the first one's settings. Widening what may share // a checkout means widening that check with it. - byTarget map[string]merger.Factory - // seq is the process-wide counter the noop mergers mint revision ids from, + byTarget map[string]lander.Factory + // seq is the process-wide counter the noop landers mint revision ids from, // held here so ids stay unique across every queue. seq *atomic.Uint64 } -func (b *mergerBuilder) build(cfg mergerConfig, where string) (merger.Factory, error) { - if cfg.Type != mergerTypeGit { - return &noopMergerFactory{seq: b.seq}, nil +func (b *landerBuilder) build(cfg landerConfig, where string) (lander.Factory, error) { + if cfg.Type != landerTypeGit { + return &noopLanderFactory{seq: b.seq}, nil } if existing, ok := b.byTarget[cfg.CheckoutPath]; ok { @@ -473,7 +473,7 @@ func (b *mergerBuilder) build(cfg mergerConfig, where string) (merger.Factory, e } } - m, err := gitmerger.NewMerger(gitmerger.Params{ + m, err := gitlander.NewLander(gitlander.Params{ CheckoutPath: cfg.CheckoutPath, Remote: cfg.Remote, Target: cfg.Target, @@ -490,92 +490,92 @@ func (b *mergerBuilder) build(cfg mergerConfig, where string) (merger.Factory, e MetricsScope: b.scope, }) if err != nil { - return nil, fmt.Errorf("%s: failed to build git merger: %w", where, err) + return nil, fmt.Errorf("%s: failed to build git lander: %w", where, err) } - b.logger.Info("git merger configured", + b.logger.Info("git lander configured", zap.String("checkout", cfg.CheckoutPath), zap.String("target", cfg.Target), zap.String("default_strategy", cfg.strategy().String()), zap.Bool("update_head_branch", cfg.UpdateHeadBranch), ) - f := &gitMergerFactory{merger: m} + f := &gitLanderFactory{lander: m} b.byTarget[cfg.CheckoutPath] = f return f, nil } -// gitMergerFactory returns one git-backed merger for every queue routed to it. -// The merger owns one checkout and serializes its own operations, so a single -// instance is shared — which is why this is the one merger factory that does +// gitLanderFactory returns one git-backed lander for every queue routed to it. +// The lander owns one checkout and serializes its own operations, so a single +// instance is shared — which is why this is the one lander factory that does // not forward its Config. Queues landing on different targets get different -// instances of it, resolved through mergerRegistry. -type gitMergerFactory struct { - merger merger.Merger +// instances of it, resolved through landerRegistry. +type gitLanderFactory struct { + lander lander.Lander } -func (f *gitMergerFactory) For(_ merger.Config) (merger.Merger, error) { - return f.merger, nil +func (f *gitLanderFactory) For(_ lander.Config) (lander.Lander, error) { + return f.lander, nil } -// noopMergerFactory builds a noop merger per queue, bound to that queue's +// noopLanderFactory builds a noop lander per queue, bound to that queue's // config. The synthetic revision-id counter is held here rather than on the -// merger so ids stay unique across every queue in the process. -type noopMergerFactory struct { +// lander so ids stay unique across every queue in the process. +type noopLanderFactory struct { seq *atomic.Uint64 } -func (f *noopMergerFactory) For(cfg merger.Config) (merger.Merger, error) { +func (f *noopLanderFactory) For(cfg lander.Config) (lander.Lander, error) { return noop.New(cfg, f.seq), nil } -// fakeMergerFactory builds a fake merger per queue, bound to that queue's +// fakeLanderFactory builds a fake lander per queue, bound to that queue's // config. As with noop, the revision-id counter lives on the factory so ids // stay unique for the lifetime of the process. -type fakeMergerFactory struct { +type fakeLanderFactory struct { seq *atomic.Uint64 } -func (f *fakeMergerFactory) For(cfg merger.Config) (merger.Merger, error) { +func (f *fakeLanderFactory) For(cfg lander.Config) (lander.Lander, error) { return fake.New(cfg, f.seq), nil } -// mergerRegistry resolves each queue's merger factory, falling back to the +// landerRegistry resolves each queue's lander factory, falling back to the // default for a queue with no entry of its own. // -// It holds factories rather than mergers so the queue's Config reaches the +// It holds factories rather than landers so the queue's Config reaches the // implementation: whether an entry yields one shared instance or a fresh // per-queue one is the factory's business, not the registry's. -type mergerRegistry struct { - byQueue map[string]merger.Factory - fallback merger.Factory +type landerRegistry struct { + byQueue map[string]lander.Factory + fallback lander.Factory } -func (r mergerRegistry) For(cfg merger.Config) (merger.Merger, error) { +func (r landerRegistry) For(cfg lander.Config) (lander.Lander, error) { if f, ok := r.byQueue[cfg.QueueName]; ok { return f.For(cfg) } return r.fallback.For(cfg) } -// parseStrategy maps the MERGE_DEFAULT_STRATEGY env value to a concrete merge +// parseStrategy maps the LAND_DEFAULT_STRATEGY env value to a concrete land // strategy, defaulting to REBASE when unset. DEFAULT is rejected because it // cannot itself be the default a step resolves to. -func parseStrategy(name string) (mergestrategypb.Strategy, error) { +func parseStrategy(name string) (landstrategypb.Strategy, error) { switch strings.ToUpper(strings.TrimSpace(name)) { case "", "REBASE": - return mergestrategypb.Strategy_REBASE, nil + return landstrategypb.Strategy_REBASE, nil case "SQUASH_REBASE": - return mergestrategypb.Strategy_SQUASH_REBASE, nil + return landstrategypb.Strategy_SQUASH_REBASE, nil case "MERGE": - return mergestrategypb.Strategy_MERGE, nil + return landstrategypb.Strategy_MERGE, nil case "PROMOTE": - return mergestrategypb.Strategy_PROMOTE, nil + return landstrategypb.Strategy_PROMOTE, nil default: - return mergestrategypb.Strategy_DEFAULT, fmt.Errorf("invalid MERGE_DEFAULT_STRATEGY %q", name) + return landstrategypb.Strategy_DEFAULT, fmt.Errorf("invalid LAND_DEFAULT_STRATEGY %q", name) } } -// splitRefspecs parses the comma-separated MERGE_FETCH_REFSPECS value. Empty +// splitRefspecs parses the comma-separated LAND_FETCH_REFSPECS value. Empty // entries are dropped so a trailing comma is harmless. func splitRefspecs(v string) []string { var out []string @@ -609,35 +609,35 @@ func envOr(key, fallback string) string { return fallback } -// newTopicRegistry builds the TopicRegistry for Runway's merge queues. Inbound -// topics (merge-conflict-check, merge) have subscriptions; outbound signal topics +// newTopicRegistry builds the TopicRegistry for Runway's land queues. Inbound +// topics (land-conflict-check, land) have subscriptions; outbound signal topics // are publish-only. func newTopicRegistry(q extqueue.Queue, subscriberName string) (consumer.TopicRegistry, error) { return consumer.NewTopicRegistry([]consumer.TopicConfig{ { - Key: runwaymq.TopicKeyMergeConflictCheck, - Name: "merge-conflict-check", + Key: runwaymq.TopicKeyLandConflictCheck, + Name: "land-conflict-check", Queue: q, Subscription: extqueue.DefaultSubscriptionConfig( - subscriberName, "runway-mergeconflictcheck", + subscriberName, "runway-landconflictcheck", ), }, { - Key: runwaymq.TopicKeyMergeConflictCheckSignal, - Name: "merge-conflict-check-signal", + Key: runwaymq.TopicKeyLandConflictCheckSignal, + Name: "land-conflict-check-signal", Queue: q, }, { - Key: runwaymq.TopicKeyMerge, - Name: "runway-merge", + Key: runwaymq.TopicKeyLand, + Name: "runway-land", Queue: q, Subscription: extqueue.DefaultSubscriptionConfig( - subscriberName, "runway-merge", + subscriberName, "runway-land", ), }, { - Key: runwaymq.TopicKeyMergeSignal, - Name: "merge-signal", + Key: runwaymq.TopicKeyLandSignal, + Name: "land-signal", Queue: q, }, // DLQ topics: the reconciler consumes these and republishes a FAILED @@ -645,19 +645,19 @@ func newTopicRegistry(q extqueue.Queue, subscriberName string) (consumer.TopicRe // topic name plus the "_dlq" suffix the subscriber uses when // dead-lettering (see dlq.TopicKey / DefaultSubscriptionConfig). { - Key: dlq.TopicKey(runwaymq.TopicKeyMergeConflictCheck), - Name: "merge-conflict-check_dlq", + Key: dlq.TopicKey(runwaymq.TopicKeyLandConflictCheck), + Name: "land-conflict-check_dlq", Queue: q, Subscription: extqueue.DLQSubscriptionConfig( - subscriberName, "runway-mergeconflictcheck-dlq", + subscriberName, "runway-landconflictcheck-dlq", ), }, { - Key: dlq.TopicKey(runwaymq.TopicKeyMerge), - Name: "runway-merge_dlq", + Key: dlq.TopicKey(runwaymq.TopicKeyLand), + Name: "runway-land_dlq", Queue: q, Subscription: extqueue.DLQSubscriptionConfig( - subscriberName, "runway-merge-dlq", + subscriberName, "runway-land-dlq", ), }, }) diff --git a/service/submitqueue/README.md b/service/submitqueue/README.md index cc80c582b..e425050a6 100644 --- a/service/submitqueue/README.md +++ b/service/submitqueue/README.md @@ -1,6 +1,6 @@ # SubmitQueue Services -Runnable wiring for the **SubmitQueue** domain's two services — the Gateway (entry point for land requests) and the Orchestrator (coordinates the pipeline) — wired with MySQL-backed extensions. The full Docker Compose workflow also starts Runway, which performs merge-conflict checks and merges. +Runnable wiring for the **SubmitQueue** domain's two services — the Gateway (entry point for land requests) and the Orchestrator (coordinates the pipeline) — wired with MySQL-backed extensions. The full Docker Compose workflow also starts Runway, which performs land-conflict checks and lands changes. ## Starting diff --git a/service/submitqueue/demo/provider/README.md b/service/submitqueue/demo/provider/README.md index 4ee0987d2..735aadf03 100644 --- a/service/submitqueue/demo/provider/README.md +++ b/service/submitqueue/demo/provider/README.md @@ -5,7 +5,7 @@ Each directory here is one **provider** — a code-hosting system SubmitQueue la | File | Selects | |---|---| | `profiles.yaml` | the change provider, build runner, conflict analyzer, scorer and build budget each queue resolves to (read by the orchestrator) | -| `merge.yaml` | the merge target each queue lands on (read by Runway) | +| `land.yaml` | the land target each queue lands on (read by Runway) | Neither holds a secret. Each integration names the *environment variable* carrying its credential, so these files stay committable and rotating a token needs no edit. @@ -13,13 +13,13 @@ Pick one with `make local-submitqueue-start PROVIDER=`, which bind-mounts | Directory | What it demonstrates | Needs | |---|---|---| -| [`fake/`](fake) | the queue alone: a change is a URI, nothing merges anywhere — the default, and what the quickstart runs | nothing | +| [`fake/`](fake) | the queue alone: a change is a URI, nothing lands anywhere — the default, and what the quickstart runs | nothing | | [`git/`](git) | a plain git remote with no provider at all: change metadata read out of the repository, and a real fetch, cherry-pick and push against it | nothing | | [`github/`](github) | a live provider: GitHub change metadata, a real repository, pull requests marked merged | a repository and a token | -The three are a ladder, and the rung is the only thing that changes: the same commands land against all of them. `git/` is worth reading first of the two real ones. It is proof that neither half needs a provider — change metadata comes from reading the repository and the merge is the same Runway code path, both against a bare repository addressed by path, with no credential and no API — and it is what the hermetic git E2E (`make e2e-git-test`) runs against. +The three are a ladder, and the rung is the only thing that changes: the same commands land against all of them. `git/` is worth reading first of the two real ones. It is proof that neither half needs a provider — change metadata comes from reading the repository and the land is the same Runway code path, both against a bare repository addressed by path, with no credential and no API — and it is what the hermetic git E2E (`make e2e-git-test`) runs against. -Note that the orchestrator and Runway each keep their **own** copy of a queue's repository and configure their own remote for it: the change provider's copy is in `profiles.yaml`, the merger's checkout in `merge.yaml`, and they are independent even when they name the same remote. That is the same shape a deployment has when both point at a remote host. +Note that the orchestrator and Runway each keep their **own** copy of a queue's repository and configure their own remote for it: the change provider's copy is in `profiles.yaml`, the lander's checkout in `land.yaml`, and they are independent even when they name the same remote. That is the same shape a deployment has when both point at a remote host. ## Adding a provider @@ -28,7 +28,7 @@ Everything provider-specific is reached through an existing seam, so a new provi | Touchpoint | Why | |---|---| | `platform/base/change/{provider}/change_id.go` | parse `{provider}://…` change URIs | -| `runway/extension/merger/git/changeref.go` | one `case` in `resolveChange`, mapping the URI to the ref the provider publishes a change's head under — GitHub `refs/pull/{n}/head`, GitLab `refs/merge-requests/{iid}/head` | +| `runway/extension/lander/git/changeref.go` | one `case` in `resolveChange`, mapping the URI to the ref the provider publishes a change's head under — GitHub `refs/pull/{n}/head`, GitLab `refs/merge-requests/{iid}/head` | | `submitqueue/extension/changeprovider/{provider}/` and one case in `changeprovider/routing` | fetch change metadata | | `submitqueue/extension/buildrunner/{provider}/` | only if the provider's CI is not already covered by the Buildkite or GitHub Actions runners | | `service/submitqueue/gateway/client/main.go` | one case in `resolvePullRequest`, so `land -pr ` accepts the provider's change URLs | @@ -37,11 +37,11 @@ Everything provider-specific is reached through an existing seam, so a new provi The Makefile line is there because a mode's *mounts* are not something the two config files can express: `github` needs a credential in the environment, `git` needs the sandbox and checkout directories bind-mounted, and `fake` needs neither. A provider reaching a remote API over a token is the common case and can reuse `docker-compose.provider.yml` verbatim, so for most new providers that line names an existing file rather than a new one. -What is **not** on that list is the point of it: the merger's apply and push paths, the head-branch update, the orchestrator pipeline, the wire contract, and the hermetic git E2E are all provider-independent and need no change. +What is **not** on that list is the point of it: the lander's apply and push paths, the head-branch update, the orchestrator pipeline, the wire contract, and the hermetic git E2E are all provider-independent and need no change. Two of those deserve explanation. -**The merger stays provider-neutral** because `resolveChange` reduces every URI to the same three things — the commit to apply, the ref it lives under, and a label — before any git command runs. The apply paths never learn which provider a change came from. +**The lander stays provider-neutral** because `resolveChange` reduces every URI to the same three things — the commit to apply, the ref it lives under, and a label — before any git command runs. The apply paths never learn which provider a change came from. **Marking a change merged needs no provider API.** A provider decides whether a change merged while it processes the push to the target branch, comparing the change's recorded head against what that push makes reachable. `MERGE` and `PROMOTE` satisfy that by construction; the rewriting strategies do not, so `updateHeadBranch` moves the change's head branch to the commit it landed as — as its own push, immediately before the target is pushed. The ordering is the mechanism: a head moved *after* the target has been pushed, or in the same atomic push, is recorded too late, and the provider marks the change closed rather than merged even though its head is demonstrably on the target. That works by matching a SHA against the remote's branch tips — no change number, no API call — so it behaves identically for a GitHub pull request and a GitLab merge request. The one case it cannot serve is a change proposed from a fork, whose head branch lives in another repository: such a change lands and stays open. diff --git a/service/submitqueue/demo/provider/fake/land.yaml b/service/submitqueue/demo/provider/fake/land.yaml new file mode 100644 index 000000000..7b3709db5 --- /dev/null +++ b/service/submitqueue/demo/provider/fake/land.yaml @@ -0,0 +1,13 @@ +# Land targets for the "fake" example: there are none. +# +# Nothing here lands. The noop lander reports success without touching a +# repository, which is what lets a request reach `landed` with no repository to +# land into. +# +# That is the mode's limit, and worth knowing before reading anything into a +# green run: `landed` here means the pipeline ran to completion, not that a +# commit exists anywhere. Switch to PROVIDER=git for a real land into a local +# repository, still without a credential — see ../git/land.yaml. + +defaults: + lander: {type: noop} diff --git a/service/submitqueue/demo/provider/fake/merge.yaml b/service/submitqueue/demo/provider/fake/merge.yaml deleted file mode 100644 index 19f3eb358..000000000 --- a/service/submitqueue/demo/provider/fake/merge.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# Merge targets for the "fake" example: there are none. -# -# Nothing here merges. The noop merger reports success without touching a -# repository, which is what lets a request reach `landed` with no repository to -# land into. -# -# That is the mode's limit, and worth knowing before reading anything into a -# green run: `landed` here means the pipeline ran to completion, not that a -# commit exists anywhere. Switch to PROVIDER=git for a real merge into a local -# repository, still without a credential — see ../git/merge.yaml. - -defaults: - merger: {type: noop} diff --git a/service/submitqueue/demo/provider/git/BUILD.bazel b/service/submitqueue/demo/provider/git/BUILD.bazel index 0cd7d9377..72d8bc953 100644 --- a/service/submitqueue/demo/provider/git/BUILD.bazel +++ b/service/submitqueue/demo/provider/git/BUILD.bazel @@ -7,7 +7,7 @@ orchestrator and runway containers. filegroup( name = "config", srcs = [ - "merge.yaml", + "land.yaml", "profiles.yaml", ], visibility = ["//test:__subpackages__"], diff --git a/service/submitqueue/demo/provider/git/merge.yaml b/service/submitqueue/demo/provider/git/land.yaml similarity index 80% rename from service/submitqueue/demo/provider/git/merge.yaml rename to service/submitqueue/demo/provider/git/land.yaml index fcba9da95..f295fa62e 100644 --- a/service/submitqueue/demo/provider/git/merge.yaml +++ b/service/submitqueue/demo/provider/git/land.yaml @@ -1,37 +1,37 @@ -# Merge targets for the "git" example: a plain git remote with no provider. +# Land targets for the "git" example: a plain git remote with no provider. # # The target is a bare repository on a shared volume, addressed by path. That -# exercises the whole merge machinery (real fetch, cherry-pick, push, +# exercises the whole land machinery (real fetch, cherry-pick, push, # head-branch update) with no credential, no network, and no provider account, # which is what lets the hermetic git E2E (`make e2e-git-test`) gate PRs in CI. # # It doubles as the worked example of a non-GitHub target: nothing below names a -# provider, because the merger does not have one. See ../README.md. +# provider, because the lander does not have one. See ../README.md. defaults: - # Any queue without an entry below does not merge for real. - merger: {type: noop} + # Any queue without an entry below does not land for real. + lander: {type: noop} queues: # The queue `make demo-requests` and `make land` use by default. Squash-rebases to # match the GitHub demo, so switching providers changes where changes come # from and not what landing does to them. - name: demo-queue - merger: + lander: type: git # A local path needs no credential, so no tokenEnv is named. remoteUrl: file:///srv/git/sandbox.git remote: origin target: main # Deliberately not e2e-git-queue's working tree. Queues sharing a checkout - # share a single merger instance, so two differing configurations pointing + # share a single lander instance, so two differing configurations pointing # at one checkout is refused at startup. checkoutPath: /var/runway/checkouts/demo defaultStrategy: SQUASH_REBASE checkStaleness: true # Rewriting strategies leave the change's original head unreachable from # the target, so its branch is moved to the commit it landed as. On a - # provider this is what makes the change show as merged; here it is simply + # provider this is what makes the change show as landed; here it is simply # observable as the branch having moved. updateHeadBranch: true @@ -39,7 +39,7 @@ queues: # reached the target branch, in what order, and in how many ref updates. These # settings are what those assertions expect. - name: e2e-git-queue - merger: + lander: type: git remoteUrl: file:///srv/git/sandbox.git remote: origin diff --git a/service/submitqueue/demo/provider/git/profiles.yaml b/service/submitqueue/demo/provider/git/profiles.yaml index e47054cc8..9a78af19c 100644 --- a/service/submitqueue/demo/provider/git/profiles.yaml +++ b/service/submitqueue/demo/provider/git/profiles.yaml @@ -1,8 +1,8 @@ # Extension profiles for the "git" example: a plain git remote with no provider. # # There is no provider to ask about a change, and no CI to run, so both edge -# integrations stay fake. What this example exercises is the merge itself — see -# merge.yaml. +# integrations stay fake. What this example exercises is the land itself — see +# land.yaml. defaults: # A local git remote has no API to fetch change metadata from; the fake @@ -18,7 +18,7 @@ queues: # batches that touch a shared directory, matching the github mode. # # Both halves are real here. The change provider keeps its own copy of the - # same bare repository Runway merges into and reads each change out of it, so + # same bare repository Runway lands into and reads each change out of it, so # the analyzer keys on paths that were actually committed — including for a # change submitted by hand, which nothing else could have described. - name: demo-queue @@ -27,7 +27,7 @@ queues: git: # The orchestrator's own copy, and its own remote pointing at the # sandbox — mounted read-only, since it only ever fetches. Runway - # configures the same repository separately for itself in merge.yaml. + # configures the same repository separately for itself in land.yaml. remoteUrl: file:///srv/git/sandbox.git target: main repoPath: /var/submitqueue/changerepos/demo diff --git a/service/submitqueue/demo/provider/github/merge.yaml b/service/submitqueue/demo/provider/github/land.yaml similarity index 90% rename from service/submitqueue/demo/provider/github/merge.yaml rename to service/submitqueue/demo/provider/github/land.yaml index 9cacb6ad6..f605d85ce 100644 --- a/service/submitqueue/demo/provider/github/merge.yaml +++ b/service/submitqueue/demo/provider/github/land.yaml @@ -1,4 +1,4 @@ -# Merge targets for the GitHub demo. +# Land targets for the GitHub demo. # # Edit the three placeholders below to point at your own scratch repository, # then: make local-submitqueue-start PROVIDER=github @@ -8,13 +8,13 @@ # edit here. defaults: - # Any queue without an entry below does not merge for real, so a typo in a + # Any queue without an entry below does not land for real, so a typo in a # queue name fails safe rather than landing somewhere unintended. - merger: {type: noop} + lander: {type: noop} queues: - name: demo-queue - merger: + lander: type: git # ---- point these at your own scratch repo ---- remoteUrl: https://github.com/behinddwalls/sq-demo.git @@ -43,6 +43,6 @@ queues: # which git would echo back in error messages. # # For SSH instead, drop tokenEnv, use an ssh:// remoteUrl, and mount a - # deploy key: the merger already passes SSH_AUTH_SOCK and + # deploy key: the lander already passes SSH_AUTH_SOCK and # GIT_SSH_COMMAND through to git. tokenEnv: GITHUB_TOKEN diff --git a/service/submitqueue/demo/requests/BUILD.bazel b/service/submitqueue/demo/requests/BUILD.bazel index e5a0dc656..66484a849 100644 --- a/service/submitqueue/demo/requests/BUILD.bazel +++ b/service/submitqueue/demo/requests/BUILD.bazel @@ -12,7 +12,7 @@ go_library( importpath = "github.com/uber/submitqueue/service/submitqueue/demo/requests", visibility = ["//visibility:private"], deps = [ - "//api/base/mergestrategy/protopb:go_default_library", + "//api/base/landstrategy/protopb:go_default_library", "//platform/base/change/git:go_default_library", "//platform/base/change/github:go_default_library", "//platform/fakemarker:go_default_library", @@ -24,7 +24,7 @@ go_library( go_binary( name = "requests", - # -provider git authors changes with the same pinned git the merger uses. + # -provider git authors changes with the same pinned git the lander uses. # Unused by the other two providers, which touch no repository. args = ["-git=$(location @git//:git)"], data = ["@git"], @@ -41,12 +41,12 @@ go_test( data = ["@git"], embed = [":go_default_library"], # The git source drives a real repository, with the same pinned git the - # merger and the sandbox provisioner use. + # lander and the sandbox provisioner use. env = { "SUBMITQUEUE_TEST_GIT": "$(location @git//:git)", }, deps = [ - "//api/base/mergestrategy/protopb:go_default_library", + "//api/base/landstrategy/protopb:go_default_library", "//platform/base/change/git:go_default_library", "//platform/fakemarker:go_default_library", "//platform/git/exec:go_default_library", diff --git a/service/submitqueue/demo/requests/fake.go b/service/submitqueue/demo/requests/fake.go index ee4c6a005..a7aec5241 100644 --- a/service/submitqueue/demo/requests/fake.go +++ b/service/submitqueue/demo/requests/fake.go @@ -38,7 +38,7 @@ const fakeRepo = "demo" // queue work, and the reason the quickstart needs neither a repository nor a // credential — at the cost of the URIs pointing at nothing, which is only // sound because the fake change provider echoes back whatever it is handed and -// the noop merger never tries to fetch it. +// the noop lander never tries to fetch it. type fakeSource struct{} func (fakeSource) baseSHA(_ context.Context, branch string) (string, error) { diff --git a/service/submitqueue/demo/requests/git.go b/service/submitqueue/demo/requests/git.go index 9559d1231..867413d42 100644 --- a/service/submitqueue/demo/requests/git.go +++ b/service/submitqueue/demo/requests/git.go @@ -26,7 +26,7 @@ import ( "github.com/uber/submitqueue/submitqueue/client" ) -// gitRemote is the authority in the change URIs this source mints. The merger +// gitRemote is the authority in the change URIs this source mints. The lander // reads the ref and commit out of a URI and reaches the repository through its // own configured remote, so this identifies a change rather than routing to it // — which is why a local sandbox can carry a hostname it does not answer to. diff --git a/service/submitqueue/demo/requests/main.go b/service/submitqueue/demo/requests/main.go index c1e3becfa..ca1ce28da 100644 --- a/service/submitqueue/demo/requests/main.go +++ b/service/submitqueue/demo/requests/main.go @@ -50,7 +50,7 @@ // // - fake (default): a change is a URI and nothing else. No repository, no // credential, no I/O — the fastest way to put traffic through the queue. -// - git: a branch pushed to the sandbox repository the stack merges into. +// - git: a branch pushed to the sandbox repository the stack lands into. // Real commits, still no credential. // - github: a real pull request over the REST API, which needs no clone and // no git binary, only GITHUB_TOKEN — the same credential the stack uses. @@ -66,7 +66,7 @@ import ( "strings" "time" - mergestrategypb "github.com/uber/submitqueue/api/base/mergestrategy/protopb" + landstrategypb "github.com/uber/submitqueue/api/base/landstrategy/protopb" gitexec "github.com/uber/submitqueue/platform/git/exec" "github.com/uber/submitqueue/submitqueue/client" "golang.org/x/sync/errgroup" @@ -130,7 +130,7 @@ func parseFlags() config { flag.BoolVar(&c.tls, "tls", false, "dial the gateway with transport security") flag.StringVar(&c.tokenEnv, "token-env", client.DefaultTokenEnv, "environment variable holding the gateway bearer token") flag.StringVar(&c.queue, "queue", "demo-queue", "queue to land on") - flag.StringVar(&c.strategy, "strategy", "SQUASH_REBASE", "merge strategy") + flag.StringVar(&c.strategy, "strategy", "SQUASH_REBASE", "land strategy") flag.Parse() // Only the GitHub source reads a credential; the other two must not fail, @@ -311,7 +311,7 @@ func newChangeSource(ctx context.Context, cfg config) (changeSource, func(), err } // sandboxRepo is the repository the git provider's sandbox holds, matching what -// tool/gitsandbox creates and what demo/provider/git/merge.yaml merges into. +// tool/gitsandbox creates and what demo/provider/git/land.yaml lands into. const sandboxRepo = "sandbox" // target describes where the run is creating changes, for the opening line. @@ -441,7 +441,7 @@ func changeFileCount(tag string, change, min int) int { // lander enqueues a request. *client.Client is the real one; a test supplies // its own to observe when each change is enqueued relative to when it is created. type lander interface { - Land(ctx context.Context, queue string, uris []string, strategy mergestrategypb.Strategy) (string, error) + Land(ctx context.Context, queue string, uris []string, strategy landstrategypb.Strategy) (string, error) } // createAndEnqueue creates the changes and puts them on the queue, filling @@ -465,7 +465,7 @@ func createAndEnqueue( src changeSource, sq lander, cfg config, - strategy mergestrategypb.Strategy, + strategy landstrategypb.Strategy, tag, baseSHA string, t *client.Tracker, ) ([]change, error) { @@ -494,7 +494,7 @@ func createIndependent( src changeSource, sq lander, cfg config, - strategy mergestrategypb.Strategy, + strategy landstrategypb.Strategy, tag, baseSHA string, t *client.Tracker, ) ([]change, error) { @@ -551,7 +551,7 @@ func createBurst( src changeSource, sq lander, cfg config, - strategy mergestrategypb.Strategy, + strategy landstrategypb.Strategy, tag, baseSHA string, t *client.Tracker, ) ([]change, error) { @@ -604,7 +604,7 @@ func createStack( src changeSource, sq lander, cfg config, - strategy mergestrategypb.Strategy, + strategy landstrategypb.Strategy, tag, baseSHA string, t *client.Tracker, ) ([]change, error) { diff --git a/service/submitqueue/demo/requests/main_test.go b/service/submitqueue/demo/requests/main_test.go index 7d0a36c89..7638ad614 100644 --- a/service/submitqueue/demo/requests/main_test.go +++ b/service/submitqueue/demo/requests/main_test.go @@ -25,7 +25,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - mergestrategypb "github.com/uber/submitqueue/api/base/mergestrategy/protopb" + landstrategypb "github.com/uber/submitqueue/api/base/landstrategy/protopb" "github.com/uber/submitqueue/submitqueue/client" ) @@ -233,9 +233,9 @@ func (r recordingSource) open(ctx context.Context, spec changeSpec) (openedChang } // landerFunc adapts a function to the lander interface. -type landerFunc func(context.Context, string, []string, mergestrategypb.Strategy) (string, error) +type landerFunc func(context.Context, string, []string, landstrategypb.Strategy) (string, error) -func (f landerFunc) Land(ctx context.Context, queue string, uris []string, s mergestrategypb.Strategy) (string, error) { +func (f landerFunc) Land(ctx context.Context, queue string, uris []string, s landstrategypb.Strategy) (string, error) { return f(ctx, queue, uris, s) } @@ -252,7 +252,7 @@ func TestCreateBurst_EnqueuesOnlyAfterEveryChangeIsCreated(t *testing.T) { var mu sync.Mutex createdWhenLanded := make([]int64, 0, count) - lander := landerFunc(func(context.Context, string, []string, mergestrategypb.Strategy) (string, error) { + lander := landerFunc(func(context.Context, string, []string, landstrategypb.Strategy) (string, error) { mu.Lock() createdWhenLanded = append(createdWhenLanded, atomic.LoadInt64(&created)) mu.Unlock() @@ -261,7 +261,7 @@ func TestCreateBurst_EnqueuesOnlyAfterEveryChangeIsCreated(t *testing.T) { tracker := client.NewTracker(client.NewRows(count)) got, err := createBurst(context.Background(), src, lander, cfg, - mergestrategypb.Strategy_SQUASH_REBASE, "run", "base", tracker) + landstrategypb.Strategy_SQUASH_REBASE, "run", "base", tracker) require.NoError(t, err) require.Len(t, got, count) diff --git a/service/submitqueue/docker-compose.fake.yml b/service/submitqueue/docker-compose.fake.yml index d078c5647..1c5228609 100644 --- a/service/submitqueue/docker-compose.fake.yml +++ b/service/submitqueue/docker-compose.fake.yml @@ -8,14 +8,14 @@ # # The lightest of the three overlays, and the only one that mounts nothing but # configuration: there is no repository to reach and no credential to carry, so -# a change is a URI and the merge is a no-op. See demo/provider/fake. +# a change is a URI and the land is a no-op. See demo/provider/fake. # # Required in the environment: -# SQ_PROVIDER_CONFIG_DIR directory holding profiles.yaml and merge.yaml +# SQ_PROVIDER_CONFIG_DIR directory holding profiles.yaml and land.yaml # # The stack would run without this overlay at all — the orchestrator falls back # to fake integrations when no profiles are configured, and Runway to the noop -# merger. Mounting the configuration anyway is what makes the mode a file +# lander. Mounting the configuration anyway is what makes the mode a file # someone can read and diff against ../git and ../github, rather than a # behaviour they have to know about. @@ -30,8 +30,8 @@ services: runway-service: environment: - # Per-queue merge targets. Every queue here resolves to the noop merger, + # Per-queue land targets. Every queue here resolves to the noop lander, # which reports success without touching a repository. - - MERGE_CONFIG_PATH=/etc/submitqueue/merge.yaml + - LAND_CONFIG_PATH=/etc/submitqueue/land.yaml volumes: - ${SQ_PROVIDER_CONFIG_DIR:?set SQ_PROVIDER_CONFIG_DIR, e.g. ./service/submitqueue/demo/provider/fake}:/etc/submitqueue:ro diff --git a/service/submitqueue/docker-compose.git.yml b/service/submitqueue/docker-compose.git.yml index e117575e6..7452dcade 100644 --- a/service/submitqueue/docker-compose.git.yml +++ b/service/submitqueue/docker-compose.git.yml @@ -1,24 +1,24 @@ -# Compose overlay: point Runway at a real git merge target. +# Compose overlay: point Runway at a real git land target. # # Layered over docker-compose.yml, which it does not modify: # # docker compose -f docker-compose.yml -f docker-compose.git.yml up # -# Used by the hermetic git E2E (`make e2e-git-test`), which merges it through +# Used by the hermetic git E2E (`make e2e-git-test`), which lands it through # testutil.WithOverlay. Everything else about the stack — the services, their # databases, the consumer gate — comes from the base file, so a service added # there reaches this variant too. # -# The merge target is a bare repository on a shared volume, addressed by path. -# That exercises the whole merge path (git in the image, checkout provisioning, +# The land target is a bare repository on a shared volume, addressed by path. +# That exercises the whole land path (git in the image, checkout provisioning, # real cherry-pick and push, head-branch updates) with no credential, no # network, and no account anywhere — which is what lets it gate a pull request. # # Required in the environment, all owned by the test: -# SQ_PROVIDER_CONFIG_DIR profiles.yaml / merge.yaml selecting each queue's +# SQ_PROVIDER_CONFIG_DIR profiles.yaml / land.yaml selecting each queue's # extensions (service/submitqueue/demo/provider/git) -# SQ_GIT_SANDBOX_DIR the bare repository the merger fetches and pushes -# SQ_RUNWAY_CHECKOUT_DIR storage for the working trees the merger owns +# SQ_GIT_SANDBOX_DIR the bare repository the lander fetches and pushes +# SQ_RUNWAY_CHECKOUT_DIR storage for the working trees the lander owns # # Optional: # SQ_ORCHESTRATOR_REPO_DIR storage for the orchestrator's own repository @@ -29,11 +29,11 @@ services: environment: # Which change provider, build runner, and conflict analyzer each queue # resolves to. The build runner stays fake here; the change provider and - # the merge are real. + # the land are real. - PROFILES_CONFIG_PATH=/etc/submitqueue/profiles.yaml volumes: - ${SQ_PROVIDER_CONFIG_DIR}:/etc/submitqueue:ro - # The same bare repository Runway merges into, read-only: the orchestrator + # The same bare repository Runway lands into, read-only: the orchestrator # only ever fetches from it. Each service keeps its own copy and points # its own remote at this one, which is the arrangement a real deployment # has with both of them pointing at a remote host. @@ -46,9 +46,9 @@ services: runway-service: environment: - # Per-queue merge targets. Without this Runway falls back to the noop - # merger and nothing would actually be pushed. - - MERGE_CONFIG_PATH=/etc/submitqueue/merge.yaml + # Per-queue land targets. Without this Runway falls back to the noop + # lander and nothing would actually be pushed. + - LAND_CONFIG_PATH=/etc/submitqueue/land.yaml volumes: - ${SQ_PROVIDER_CONFIG_DIR}:/etc/submitqueue:ro - ${SQ_GIT_SANDBOX_DIR}:/srv/git @@ -59,10 +59,10 @@ services: # SQ_RUNWAY_CHECKOUT_DIR at a host directory, because it runs the services # as the host user and a named volume would arrive owned by root. Left # unset — which is how `make local-submitqueue-start PROVIDER=git` runs it — - # it is a named volume instead, keeping the merger's object writes off a + # it is a named volume instead, keeping the lander's object writes off a # bind mount: this is the directory git clones, cherry-picks and commits # in, and on macOS a freshly written loose object can read back as corrupt - # over the host filesystem bridge, failing a merge that has nothing wrong + # over the host filesystem bridge, failing a land that has nothing wrong # with it. The bare repository above only receives finished pushes, so it # stays a bind mount and keeps `git log` on the host working. - ${SQ_RUNWAY_CHECKOUT_DIR:-runway-checkouts}:/var/runway/checkouts diff --git a/service/submitqueue/docker-compose.provider.yml b/service/submitqueue/docker-compose.provider.yml index fbd22d9a3..f5ec5dab0 100644 --- a/service/submitqueue/docker-compose.provider.yml +++ b/service/submitqueue/docker-compose.provider.yml @@ -11,7 +11,7 @@ # demo/provider// — so adding a provider needs no change to this file. # # Required in the environment: -# SQ_PROVIDER_CONFIG_DIR directory holding profiles.yaml and merge.yaml +# SQ_PROVIDER_CONFIG_DIR directory holding profiles.yaml and land.yaml # GITHUB_TOKEN (for the github example) the credential named by # tokenEnv in those files # @@ -32,15 +32,15 @@ services: runway-service: environment: - # Per-queue merge targets. Without this Runway falls back to the noop - # merger and nothing is ever pushed. - - MERGE_CONFIG_PATH=/etc/submitqueue/merge.yaml + # Per-queue land targets. Without this Runway falls back to the noop + # lander and nothing is ever pushed. + - LAND_CONFIG_PATH=/etc/submitqueue/land.yaml # Used to clone, fetch, and push. Written into the checkout as an HTTP # Authorization header, never into the remote URL. - GITHUB_TOKEN=${GITHUB_TOKEN:?set GITHUB_TOKEN to reach the provider} volumes: - ${SQ_PROVIDER_CONFIG_DIR:?set SQ_PROVIDER_CONFIG_DIR, e.g. ./service/submitqueue/demo/provider/github}:/etc/submitqueue:ro - # Working trees the merger owns. A named volume so a restart re-uses the + # Working trees the lander owns. A named volume so a restart re-uses the # clone instead of fetching the repository again. - runway-checkouts:/var/runway/checkouts diff --git a/service/submitqueue/docker-compose.yml b/service/submitqueue/docker-compose.yml index c7a11d77d..526dce477 100644 --- a/service/submitqueue/docker-compose.yml +++ b/service/submitqueue/docker-compose.yml @@ -118,7 +118,7 @@ services: mysql-queue: condition: service_healthy - # Runway performs the merge-conflict check and the committing merge. The + # Runway performs the land-conflict check and the committing land. The # orchestrator hands work to it on runway-owned topics and blocks on the # signal topics, so without runway a request stalls at "started". It only # needs the queue DB (shared with the orchestrator over mysql-queue); it has diff --git a/service/submitqueue/gateway/client/main.go b/service/submitqueue/gateway/client/main.go index f17dd01fa..f8081f412 100644 --- a/service/submitqueue/gateway/client/main.go +++ b/service/submitqueue/gateway/client/main.go @@ -360,7 +360,7 @@ func resolvePullRequests(ctx context.Context, urls []string) ([]string, error) { // resolvePullRequest dispatches a change URL to the provider that hosts it. // // Only GitHub is implemented. Adding a provider is a case here plus its own -// resolver, mirroring how the merger and the change provider each dispatch on +// resolver, mirroring how the lander and the change provider each dispatch on // the change's provider rather than assuming one. func resolvePullRequest(ctx context.Context, raw string) (string, error) { u, err := url.Parse(raw) diff --git a/service/submitqueue/gateway/server/mapper/BUILD.bazel b/service/submitqueue/gateway/server/mapper/BUILD.bazel index 76271ee87..84fed9d03 100644 --- a/service/submitqueue/gateway/server/mapper/BUILD.bazel +++ b/service/submitqueue/gateway/server/mapper/BUILD.bazel @@ -12,10 +12,10 @@ go_library( importpath = "github.com/uber/submitqueue/service/submitqueue/gateway/server/mapper", visibility = ["//visibility:public"], deps = [ - "//api/base/mergestrategy/protopb:go_default_library", + "//api/base/landstrategy/protopb:go_default_library", "//api/submitqueue/gateway/protopb:go_default_library", "//platform/base/change:go_default_library", - "//platform/base/mergestrategy:go_default_library", + "//platform/base/landstrategy:go_default_library", "//submitqueue/entity:go_default_library", ], ) @@ -32,10 +32,10 @@ go_test( embed = [":go_default_library"], deps = [ "//api/base/change/protopb:go_default_library", - "//api/base/mergestrategy/protopb:go_default_library", + "//api/base/landstrategy/protopb:go_default_library", "//api/submitqueue/gateway/protopb:go_default_library", "//platform/base/change:go_default_library", - "//platform/base/mergestrategy:go_default_library", + "//platform/base/landstrategy:go_default_library", "//submitqueue/entity:go_default_library", "@com_github_stretchr_testify//assert:go_default_library", "@com_github_stretchr_testify//require:go_default_library", diff --git a/service/submitqueue/gateway/server/mapper/land.go b/service/submitqueue/gateway/server/mapper/land.go index 8c77d56fb..a178c3d44 100644 --- a/service/submitqueue/gateway/server/mapper/land.go +++ b/service/submitqueue/gateway/server/mapper/land.go @@ -22,21 +22,21 @@ import ( "errors" "fmt" - mergestrategypb "github.com/uber/submitqueue/api/base/mergestrategy/protopb" + landstrategypb "github.com/uber/submitqueue/api/base/landstrategy/protopb" pb "github.com/uber/submitqueue/api/submitqueue/gateway/protopb" "github.com/uber/submitqueue/platform/base/change" - "github.com/uber/submitqueue/platform/base/mergestrategy" + "github.com/uber/submitqueue/platform/base/landstrategy" "github.com/uber/submitqueue/submitqueue/entity" ) // errUnknownStrategy is returned when a proto Strategy enum has no known -// mergestrategy.MergeStrategy mapping. +// landstrategy.Strategy mapping. var errUnknownStrategy = errors.New("unknown land strategy in proto message") // ProtoToLandRequest maps the wire LandRequest to the entity.LandRequest the controller operates on. // The ID is left empty; the controller assigns it. func ProtoToLandRequest(req *pb.LandRequest) (entity.LandRequest, error) { - strategy, err := resolveMergeStrategy(req.GetStrategy()) + strategy, err := resolveLandStrategy(req.GetStrategy()) if err != nil { return entity.LandRequest{}, fmt.Errorf("failed to map land strategy: %w", err) } @@ -47,21 +47,21 @@ func ProtoToLandRequest(req *pb.LandRequest) (entity.LandRequest, error) { }, nil } -// resolveMergeStrategy maps a proto Strategy enum to the shared mergestrategy.MergeStrategy. -func resolveMergeStrategy(s mergestrategypb.Strategy) (mergestrategy.MergeStrategy, error) { +// resolveLandStrategy maps a proto Strategy enum to the shared landstrategy.Strategy. +func resolveLandStrategy(s landstrategypb.Strategy) (landstrategy.Strategy, error) { switch s { - case mergestrategypb.Strategy_DEFAULT: + case landstrategypb.Strategy_DEFAULT: // TODO: resolve default strategy based on queue configuration - return mergestrategy.MergeStrategyRebase, nil - case mergestrategypb.Strategy_REBASE: - return mergestrategy.MergeStrategyRebase, nil - case mergestrategypb.Strategy_SQUASH_REBASE: - return mergestrategy.MergeStrategySquashRebase, nil - case mergestrategypb.Strategy_MERGE: - return mergestrategy.MergeStrategyMerge, nil - case mergestrategypb.Strategy_PROMOTE: - return mergestrategy.MergeStrategyPromote, nil + return landstrategy.StrategyRebase, nil + case landstrategypb.Strategy_REBASE: + return landstrategy.StrategyRebase, nil + case landstrategypb.Strategy_SQUASH_REBASE: + return landstrategy.StrategySquashRebase, nil + case landstrategypb.Strategy_MERGE: + return landstrategy.StrategyMerge, nil + case landstrategypb.Strategy_PROMOTE: + return landstrategy.StrategyPromote, nil default: - return mergestrategy.MergeStrategyUnknown, fmt.Errorf("%w: %v", errUnknownStrategy, s) + return landstrategy.StrategyUnknown, fmt.Errorf("%w: %v", errUnknownStrategy, s) } } diff --git a/service/submitqueue/gateway/server/mapper/land_test.go b/service/submitqueue/gateway/server/mapper/land_test.go index 348282856..d21857928 100644 --- a/service/submitqueue/gateway/server/mapper/land_test.go +++ b/service/submitqueue/gateway/server/mapper/land_test.go @@ -20,10 +20,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" changepb "github.com/uber/submitqueue/api/base/change/protopb" - mergestrategypb "github.com/uber/submitqueue/api/base/mergestrategy/protopb" + landstrategypb "github.com/uber/submitqueue/api/base/landstrategy/protopb" pb "github.com/uber/submitqueue/api/submitqueue/gateway/protopb" "github.com/uber/submitqueue/platform/base/change" - "github.com/uber/submitqueue/platform/base/mergestrategy" + "github.com/uber/submitqueue/platform/base/landstrategy" "github.com/uber/submitqueue/submitqueue/entity" ) @@ -41,13 +41,13 @@ func TestProtoToLandRequest(t *testing.T) { req: &pb.LandRequest{ Queue: "test-queue", Change: &changepb.Change{Uris: []string{uri}}, - Strategy: mergestrategypb.Strategy_SQUASH_REBASE, + Strategy: landstrategypb.Strategy_SQUASH_REBASE, }, // ID is not assigned by the mapper — the controller mints it. expected: entity.LandRequest{ Queue: "test-queue", Change: change.Change{URIs: []string{uri}}, - LandStrategy: mergestrategy.MergeStrategySquashRebase, + LandStrategy: landstrategy.StrategySquashRebase, }, }, { @@ -56,7 +56,7 @@ func TestProtoToLandRequest(t *testing.T) { // The mapper does not validate; it leaves URIs empty for the controller to reject. expected: entity.LandRequest{ Queue: "test-queue", - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, }, }, { @@ -64,7 +64,7 @@ func TestProtoToLandRequest(t *testing.T) { req: &pb.LandRequest{ Queue: "test-queue", Change: &changepb.Change{Uris: []string{uri}}, - Strategy: mergestrategypb.Strategy(9999), + Strategy: landstrategypb.Strategy(9999), }, expectedErr: errUnknownStrategy, }, @@ -83,24 +83,24 @@ func TestProtoToLandRequest(t *testing.T) { } } -func TestResolveMergeStrategy(t *testing.T) { +func TestResolveLandStrategy(t *testing.T) { tests := []struct { name string - in mergestrategypb.Strategy - want mergestrategy.MergeStrategy + in landstrategypb.Strategy + want landstrategy.Strategy errMsg string }{ - {name: "default", in: mergestrategypb.Strategy_DEFAULT, want: mergestrategy.MergeStrategyRebase}, - {name: "rebase", in: mergestrategypb.Strategy_REBASE, want: mergestrategy.MergeStrategyRebase}, - {name: "squash_rebase", in: mergestrategypb.Strategy_SQUASH_REBASE, want: mergestrategy.MergeStrategySquashRebase}, - {name: "merge", in: mergestrategypb.Strategy_MERGE, want: mergestrategy.MergeStrategyMerge}, - {name: "promote", in: mergestrategypb.Strategy_PROMOTE, want: mergestrategy.MergeStrategyPromote}, - {name: "unknown", in: mergestrategypb.Strategy(9999), errMsg: "unknown land strategy in proto message"}, + {name: "default", in: landstrategypb.Strategy_DEFAULT, want: landstrategy.StrategyRebase}, + {name: "rebase", in: landstrategypb.Strategy_REBASE, want: landstrategy.StrategyRebase}, + {name: "squash_rebase", in: landstrategypb.Strategy_SQUASH_REBASE, want: landstrategy.StrategySquashRebase}, + {name: "merge", in: landstrategypb.Strategy_MERGE, want: landstrategy.StrategyMerge}, + {name: "promote", in: landstrategypb.Strategy_PROMOTE, want: landstrategy.StrategyPromote}, + {name: "unknown", in: landstrategypb.Strategy(9999), errMsg: "unknown land strategy in proto message"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := resolveMergeStrategy(tt.in) + got, err := resolveLandStrategy(tt.in) if tt.errMsg != "" { assert.ErrorContains(t, err, tt.errMsg) return diff --git a/service/submitqueue/gateway/server/queues.yaml b/service/submitqueue/gateway/server/queues.yaml index 693a3edb0..e1e365bf8 100644 --- a/service/submitqueue/gateway/server/queues.yaml +++ b/service/submitqueue/gateway/server/queues.yaml @@ -21,7 +21,7 @@ queues: # exercise the conflict-analysis error path. See newQueueRegistry in the # orchestrator example server. - name: e2e-conflict-error-queue - # Used by the hermetic git E2E, where Runway is wired to a real git merger + # Used by the hermetic git E2E, where Runway is wired to a real git lander # against a bare repository. See service/submitqueue/demo/provider/git. - name: e2e-git-queue # Used by the provider demo stack (make local-submitqueue-start) in every mode — diff --git a/service/submitqueue/orchestrator/server/config.go b/service/submitqueue/orchestrator/server/config.go index 945374522..2b1c02147 100644 --- a/service/submitqueue/orchestrator/server/config.go +++ b/service/submitqueue/orchestrator/server/config.go @@ -134,7 +134,7 @@ type changeProviderConfig struct { // gitProviderConfig configures the git change provider, which derives change // metadata from a repository rather than asking a service for it. // -// It mirrors Runway's merger block deliberately: each service keeps its own +// It mirrors Runway's lander block deliberately: each service keeps its own // copy of a queue's repository and says for itself where that copy fetches // from, so the two are configured independently even when they name the same // remote. diff --git a/service/submitqueue/orchestrator/server/config_test.go b/service/submitqueue/orchestrator/server/config_test.go index 2de8278c5..aaab51c9a 100644 --- a/service/submitqueue/orchestrator/server/config_test.go +++ b/service/submitqueue/orchestrator/server/config_test.go @@ -301,7 +301,7 @@ func TestNewProfiles_ResolvesPerQueueAnalyzers(t *testing.T) { func TestNewProfiles_FailsWhenAForgeTokenIsMissing(t *testing.T) { // Failing at startup beats accepting land requests and discovering the - // credential gap one API call into the first merge. + // credential gap one API call into the first land. t.Setenv("TEST_GH_TOKEN_UNSET", "") path := writeProfiles(t, ` defaults: diff --git a/stovepipe/README.md b/stovepipe/README.md index 269e8388d..ec1a6c9cd 100644 --- a/stovepipe/README.md +++ b/stovepipe/README.md @@ -1,6 +1,6 @@ # Stovepipe -Stovepipe is a post-merge validation service. Its layout: +Stovepipe is a post-land validation service. Its layout: - `controller/` — business logic (transport-agnostic). Exposes the `Ping` and `Ingest` RPCs, and consumes the internal pipeline stages (`process`, `build`, `buildsignal`, `record`) plus a DLQ reconciler. diff --git a/submitqueue/client/BUILD.bazel b/submitqueue/client/BUILD.bazel index 889715d1d..7612633b4 100644 --- a/submitqueue/client/BUILD.bazel +++ b/submitqueue/client/BUILD.bazel @@ -14,7 +14,7 @@ go_library( visibility = ["//visibility:public"], deps = [ "//api/base/change/protopb:go_default_library", - "//api/base/mergestrategy/protopb:go_default_library", + "//api/base/landstrategy/protopb:go_default_library", "//api/submitqueue/gateway/protopb:go_default_library", "//submitqueue/entity:go_default_library", "@org_golang_google_grpc//:go_default_library", diff --git a/submitqueue/client/land.go b/submitqueue/client/land.go index d401ccce2..aae3a84d4 100644 --- a/submitqueue/client/land.go +++ b/submitqueue/client/land.go @@ -20,7 +20,7 @@ import ( "strings" changepb "github.com/uber/submitqueue/api/base/change/protopb" - mergestrategypb "github.com/uber/submitqueue/api/base/mergestrategy/protopb" + landstrategypb "github.com/uber/submitqueue/api/base/landstrategy/protopb" pb "github.com/uber/submitqueue/api/submitqueue/gateway/protopb" ) @@ -32,7 +32,7 @@ func (c *Client) Land( ctx context.Context, queue string, uris []string, - strategy mergestrategypb.Strategy, + strategy landstrategypb.Strategy, ) (string, error) { if queue == "" { return "", fmt.Errorf("queue must not be empty") @@ -54,19 +54,19 @@ func (c *Client) Land( // ParseStrategy maps a strategy name to its wire value. An empty name selects // the queue's configured default. -func ParseStrategy(name string) (mergestrategypb.Strategy, error) { +func ParseStrategy(name string) (landstrategypb.Strategy, error) { switch strings.ToUpper(strings.TrimSpace(name)) { case "", "DEFAULT": - return mergestrategypb.Strategy_DEFAULT, nil + return landstrategypb.Strategy_DEFAULT, nil case "REBASE": - return mergestrategypb.Strategy_REBASE, nil + return landstrategypb.Strategy_REBASE, nil case "SQUASH_REBASE": - return mergestrategypb.Strategy_SQUASH_REBASE, nil + return landstrategypb.Strategy_SQUASH_REBASE, nil case "MERGE": - return mergestrategypb.Strategy_MERGE, nil + return landstrategypb.Strategy_MERGE, nil case "PROMOTE": - return mergestrategypb.Strategy_PROMOTE, nil + return landstrategypb.Strategy_PROMOTE, nil default: - return mergestrategypb.Strategy_DEFAULT, fmt.Errorf("unknown strategy %q", name) + return landstrategypb.Strategy_DEFAULT, fmt.Errorf("unknown strategy %q", name) } } diff --git a/submitqueue/core/batch/transition_test.go b/submitqueue/core/batch/transition_test.go index b99ff47ce..3bc814d42 100644 --- a/submitqueue/core/batch/transition_test.go +++ b/submitqueue/core/batch/transition_test.go @@ -147,7 +147,7 @@ func TestEnsureRecord(t *testing.T) { batch := entity.Batch{ ID: "monorepo/batch/7", Queue: "monorepo", - State: entity.BatchStateMerging, + State: entity.BatchStateLanding, Version: 5, } storeErr := errors.New("storage failed") diff --git a/submitqueue/core/changeset/README.md b/submitqueue/core/changeset/README.md index 4e8c9463c..ddee68ded 100644 --- a/submitqueue/core/changeset/README.md +++ b/submitqueue/core/changeset/README.md @@ -1,6 +1,6 @@ # changeset -`changeset` resolves batch identity into the changes a batch contains. Current consumers include build-runner and scorer implementations and the path-overlap conflict analyzer. The merge controller loads member requests directly because its Runway payload preserves one ordered merge step per request. +`changeset` resolves batch identity into the changes a batch contains. Current consumers include build-runner and scorer implementations and the path-overlap conflict analyzer. The land controller loads member requests directly because its Runway payload preserves one ordered land step per request. ## Why it exists diff --git a/submitqueue/core/changeset/changeset.go b/submitqueue/core/changeset/changeset.go index 3edf21a8f..e83c75584 100644 --- a/submitqueue/core/changeset/changeset.go +++ b/submitqueue/core/changeset/changeset.go @@ -14,7 +14,7 @@ // Package changeset resolves batch identity into the changes a batch contains. // It is the single place the orchestrator walks batch -> requests -> changes, -// consolidating what the build and merge controllers each did privately. +// consolidating what the build and land controllers each did privately. // Decision/action extensions (scorer, buildrunner, and future // detail-aware conflict analyzers) take thin identity entities and resolve their // granular content through an injected Resolver instead of being handed @@ -32,14 +32,14 @@ import ( // Resolver turns batch identity into the changes the batch contains. Both methods // operate on a single batch — callers with several batches (a build's base, a -// merge train) loop and keep the per-batch boundary by holding a slice per batch. +// multi-batch land) loop and keep the per-batch boundary by holding a slice per batch. // The two methods differ only in fidelity: ChangesForBatch is the cheap URI-only // view; DetailedForBatch reads the change store for provider details. type Resolver interface { // ChangesForBatch resolves a batch's contained requests into their raw // changes (URIs only; no change-store read), in batch.Contains order. A batch // with no requests yields an empty slice. Used by the build (base/head) and - // merge stages. + // land stages. ChangesForBatch(ctx context.Context, batch entity.Batch) ([]change.Change, error) // DetailedForBatch resolves a batch into its normalized, batch-level view: diff --git a/submitqueue/core/request/request_test.go b/submitqueue/core/request/request_test.go index fe081ce4a..fd14736ca 100644 --- a/submitqueue/core/request/request_test.go +++ b/submitqueue/core/request/request_test.go @@ -54,9 +54,9 @@ func TestGetCurrentStateFromRequestLog(t *testing.T) { name: "terminal error status with last error", logs: []entity.RequestLog{ {RequestID: "q/1", TimestampMs: 1000, Type: entity.RequestLogTypeStatus, Status: entity.RequestStatusStarted, RequestVersion: 1, LastError: "", Metadata: map[string]string{}}, - {RequestID: "q/1", TimestampMs: 2000, Type: entity.RequestLogTypeStatus, Status: entity.RequestStatusError, RequestVersion: 4, LastError: "merge conflict", Metadata: map[string]string{"step": "merge"}}, + {RequestID: "q/1", TimestampMs: 2000, Type: entity.RequestLogTypeStatus, Status: entity.RequestStatusError, RequestVersion: 4, LastError: "merge conflict", Metadata: map[string]string{"step": "land"}}, }, - expected: CurrentState{Status: entity.RequestStatusError, LastError: "merge conflict", Metadata: map[string]string{"step": "merge"}}, + expected: CurrentState{Status: entity.RequestStatusError, LastError: "merge conflict", Metadata: map[string]string{"step": "land"}}, }, { name: "multiple terminal records picks highest version", diff --git a/submitqueue/core/topickey/topickey.go b/submitqueue/core/topickey/topickey.go index fa31076fa..6bdae00f3 100644 --- a/submitqueue/core/topickey/topickey.go +++ b/submitqueue/core/topickey/topickey.go @@ -44,16 +44,16 @@ const ( // so the state machine re-evaluates, and holds the delivery for the next // poll when the build has not yet reached a terminal state. TopicKeyBuildSignal TopicKey = "buildsignal" - // TopicKeyMerge is the pipeline stage where speculated batches are published for merging. - TopicKeyMerge TopicKey = "submitqueue-merge" - // TopicKeyConclude is the pipeline stage where merged requests are published for conclusion. + // TopicKeyLand is the pipeline stage where speculated batches are published for landing. + TopicKeyLand TopicKey = "submitqueue-land" + // TopicKeyConclude is the pipeline stage where landed requests are published for conclusion. TopicKeyConclude TopicKey = "conclude" // TopicKeyLog is the pipeline stage where per-request logs are written. TopicKeyLog TopicKey = "log" ) // MetadataKeyFailureReason is the conclude message's metadata attribute carrying -// a failed batch's human-readable reason. Set by the failure sites (merge and +// a failed batch's human-readable reason. Set by the failure sites (land and // speculate) on the conclude publish and read by conclude to stamp the request's // terminal log; absent on the landed and cancelled paths. const MetadataKeyFailureReason = "failure_reason" diff --git a/submitqueue/entity/BUILD.bazel b/submitqueue/entity/BUILD.bazel index a2dc0a684..74eae9f94 100644 --- a/submitqueue/entity/BUILD.bazel +++ b/submitqueue/entity/BUILD.bazel @@ -12,8 +12,8 @@ go_library( "change_record.go", "conflict.go", "land.go", + "land_check_result.go", "list.go", - "merge_result.go", "path_build.go", "push_result.go", "queue_batch_state.go", @@ -31,7 +31,7 @@ go_library( deps = [ "//platform/base/change:go_default_library", "//platform/base/failure:go_default_library", - "//platform/base/mergestrategy:go_default_library", + "//platform/base/landstrategy:go_default_library", ], ) @@ -49,7 +49,7 @@ go_test( embed = [":go_default_library"], deps = [ "//platform/base/change:go_default_library", - "//platform/base/mergestrategy:go_default_library", + "//platform/base/landstrategy:go_default_library", "@com_github_stretchr_testify//assert:go_default_library", "@com_github_stretchr_testify//require:go_default_library", ], diff --git a/submitqueue/entity/batch.go b/submitqueue/entity/batch.go index f9b280cb8..25f9d4af8 100644 --- a/submitqueue/entity/batch.go +++ b/submitqueue/entity/batch.go @@ -31,15 +31,15 @@ const ( BatchStateCreated BatchState = "created" // BatchStateSpeculating is the state of a batch that is undergoing speculative execution. BatchStateSpeculating BatchState = "speculating" - // BatchStateMerging is the state of a batch that is being merged after speculative execution. - BatchStateMerging BatchState = "merging" + // BatchStateLanding is the state of a batch that is being landed after speculative execution. + BatchStateLanding BatchState = "landing" // BatchStateSucceeded is the terminal state of a batch that has been successfully landed. BatchStateSucceeded BatchState = "succeeded" // BatchStateFailed is the terminal state of a batch that has failed. BatchStateFailed BatchState = "failed" // BatchStateCancelling is the non-terminal intent state set when a cancel has been requested but the // batch has not yet been transitioned to BatchStateCancelled. A batch in this state may still reach - // BatchStateSucceeded or BatchStateFailed if a concurrent merge wins the race (e.g. the push had + // BatchStateSucceeded or BatchStateFailed if a concurrent land wins the race (e.g. the push had // already completed before the cancel CAS observed the batch); those terminal states prevail. // Forward-progress controllers must treat this state as halted (no new work). The speculate // controller owns the transition to the terminal BatchStateCancelled and the downstream fan-out @@ -67,7 +67,7 @@ func (s BatchState) IsTerminal() bool { var nonCancellableBatchStates = map[BatchState]bool{ BatchStateUnknown: true, BatchStateCreating: true, - BatchStateMerging: true, + BatchStateLanding: true, BatchStateSucceeded: true, BatchStateFailed: true, BatchStateCancelled: true, @@ -80,7 +80,7 @@ func (s BatchState) IsCancellable() bool { } // IsBatchStateHalted returns true if the batch is either terminal or in the process of being cancelled. -// Forward-progress controllers (build, buildsignal, speculate, merge) use this to short-circuit +// Forward-progress controllers (build, buildsignal, speculate, land) use this to short-circuit // work for batches that the user has asked to cancel — even though Cancelling is non-terminal, no // further pipeline work should start (cancel will write the terminal state and fan out). func IsBatchStateHalted(s BatchState) bool { @@ -94,7 +94,7 @@ func AllBatchStates() []BatchState { BatchStateCreating, BatchStateCreated, BatchStateSpeculating, - BatchStateMerging, + BatchStateLanding, BatchStateSucceeded, BatchStateFailed, BatchStateCancelling, @@ -108,7 +108,7 @@ func ActiveBatchStates() []BatchState { return []BatchState{ BatchStateCreated, BatchStateSpeculating, - BatchStateMerging, + BatchStateLanding, BatchStateCancelling, } } @@ -127,11 +127,11 @@ func DependencyBatchStates() []BatchState { return []BatchState{ BatchStateCreated, BatchStateSpeculating, - BatchStateMerging, + BatchStateLanding, } } -// Batch represents a group of requests to land (merge into target branch of the source control repository). +// Batch represents a group of requests to land on the source control repository's target branch. type Batch struct { // ID is the globally unique identifier for the batch. Format: "/batch/". ID string diff --git a/submitqueue/entity/batch_test.go b/submitqueue/entity/batch_test.go index 558d95141..0d1496d14 100644 --- a/submitqueue/entity/batch_test.go +++ b/submitqueue/entity/batch_test.go @@ -31,7 +31,7 @@ func TestBatchState_IsTerminal(t *testing.T) { {name: "creating", state: BatchStateCreating, terminal: false}, {name: "created", state: BatchStateCreated, terminal: false}, {name: "speculating", state: BatchStateSpeculating, terminal: false}, - {name: "merging", state: BatchStateMerging, terminal: false}, + {name: "landing", state: BatchStateLanding, terminal: false}, {name: "succeeded", state: BatchStateSucceeded, terminal: true}, {name: "failed", state: BatchStateFailed, terminal: true}, {name: "cancelled", state: BatchStateCancelled, terminal: true}, @@ -52,7 +52,7 @@ func TestIsCancellable(t *testing.T) { assert.True(t, BatchState("future").IsCancellable()) assert.False(t, BatchStateUnknown.IsCancellable()) assert.False(t, BatchStateCreating.IsCancellable()) - assert.False(t, BatchStateMerging.IsCancellable()) + assert.False(t, BatchStateLanding.IsCancellable()) assert.False(t, BatchStateSucceeded.IsCancellable()) assert.False(t, BatchStateFailed.IsCancellable()) assert.False(t, BatchStateCancelled.IsCancellable()) diff --git a/submitqueue/entity/land.go b/submitqueue/entity/land.go index edeaa60af..3e19aad57 100644 --- a/submitqueue/entity/land.go +++ b/submitqueue/entity/land.go @@ -18,7 +18,7 @@ import ( "encoding/json" "github.com/uber/submitqueue/platform/base/change" - "github.com/uber/submitqueue/platform/base/mergestrategy" + "github.com/uber/submitqueue/platform/base/landstrategy" ) // LandRequest represents the gateway-owned fields of a land request sent over the queue @@ -33,7 +33,7 @@ type LandRequest struct { Change change.Change `json:"change"` // LandStrategy is the source control integration strategy to use for this // land operation. It applies to every URI of Change, the same way to each. - LandStrategy mergestrategy.MergeStrategy `json:"land_strategy"` + LandStrategy landstrategy.Strategy `json:"land_strategy"` } // ToBytes serializes the LandRequest to JSON bytes for queue message payload. diff --git a/submitqueue/entity/merge_result.go b/submitqueue/entity/land_check_result.go similarity index 69% rename from submitqueue/entity/merge_result.go rename to submitqueue/entity/land_check_result.go index ad5078201..1c3e5b7ae 100644 --- a/submitqueue/entity/merge_result.go +++ b/submitqueue/entity/land_check_result.go @@ -14,11 +14,11 @@ 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. +// LandCheckResult holds the outcome of a landability check. +type LandCheckResult struct { + // Landable is true if the request's changes are expected to land cleanly. + Landable bool + // Reason is a human-readable explanation when Landable is false. + // Empty when Landable is true. Reason string } diff --git a/submitqueue/entity/land_test.go b/submitqueue/entity/land_test.go index 8820aea66..e602d6eab 100644 --- a/submitqueue/entity/land_test.go +++ b/submitqueue/entity/land_test.go @@ -20,7 +20,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/uber/submitqueue/platform/base/change" - "github.com/uber/submitqueue/platform/base/mergestrategy" + "github.com/uber/submitqueue/platform/base/landstrategy" ) func TestLandRequest_ToBytes(t *testing.T) { @@ -31,7 +31,7 @@ func TestLandRequest_ToBytes(t *testing.T) { "github://github.example.com/uber/submitqueue/pull/456/abcdef0123456789abcdef0123456789abcdef01", "github://github.example.com/uber/submitqueue/pull/789/0123456789abcdef0123456789abcdef01234567", }}, - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, } data, err := req.ToBytes() @@ -50,7 +50,7 @@ func TestLandRequestFromBytes(t *testing.T) { ID: "my-queue/999", Queue: "my-queue", Change: change.Change{URIs: []string{"code.uber.internal.com/D111"}}, - LandStrategy: mergestrategy.MergeStrategyMerge, + LandStrategy: landstrategy.StrategyMerge, } // Serialize @@ -84,7 +84,7 @@ func TestLandRequestFromBytes_EmptyData(t *testing.T) { // Empty JSON should deserialize with zero values assert.Empty(t, req.ID) assert.Empty(t, req.Queue) - assert.Equal(t, mergestrategy.MergeStrategyUnknown, req.LandStrategy) + assert.Equal(t, landstrategy.StrategyUnknown, req.LandStrategy) } func TestLandRequest_SerializationRoundTrip(t *testing.T) { @@ -102,7 +102,7 @@ func TestLandRequest_SerializationRoundTrip(t *testing.T) { "github://github.example.com/uber/repo-a/pull/102/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "github://github.example.com/uber/repo-a/pull/103/cccccccccccccccccccccccccccccccccccccccc", }}, - LandStrategy: mergestrategy.MergeStrategySquashRebase, + LandStrategy: landstrategy.StrategySquashRebase, }, }, { @@ -111,7 +111,7 @@ func TestLandRequest_SerializationRoundTrip(t *testing.T) { ID: "queue2/200", Queue: "queue2", Change: change.Change{URIs: []string{"code.uber.internal.com/D12345"}}, - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, }, }, { @@ -120,7 +120,7 @@ func TestLandRequest_SerializationRoundTrip(t *testing.T) { ID: "queue3/300", Queue: "queue3", Change: change.Change{URIs: []string{"github.uber.com/internal/service/999/deadbeef12"}}, - LandStrategy: mergestrategy.MergeStrategyMerge, + LandStrategy: landstrategy.StrategyMerge, }, }, } diff --git a/submitqueue/entity/push_result.go b/submitqueue/entity/push_result.go index 0b1352c0e..0a6579aab 100644 --- a/submitqueue/entity/push_result.go +++ b/submitqueue/entity/push_result.go @@ -48,7 +48,7 @@ type ChangeOutcome struct { } // 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 +// multi-batch land 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. diff --git a/submitqueue/entity/request.go b/submitqueue/entity/request.go index 83bd30a5d..3ed7e65f2 100644 --- a/submitqueue/entity/request.go +++ b/submitqueue/entity/request.go @@ -18,7 +18,7 @@ import ( "encoding/json" "github.com/uber/submitqueue/platform/base/change" - "github.com/uber/submitqueue/platform/base/mergestrategy" + "github.com/uber/submitqueue/platform/base/landstrategy" ) // RequestState defines the possible states of a land request. They are internal and used to implement a state machine. A separate RequestStatus type is used to track the customer-friendly status of a request. @@ -29,7 +29,7 @@ const ( RequestStateUnknown RequestState = "" // RequestStateStarted is the initial state of a land request. It is confirmed by the system but the processing is not started yet. RequestStateStarted RequestState = "started" - // RequestStateValidated indicates that the request has been validated (duplicate check, merge check etc.) successfully. + // RequestStateValidated indicates that the request has been validated (duplicate check, landability check, etc.) successfully. RequestStateValidated RequestState = "validated" // RequestStateBatched indicates that the request is enrolled in a batch whose dependencies have been // resolved. The CAS-write of this state is the serialization point against cancellation: it lands with @@ -45,7 +45,7 @@ const ( RequestStateError RequestState = "error" // RequestStateCancelling is the non-terminal intent state set when the user has requested cancellation but the // request has not yet been transitioned to RequestStateCancelled. A request in this state may still reach - // RequestStateLanded or RequestStateError if a concurrent merge or failure wins the race; those terminal + // RequestStateLanded or RequestStateError if a concurrent land or failure wins the race; those terminal // states prevail. Forward-progress controllers must treat this state the same as terminal (i.e. do not start // any new work on the request). RequestStateCancelling RequestState = "cancelling" @@ -68,7 +68,7 @@ func IsRequestStateHalted(s RequestState) bool { return IsRequestStateTerminal(s) || s == RequestStateCancelling } -// Request defines a request to land (merge into target branch of the source control repository) a set of code changes. +// Request defines a request to land a set of code changes on the source control repository's target branch. // The object is immutable after creation. type Request struct { // **************** @@ -83,7 +83,7 @@ type Request struct { Change change.Change `json:"change"` // LandStrategy is the source control integration strategy to use for this // land operation. It applies to every URI of Change, the same way to each. - LandStrategy mergestrategy.MergeStrategy `json:"land_strategy"` + LandStrategy landstrategy.Strategy `json:"land_strategy"` // **************** // Following fields could be changed throughout the lifecycle of the request diff --git a/submitqueue/entity/request_log.go b/submitqueue/entity/request_log.go index 72bad9ef6..a1be20fc3 100644 --- a/submitqueue/entity/request_log.go +++ b/submitqueue/entity/request_log.go @@ -45,10 +45,10 @@ const ( // RequestStatusStarted is the initial status of a request. It corresponds to the RequestStateStarted state and typically set by the orchestrator service when the request is received and persisted to the operating database. RequestStatusStarted RequestStatus = "started" - // RequestStatusValidating indicates that the request is currently being validated (e.g., duplicate check, merge check, etc.). + // RequestStatusValidating indicates that the request is currently being validated (e.g., duplicate check, landability check, etc.). RequestStatusValidating RequestStatus = "validating" - // RequestStatusValidated indicates that the request has been validated (duplicate check, merge check etc.) successfully. It corresponds to the RequestStateValidated state. + // RequestStatusValidated indicates that the request has been validated (duplicate check, landability check, etc.) successfully. It corresponds to the RequestStateValidated state. RequestStatusValidated RequestStatus = "validated" // RequestStatusBatching indicates that a batch has been created for the request and is resolving what it must serialize behind. @@ -75,7 +75,7 @@ const ( RequestStatusError RequestStatus = "error" // RequestStatusCancelling indicates that the user has requested cancellation but the request has not yet transitioned - // to the RequestStateCancelled state. Cancellation is best-effort: a request that has already been merged or that + // to the RequestStateCancelled state. Cancellation is best-effort: a request that has already landed or that // races to completion before the cancel propagates through the pipeline may still land. Observers should treat this // as intent only and rely on RequestStatusCancelled (or RequestStatusLanded) for the terminal outcome. Emitted by // the gateway when the Cancel RPC is received. diff --git a/submitqueue/entity/request_test.go b/submitqueue/entity/request_test.go index 4415fb204..75fb93a5c 100644 --- a/submitqueue/entity/request_test.go +++ b/submitqueue/entity/request_test.go @@ -20,7 +20,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/uber/submitqueue/platform/base/change" - "github.com/uber/submitqueue/platform/base/mergestrategy" + "github.com/uber/submitqueue/platform/base/landstrategy" ) func TestRequest_ToBytes(t *testing.T) { @@ -31,7 +31,7 @@ func TestRequest_ToBytes(t *testing.T) { "github://github.example.com/uber/submitqueue/pull/456/abcdef0123456789abcdef0123456789abcdef01", "github://github.example.com/uber/submitqueue/pull/789/0123456789abcdef0123456789abcdef01234567", }}, - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, State: RequestStateStarted, Version: 1, } @@ -53,7 +53,7 @@ func TestRequestFromBytes(t *testing.T) { ID: "my-queue/999", Queue: "my-queue", Change: change.Change{URIs: []string{"code.uber.internal.com/D111"}}, - LandStrategy: mergestrategy.MergeStrategyMerge, + LandStrategy: landstrategy.StrategyMerge, State: RequestStateProcessing, Version: 3, } @@ -92,7 +92,7 @@ func TestRequestFromBytes_EmptyData(t *testing.T) { assert.Empty(t, req.ID) assert.Empty(t, req.Queue) assert.Equal(t, RequestStateUnknown, req.State) - assert.Equal(t, mergestrategy.MergeStrategyUnknown, req.LandStrategy) + assert.Equal(t, landstrategy.StrategyUnknown, req.LandStrategy) assert.Equal(t, int32(0), req.Version) } @@ -153,7 +153,7 @@ func TestRequest_SerializationRoundTrip(t *testing.T) { "github://github.example.com/uber/repo-a/pull/102/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "github://github.example.com/uber/repo-a/pull/103/cccccccccccccccccccccccccccccccccccccccc", }}, - LandStrategy: mergestrategy.MergeStrategySquashRebase, + LandStrategy: landstrategy.StrategySquashRebase, State: RequestStateLanded, Version: 5, }, @@ -164,7 +164,7 @@ func TestRequest_SerializationRoundTrip(t *testing.T) { ID: "queue2/200", Queue: "queue2", Change: change.Change{URIs: []string{"code.uber.internal.com/D12345"}}, - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, State: RequestStateStarted, Version: 1, }, @@ -175,7 +175,7 @@ func TestRequest_SerializationRoundTrip(t *testing.T) { ID: "queue3/300", Queue: "queue3", Change: change.Change{URIs: []string{"github.uber.com/internal/service/999/deadbeef12"}}, - LandStrategy: mergestrategy.MergeStrategyMerge, + LandStrategy: landstrategy.StrategyMerge, State: RequestStateError, Version: 10, }, diff --git a/submitqueue/entity/speculation.go b/submitqueue/entity/speculation.go index 1d199c902..734dfe1f6 100644 --- a/submitqueue/entity/speculation.go +++ b/submitqueue/entity/speculation.go @@ -185,7 +185,7 @@ type SpeculationPathSet struct { } // PathAction is an action proposed on a speculation path. The set is limited to -// build and cancel; there is no merge or fail action, because a batch's verdict +// build and cancel; there is no land or fail action, because a batch's verdict // is a controller-owned fact, not a proposed action. type PathAction string diff --git a/submitqueue/extension/changeprovider/git/README.md b/submitqueue/extension/changeprovider/git/README.md index 7f9cbec15..af330719d 100644 --- a/submitqueue/extension/changeprovider/git/README.md +++ b/submitqueue/extension/changeprovider/git/README.md @@ -14,7 +14,7 @@ A change that shares no history with what it claims to land on is an error, not ## What this package is, and isn't -This package is only the derivation: parse the URI, pick the baseline, read the diff and author, shape the result. Everything about *reaching* the repository — the local bare copy, fetching, merge bases, authentication, the git command environment — is transport plumbing that lives in [`platform/git/repo`](../../../../platform/git/repo) (built on [`platform/git/exec`](../../../../platform/git/exec)), which the merger and any other git caller share. The provider depends on a small `Repository` interface it defines, so it holds no `os/exec` and no credential handling of its own. +This package is only the derivation: parse the URI, pick the baseline, read the diff and author, shape the result. Everything about *reaching* the repository — the local bare copy, fetching, merge bases, authentication, the git command environment — is transport plumbing that lives in [`platform/git/repo`](../../../../platform/git/repo) (built on [`platform/git/exec`](../../../../platform/git/exec)), which the lander and any other git caller share. The provider depends on a small `Repository` interface it defines, so it holds no `os/exec` and no credential handling of its own. ## Tests diff --git a/submitqueue/extension/mergechecker/BUILD.bazel b/submitqueue/extension/landchecker/BUILD.bazel similarity index 83% rename from submitqueue/extension/mergechecker/BUILD.bazel rename to submitqueue/extension/landchecker/BUILD.bazel index 23ecace72..74705137c 100644 --- a/submitqueue/extension/mergechecker/BUILD.bazel +++ b/submitqueue/extension/landchecker/BUILD.bazel @@ -2,8 +2,8 @@ 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", + srcs = ["landchecker.go"], + importpath = "github.com/uber/submitqueue/submitqueue/extension/landchecker", visibility = ["//visibility:public"], deps = ["//submitqueue/entity:go_default_library"], ) diff --git a/submitqueue/extension/mergechecker/fake/BUILD.bazel b/submitqueue/extension/landchecker/fake/BUILD.bazel similarity index 82% rename from submitqueue/extension/mergechecker/fake/BUILD.bazel rename to submitqueue/extension/landchecker/fake/BUILD.bazel index d1c85e0ce..24a251648 100644 --- a/submitqueue/extension/mergechecker/fake/BUILD.bazel +++ b/submitqueue/extension/landchecker/fake/BUILD.bazel @@ -3,12 +3,12 @@ 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", + importpath = "github.com/uber/submitqueue/submitqueue/extension/landchecker/fake", visibility = ["//visibility:public"], deps = [ "//platform/fakemarker:go_default_library", "//submitqueue/entity:go_default_library", - "//submitqueue/extension/mergechecker:go_default_library", + "//submitqueue/extension/landchecker:go_default_library", ], ) @@ -19,7 +19,7 @@ go_test( deps = [ "//platform/base/change:go_default_library", "//submitqueue/entity:go_default_library", - "//submitqueue/extension/mergechecker:go_default_library", + "//submitqueue/extension/landchecker: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/landchecker/fake/fake.go similarity index 59% rename from submitqueue/extension/mergechecker/fake/fake.go rename to submitqueue/extension/landchecker/fake/fake.go index effffc5dd..621cf7a1b 100644 --- a/submitqueue/extension/mergechecker/fake/fake.go +++ b/submitqueue/extension/landchecker/fake/fake.go @@ -12,14 +12,14 @@ // 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, +// Package fake provides a landchecker.LandChecker whose outcome is driven by +// the input change. With no marker it reports every change as landable, // 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 +// sq-fake=unlandable -> Result{Landable: false} +// sq-fake=landcheck-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 @@ -32,37 +32,37 @@ import ( "github.com/uber/submitqueue/platform/fakemarker" "github.com/uber/submitqueue/submitqueue/entity" - "github.com/uber/submitqueue/submitqueue/extension/mergechecker" + "github.com/uber/submitqueue/submitqueue/extension/landchecker" ) // Recognized marker tokens. See the package doc for the convention. const ( - tokenUnmergeable = "unmergeable" - tokenError = "mergecheck-error" + tokenUnlandable = "unlandable" + tokenError = "landcheck-error" ) -// checker is a mergechecker.MergeChecker that reports changes as mergeable +// checker is a landchecker.LandChecker that reports changes as landable // 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 + cfg landchecker.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 { +// New returns a landchecker.LandChecker bound to the queue named in cfg that +// defaults to landable and honors marker tokens embedded in change URIs. +func New(cfg landchecker.Config) landchecker.LandChecker { return checker{cfg: cfg} } -// Check reports the change as mergeable unless a recognized marker token is +// Check reports the change as landable 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) { +func (checker) Check(_ context.Context, request entity.Request) (entity.LandCheckResult, error) { switch fakemarker.Token(request.Change.URIs) { - case tokenUnmergeable: - return entity.MergeResult{Mergeable: false, Reason: "fake: marked unmergeable"}, nil + case tokenUnlandable: + return entity.LandCheckResult{Landable: false, Reason: "fake: marked unlandable"}, nil case tokenError: - return entity.MergeResult{}, fmt.Errorf("fake: marked merge-check error") + return entity.LandCheckResult{}, fmt.Errorf("fake: marked land-check error") default: - return entity.MergeResult{Mergeable: true}, nil + return entity.LandCheckResult{Landable: true}, nil } } diff --git a/submitqueue/extension/mergechecker/fake/fake_test.go b/submitqueue/extension/landchecker/fake/fake_test.go similarity index 72% rename from submitqueue/extension/mergechecker/fake/fake_test.go rename to submitqueue/extension/landchecker/fake/fake_test.go index 5fb833d41..bf52187a3 100644 --- a/submitqueue/extension/mergechecker/fake/fake_test.go +++ b/submitqueue/extension/landchecker/fake/fake_test.go @@ -22,47 +22,47 @@ import ( "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" + "github.com/uber/submitqueue/submitqueue/extension/landchecker" ) // testCfg is the per-queue identity used by every case in this file. -var testCfg = mergechecker.Config{QueueName: "test-queue"} +var testCfg = landchecker.Config{QueueName: "test-queue"} func TestNew_ImplementsInterface(t *testing.T) { - var _ mergechecker.MergeChecker = New(testCfg) + var _ landchecker.LandChecker = New(testCfg) } func TestChecker_Check(t *testing.T) { tests := []struct { - name string - uris []string - wantMergeable bool - wantErr bool + name string + uris []string + wantLandable bool + wantErr bool }{ { - name: "no marker is mergeable", - uris: []string{"github://github.example.com/owner/repo/pull/1/abc"}, - wantMergeable: true, + name: "no marker is landable", + uris: []string{"github://github.example.com/owner/repo/pull/1/abc"}, + wantLandable: true, }, { - name: "no URIs is mergeable", - uris: nil, - wantMergeable: true, + name: "no URIs is landable", + uris: nil, + wantLandable: true, }, { - name: "unmergeable marker", - uris: []string{"github://github.example.com/owner/repo/pull/1/abc?sq-fake=unmergeable"}, + name: "unlandable marker", + uris: []string{"github://github.example.com/owner/repo/pull/1/abc?sq-fake=unlandable"}, }, { name: "error marker", - uris: []string{"github://github.example.com/owner/repo/pull/1/abc?sq-fake=mergecheck-error"}, + uris: []string{"github://github.example.com/owner/repo/pull/1/abc?sq-fake=landcheck-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", + "github://github.example.com/owner/repo/pull/2/def?sq-fake=unlandable", }, }, } @@ -76,7 +76,7 @@ func TestChecker_Check(t *testing.T) { return } require.NoError(t, err) - assert.Equal(t, tt.wantMergeable, res.Mergeable) + assert.Equal(t, tt.wantLandable, res.Landable) }) } } diff --git a/submitqueue/extension/mergechecker/github/BUILD.bazel b/submitqueue/extension/landchecker/github/BUILD.bazel similarity index 88% rename from submitqueue/extension/mergechecker/github/BUILD.bazel rename to submitqueue/extension/landchecker/github/BUILD.bazel index faf87e605..3e790f3b8 100644 --- a/submitqueue/extension/mergechecker/github/BUILD.bazel +++ b/submitqueue/extension/landchecker/github/BUILD.bazel @@ -7,13 +7,13 @@ go_library( "graphql.go", "validate.go", ], - importpath = "github.com/uber/submitqueue/submitqueue/extension/mergechecker/github", + importpath = "github.com/uber/submitqueue/submitqueue/extension/landchecker/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", + "//submitqueue/extension/landchecker:go_default_library", "@com_github_uber_go_tally//:go_default_library", "@org_uber_go_zap//:go_default_library", ], @@ -32,7 +32,7 @@ go_test( "//platform/base/change/github:go_default_library", "//platform/http:go_default_library", "//submitqueue/entity:go_default_library", - "//submitqueue/extension/mergechecker:go_default_library", + "//submitqueue/extension/landchecker: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", diff --git a/submitqueue/extension/mergechecker/github/checker.go b/submitqueue/extension/landchecker/github/checker.go similarity index 76% rename from submitqueue/extension/mergechecker/github/checker.go rename to submitqueue/extension/landchecker/github/checker.go index 7d01c1063..d9d74f739 100644 --- a/submitqueue/extension/mergechecker/github/checker.go +++ b/submitqueue/extension/landchecker/github/checker.go @@ -26,15 +26,15 @@ import ( 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" + "github.com/uber/submitqueue/submitqueue/extension/landchecker" "go.uber.org/zap" ) -// Params holds the dependencies for the GitHub MergeChecker. +// Params holds the dependencies for the GitHub LandChecker. type Params struct { // Config is the per-queue identity handed to the Factory that built this - // merge checker. - Config mergechecker.Config + // land checker. + Config landchecker.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 @@ -44,31 +44,31 @@ type Params struct { MetricsScope tally.Scope } -// mergeChecker implements the mergechecker.MergeChecker interface using the GitHub GraphQL API. -type mergeChecker struct { +// landChecker implements the landchecker.LandChecker interface using the GitHub GraphQL API. +type landChecker struct { // cfg is the per-queue identity this checker was built for. - cfg mergechecker.Config + cfg landchecker.Config httpClient *http.Client logger *zap.SugaredLogger metricsScope tally.Scope } -// Verify mergeChecker implements mergechecker.MergeChecker at compile time. -var _ mergechecker.MergeChecker = (*mergeChecker)(nil) +// Verify landChecker implements landchecker.LandChecker at compile time. +var _ landchecker.LandChecker = (*landChecker)(nil) -// NewMergeChecker creates a new GitHub MergeChecker bound to the queue named in +// NewLandChecker creates a new GitHub LandChecker bound to the queue named in // params.Config. -func NewMergeChecker(params Params) mergechecker.MergeChecker { - return &mergeChecker{ +func NewLandChecker(params Params) landchecker.LandChecker { + return &landChecker{ cfg: params.Config, httpClient: params.HTTPClient, - logger: params.Logger.Named("github_mergechecker"), - metricsScope: params.MetricsScope.SubScope("github_mergechecker"), + logger: params.Logger.Named("github_landchecker"), + metricsScope: params.MetricsScope.SubScope("github_landchecker"), } } // 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) { +func (c *landChecker) Check(ctx context.Context, request entity.Request) (result entity.LandCheckResult, retErr error) { const opName = "check" op := metrics.Begin(c.metricsScope, opName, metrics.LongLatencyBuckets) @@ -95,7 +95,7 @@ func (c *mergeChecker) Check(ctx context.Context, request entity.Request) (resul return result, fmt.Errorf("failed to fetch PR info: %w", err) } - // Validate PR mergeability + // Validate PR landability mergeable, reason, err := validatePRs(changeIDs, prInfoMap) if err != nil { metrics.NamedCounter(c.metricsScope, opName, "validation_errors", 1) @@ -103,22 +103,22 @@ func (c *mergeChecker) Check(ctx context.Context, request entity.Request) (resul } if !mergeable { - metrics.NamedCounter(c.metricsScope, opName, "not_mergeable", 1) + metrics.NamedCounter(c.metricsScope, opName, "not_landable", 1) c.logger.Infow("change not mergeable", "reason", reason, "change_uris", change.URIs, ) } else { - metrics.NamedCounter(c.metricsScope, opName, "mergeable", 1) + metrics.NamedCounter(c.metricsScope, opName, "landable", 1) } - result.Mergeable = mergeable + result.Landable = 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) { +func (c *landChecker) fetchPRInfo(ctx context.Context, changeIDs []entitygithub.ChangeID) (map[int]PRInfo, error) { query := buildGraphQLQuery(changeIDs) reqBody, err := json.Marshal(graphQLRequest{Query: query}) diff --git a/submitqueue/extension/mergechecker/github/checker_test.go b/submitqueue/extension/landchecker/github/checker_test.go similarity index 78% rename from submitqueue/extension/mergechecker/github/checker_test.go rename to submitqueue/extension/landchecker/github/checker_test.go index e38f1b389..5e0866958 100644 --- a/submitqueue/extension/mergechecker/github/checker_test.go +++ b/submitqueue/extension/landchecker/github/checker_test.go @@ -28,15 +28,15 @@ import ( "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" + "github.com/uber/submitqueue/submitqueue/extension/landchecker" "go.uber.org/zap/zaptest" ) -func newTestMergeChecker(t *testing.T, serverURL string) mergechecker.MergeChecker { +func newTestLandChecker(t *testing.T, serverURL string) landchecker.LandChecker { t.Helper() client, err := phttp.NewClient(serverURL) require.NoError(t, err) - return NewMergeChecker(Params{ + return NewLandChecker(Params{ HTTPClient: client, Logger: zaptest.NewLogger(t).Sugar(), MetricsScope: tally.NoopScope, @@ -80,31 +80,31 @@ func graphQLHandler(t *testing.T, prInfos []PRInfo) http.HandlerFunc { } } -func TestMergeChecker_Check(t *testing.T) { +func TestLandChecker_Check(t *testing.T) { tests := []struct { - name string - handler http.HandlerFunc - change change.Change - wantMergeable bool - wantReason string - wantErr bool + name string + handler http.HandlerFunc + change change.Change + wantLandable 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, + change: change.Change{URIs: []string{"github://github.example.com/uber/repo/pull/1/" + shaAFull}}, + wantLandable: 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", + change: change.Change{URIs: []string{"github://github.example.com/uber/repo/pull/1/" + shaAFull}}, + wantLandable: false, + wantReason: "PR #1 has merge conflicts", }, { name: "stack of two PRs mergeable", @@ -112,11 +112,11 @@ func TestMergeChecker_Check(t *testing.T) { {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, + change: change.Change{URIs: []string{"github://github.example.com/uber/repo/pull/1/" + sha1Full, "github://github.example.com/uber/repo/pull/2/" + sha2Full}}, + wantLandable: true, }, { - name: "unknown mergeability returns error", + name: "unknown landability returns error", handler: graphQLHandler(t, []PRInfo{ {Number: 1, Mergeable: PRMergeableStateUnknown, BaseRefName: "main", HeadRefName: "feature-1", HeadRefOid: shaAFull, State: PRStateOpen}, }), @@ -128,9 +128,9 @@ func TestMergeChecker_Check(t *testing.T) { 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), + change: change.Change{URIs: []string{"github://github.example.com/uber/repo/pull/1/" + shaOldFull}}, + wantLandable: false, + wantReason: fmt.Sprintf("PR #1 head SHA changed: expected %s, got %s", shaOldFull, shaNewFull), }, { name: "invalid change ID", @@ -156,14 +156,14 @@ func TestMergeChecker_Check(t *testing.T) { server := httptest.NewServer(tt.handler) defer server.Close() - mc := newTestMergeChecker(t, server.URL) + mc := newTestLandChecker(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.wantLandable, result.Landable) assert.Equal(t, tt.wantReason, result.Reason) }) } diff --git a/submitqueue/extension/mergechecker/github/graphql.go b/submitqueue/extension/landchecker/github/graphql.go similarity index 100% rename from submitqueue/extension/mergechecker/github/graphql.go rename to submitqueue/extension/landchecker/github/graphql.go diff --git a/submitqueue/extension/mergechecker/github/graphql_test.go b/submitqueue/extension/landchecker/github/graphql_test.go similarity index 100% rename from submitqueue/extension/mergechecker/github/graphql_test.go rename to submitqueue/extension/landchecker/github/graphql_test.go diff --git a/submitqueue/extension/mergechecker/github/validate.go b/submitqueue/extension/landchecker/github/validate.go similarity index 100% rename from submitqueue/extension/mergechecker/github/validate.go rename to submitqueue/extension/landchecker/github/validate.go diff --git a/submitqueue/extension/mergechecker/github/validate_test.go b/submitqueue/extension/landchecker/github/validate_test.go similarity index 99% rename from submitqueue/extension/mergechecker/github/validate_test.go rename to submitqueue/extension/landchecker/github/validate_test.go index efe622558..ed401f4ed 100644 --- a/submitqueue/extension/mergechecker/github/validate_test.go +++ b/submitqueue/extension/landchecker/github/validate_test.go @@ -89,7 +89,7 @@ func TestValidatePRs(t *testing.T) { wantReason: "PR #1 has merge conflicts", }, { - name: "unknown mergeability returns error", + name: "unknown landability returns error", changeIDs: []entitygithub.ChangeID{ {Scheme: "github", Org: "uber", Repo: "repo", PRNumber: 1, HeadCommitSHA: "sha1"}, }, diff --git a/submitqueue/extension/mergechecker/mergechecker.go b/submitqueue/extension/landchecker/landchecker.go similarity index 59% rename from submitqueue/extension/mergechecker/mergechecker.go rename to submitqueue/extension/landchecker/landchecker.go index 7ad8c41c4..34b5050c7 100644 --- a/submitqueue/extension/mergechecker/mergechecker.go +++ b/submitqueue/extension/landchecker/landchecker.go @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -package mergechecker +package landchecker -//go:generate mockgen -source=mergechecker.go -destination=mock/mergechecker_mock.go -package=mock +//go:generate mockgen -source=landchecker.go -destination=mock/landchecker_mock.go -package=mock import ( "context" @@ -22,26 +22,26 @@ import ( "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 +// LandChecker predicts whether a request's changes can land cleanly. +type LandChecker interface { + // Check is a fail-fast landability check that optimistically assesses + // whether the request's changes can be landed. 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) + // guarantee that the changes will apply cleanly at land time. + Check(ctx context.Context, request entity.Request) (entity.LandCheckResult, 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 identifies the queue this LandChecker serves. QueueName string } -// Factory builds the MergeChecker for a queue. Implementations are provided by +// Factory builds the LandChecker 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) + // For returns the LandChecker for the given queue. + For(cfg Config) (LandChecker, error) } diff --git a/submitqueue/extension/mergechecker/mock/BUILD.bazel b/submitqueue/extension/landchecker/mock/BUILD.bazel similarity index 71% rename from submitqueue/extension/mergechecker/mock/BUILD.bazel rename to submitqueue/extension/landchecker/mock/BUILD.bazel index 1b958a953..ae979bd56 100644 --- a/submitqueue/extension/mergechecker/mock/BUILD.bazel +++ b/submitqueue/extension/landchecker/mock/BUILD.bazel @@ -2,12 +2,12 @@ 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", + srcs = ["landchecker_mock.go"], + importpath = "github.com/uber/submitqueue/submitqueue/extension/landchecker/mock", visibility = ["//visibility:public"], deps = [ "//submitqueue/entity:go_default_library", - "//submitqueue/extension/mergechecker:go_default_library", + "//submitqueue/extension/landchecker:go_default_library", "@org_uber_go_mock//gomock:go_default_library", ], ) diff --git a/submitqueue/extension/mergechecker/mock/mergechecker_mock.go b/submitqueue/extension/landchecker/mock/landchecker_mock.go similarity index 60% rename from submitqueue/extension/mergechecker/mock/mergechecker_mock.go rename to submitqueue/extension/landchecker/mock/landchecker_mock.go index 48b1237c8..d24084df8 100644 --- a/submitqueue/extension/mergechecker/mock/mergechecker_mock.go +++ b/submitqueue/extension/landchecker/mock/landchecker_mock.go @@ -1,9 +1,9 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: mergechecker.go +// Source: landchecker.go // // Generated by this command: // -// mockgen -source=mergechecker.go -destination=mock/mergechecker_mock.go -package=mock +// mockgen -source=landchecker.go -destination=mock/landchecker_mock.go -package=mock // // Package mock is a generated GoMock package. @@ -14,47 +14,47 @@ import ( reflect "reflect" entity "github.com/uber/submitqueue/submitqueue/entity" - mergechecker "github.com/uber/submitqueue/submitqueue/extension/mergechecker" + landchecker "github.com/uber/submitqueue/submitqueue/extension/landchecker" gomock "go.uber.org/mock/gomock" ) -// MockMergeChecker is a mock of MergeChecker interface. -type MockMergeChecker struct { +// MockLandChecker is a mock of LandChecker interface. +type MockLandChecker struct { ctrl *gomock.Controller - recorder *MockMergeCheckerMockRecorder + recorder *MockLandCheckerMockRecorder isgomock struct{} } -// MockMergeCheckerMockRecorder is the mock recorder for MockMergeChecker. -type MockMergeCheckerMockRecorder struct { - mock *MockMergeChecker +// MockLandCheckerMockRecorder is the mock recorder for MockLandChecker. +type MockLandCheckerMockRecorder struct { + mock *MockLandChecker } -// NewMockMergeChecker creates a new mock instance. -func NewMockMergeChecker(ctrl *gomock.Controller) *MockMergeChecker { - mock := &MockMergeChecker{ctrl: ctrl} - mock.recorder = &MockMergeCheckerMockRecorder{mock} +// NewMockLandChecker creates a new mock instance. +func NewMockLandChecker(ctrl *gomock.Controller) *MockLandChecker { + mock := &MockLandChecker{ctrl: ctrl} + mock.recorder = &MockLandCheckerMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockMergeChecker) EXPECT() *MockMergeCheckerMockRecorder { +func (m *MockLandChecker) EXPECT() *MockLandCheckerMockRecorder { return m.recorder } // Check mocks base method. -func (m *MockMergeChecker) Check(ctx context.Context, request entity.Request) (entity.MergeResult, error) { +func (m *MockLandChecker) Check(ctx context.Context, request entity.Request) (entity.LandCheckResult, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Check", ctx, request) - ret0, _ := ret[0].(entity.MergeResult) + ret0, _ := ret[0].(entity.LandCheckResult) ret1, _ := ret[1].(error) return ret0, ret1 } // Check indicates an expected call of Check. -func (mr *MockMergeCheckerMockRecorder) Check(ctx, request any) *gomock.Call { +func (mr *MockLandCheckerMockRecorder) 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) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Check", reflect.TypeOf((*MockLandChecker)(nil).Check), ctx, request) } // MockFactory is a mock of Factory interface. @@ -82,10 +82,10 @@ func (m *MockFactory) EXPECT() *MockFactoryMockRecorder { } // For mocks base method. -func (m *MockFactory) For(cfg mergechecker.Config) (mergechecker.MergeChecker, error) { +func (m *MockFactory) For(cfg landchecker.Config) (landchecker.LandChecker, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "For", cfg) - ret0, _ := ret[0].(mergechecker.MergeChecker) + ret0, _ := ret[0].(landchecker.LandChecker) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/submitqueue/extension/speculation/allocator/sticky/sticky.go b/submitqueue/extension/speculation/allocator/sticky/sticky.go index fac42c657..d5135b6f3 100644 --- a/submitqueue/extension/speculation/allocator/sticky/sticky.go +++ b/submitqueue/extension/speculation/allocator/sticky/sticky.go @@ -81,7 +81,7 @@ func (a alloc) Allocate(ctx context.Context, pathSets []entity.SpeculationPathSe // keeps its slot until it actually stops. // // Only the path's status matters. No batch state enters this - // decision — "merging" and the rest are states of a batch, never of + // decision — "landing" and the rest are states of a batch, never of // a path — so the rule is simply that CI is still busy with it. funded[entry.ID] = true } diff --git a/submitqueue/extension/speculation/generator/bestfirst/README.md b/submitqueue/extension/speculation/generator/bestfirst/README.md index bd34769dd..b661b2e8e 100644 --- a/submitqueue/extension/speculation/generator/bestfirst/README.md +++ b/submitqueue/extension/speculation/generator/bestfirst/README.md @@ -10,7 +10,7 @@ The [best-first speculation path generation RFC](../../../../../doc/rfc/submitqu - `Next` removes the highest-ranked candidate, advances only that head's stream, constructs that candidate's complete path, and returns it. Pulling long enough returns every path exactly once in non-increasing score order. - Ranking scores are sums of log probabilities, avoiding underflow while preserving probability order. They are meaningful only within the run that produced them. - Exact ties prefer fewer flips; head ID then decides between heads (the cross-head heap holds one candidate per head), and taken flip indexes decide within a head. -- A dependency counts as resolved only once it is terminal. Merging and cancelling are both still in progress and either can end the other way, so both stay open questions here. Whether a path betting against a merging dependency is worth funding is a matter of price, and price is the scorer's to say. +- A dependency counts as resolved only once it is terminal. Landing and cancelling are both still in progress and either can end the other way, so both stay open questions here. Whether a path betting against a landing dependency is worth funding is a matter of price, and price is the scorer's to say. - A dependency that cannot be priced — the scorer call failed, the score was not a probability, or the snapshot never carried the batch — is treated as very likely to succeed rather than ending the run. One unusable number costs its own estimate, never the queue's whole set of candidates. A batch missing from the snapshot is never handed to the scorer at all: it would resolve to a zero batch belonging to no queue. - The snapshot must contain every batch a head's direct dependencies reference, carry unique non-empty batch IDs, and give no head an empty, duplicate, or self dependency. That is the caller's precondition, not something checked here: a malformed snapshot yields undefined candidates rather than an error. diff --git a/submitqueue/extension/speculation/generator/bestfirst/bestfirst_test.go b/submitqueue/extension/speculation/generator/bestfirst/bestfirst_test.go index 7dfb9f352..5f0a5d560 100644 --- a/submitqueue/extension/speculation/generator/bestfirst/bestfirst_test.go +++ b/submitqueue/extension/speculation/generator/bestfirst/bestfirst_test.go @@ -337,7 +337,7 @@ func TestBestFirst_OnlySpeculatingHeadsProduceCandidates(t *testing.T) { {state: entity.BatchStateUnknown}, {state: entity.BatchStateCreated}, {state: entity.BatchStateSpeculating, want: true}, - {state: entity.BatchStateMerging}, + {state: entity.BatchStateLanding}, {state: entity.BatchStateSucceeded}, {state: entity.BatchStateFailed}, {state: entity.BatchStateCancelling}, @@ -417,13 +417,13 @@ func TestBestFirst_NeverScoresAnAbsentDependency(t *testing.T) { assert.InDelta(t, math.Log(defaultProbability), cands[0].RankingScore, 1e-9) } -// A merging dependency is still in progress — the merge can fail — so it stays +// A landing dependency is still in progress — the land can fail — so it stays // an open question here like any other. Whether a path betting against it is // worth funding is a matter of price, which is the scorer's to say, not a // state the search hard-codes. -func TestBestFirst_MergingDependencyStaysOpen(t *testing.T) { +func TestBestFirst_LandingDependencyStaysOpen(t *testing.T) { batches := []entity.Batch{ - {ID: "q/landing", State: entity.BatchStateMerging}, + {ID: "q/landing", State: entity.BatchStateLanding}, {ID: "q/H", State: entity.BatchStateSpeculating, Dependencies: []string{"q/landing"}}, } sc := newCountingScorer(map[string]float64{"q/landing": 0.9}) @@ -432,8 +432,8 @@ func TestBestFirst_MergingDependencyStaysOpen(t *testing.T) { require.NoError(t, err) cands := drainAll(t, iter) - assert.Equal(t, 1, sc.calls["q/landing"], "a merging dependency is priced like any other") - require.Len(t, cands, 2, "both sides of a merge that has not landed yet") + assert.Equal(t, 1, sc.calls["q/landing"], "a landing dependency is priced like any other") + require.Len(t, cands, 2, "both sides of a land that has not completed yet") assert.Equal(t, entity.DependencyAssumptionSucceeds, assumptionFor(cands[0].Path, "q/landing")) assert.Equal(t, entity.DependencyAssumptionFails, assumptionFor(cands[1].Path, "q/landing")) } diff --git a/submitqueue/extension/speculation/scorer/scorer.go b/submitqueue/extension/speculation/scorer/scorer.go index a0d75401c..c654cd821 100644 --- a/submitqueue/extension/speculation/scorer/scorer.go +++ b/submitqueue/extension/speculation/scorer/scorer.go @@ -29,7 +29,7 @@ type Scorer interface { // ultimately succeeds — reaches its terminal Succeeded state with its // changes landed, rather than Failed or Cancelled. A passing build is // necessary but not sufficient: a batch whose build already passed can - // still fail to merge, so this is the probability of the final outcome, + // still fail to land, so this is the probability of the final outcome, // not of the build alone. It is handed the batch identity and resolves the // batch's changes itself through an injected changeset.Resolver. // diff --git a/submitqueue/extension/speculation/speculator/README.md b/submitqueue/extension/speculation/speculator/README.md index 62343796a..3b842a7c7 100644 --- a/submitqueue/extension/speculation/speculator/README.md +++ b/submitqueue/extension/speculation/speculator/README.md @@ -1,6 +1,6 @@ # speculator -The `speculator` package defines the one speculation extension the speculate controller calls. A `Speculator` decides **which speculation paths to build and which running ones to cancel**, within the queue's build budget — and nothing else. It can never express a verdict: whether a batch merges or fails is fixed by the facts and computed by the controller, so swapping in a different `Speculator` changes which paths run, never a batch's outcome. +The `speculator` package defines the one speculation extension the speculate controller calls. A `Speculator` decides **which speculation paths to build and which running ones to cancel**, within the queue's build budget — and nothing else. It can never express a verdict: whether a batch lands or fails is fixed by the facts and computed by the controller, so swapping in a different `Speculator` changes which paths run, never a batch's outcome. `Speculate` is handed the queue's in-flight batches plus any finalized batches still referenced as dependencies (each with its dependency list and state) and every path set for them — live and recently finished, so a `Speculator` will not re-propose a path that already passed or failed. It returns a list of build and cancel actions; a path it wants left as-is has no entry in the result. The controller validates the output (dropping builds it shouldn't propose and rejecting cancels of passed paths), so an implementation may read extra injected data without affecting correctness. diff --git a/submitqueue/extension/speculation/speculator/speculator.go b/submitqueue/extension/speculation/speculator/speculator.go index acf7237f8..296d1c19e 100644 --- a/submitqueue/extension/speculation/speculator/speculator.go +++ b/submitqueue/extension/speculation/speculator/speculator.go @@ -25,7 +25,7 @@ import ( // Speculator decides which speculation paths to build and which running ones to // cancel, within the queue's build budget. It is the only speculation extension the // speculate controller calls. It can never express a verdict: whether a batch -// merges or fails is fixed by the facts and computed by the controller, so a +// lands or fails is fixed by the facts and computed by the controller, so a // swapped-in Speculator changes which paths run, never a batch's outcome. type Speculator interface { // Speculate proposes this run's path actions. diff --git a/submitqueue/extension/speculation/speculator/standard/README.md b/submitqueue/extension/speculation/speculator/standard/README.md index c5d869e65..c96f1f8f6 100644 --- a/submitqueue/extension/speculation/speculator/standard/README.md +++ b/submitqueue/extension/speculation/speculator/standard/README.md @@ -4,7 +4,7 @@ The `standard` `Speculator` funds the queue's most promising speculation paths f Each run it considers candidate paths in descending order of their probability of being the future that actually happens, and proposes builds down that ranking. Paths already pending or building keep the slot they hold rather than restarting; paths whose builds already finished are skipped for as long as their records remain in the supplied path sets, so a finished path can be proposed again — for a retry, say — once retention drops it; new builds fill whatever budget remains. -When the budget runs out, everything below the cut waits for a later run. That is safe because the propose-side cannot invent a batch verdict: the speculate controller still decides merge from the persisted paths, including complete coverage of unsettled dependencies. +When the budget runs out, everything below the cut waits for a later run. That is safe because the propose-side cannot invent a batch verdict: the speculate controller still decides land from the persisted paths, including complete coverage of unsettled dependencies. Both halves are swappable. The ranking is the `Generator`'s: the default `bestfirst` scores each path by the probability that all its assumptions hold. The budget policy is the `Allocator`'s: the default `sticky` fills only free slots and never preempts, where a preempting allocator would cancel a low-value in-flight path to fund a better one. diff --git a/submitqueue/extension/storage/mysql/BUILD.bazel b/submitqueue/extension/storage/mysql/BUILD.bazel index bc7d829c7..364bb7090 100644 --- a/submitqueue/extension/storage/mysql/BUILD.bazel +++ b/submitqueue/extension/storage/mysql/BUILD.bazel @@ -49,7 +49,7 @@ go_test( embed = [":go_default_library"], deps = [ "//platform/base/change:go_default_library", - "//platform/base/mergestrategy:go_default_library", + "//platform/base/landstrategy:go_default_library", "//submitqueue/entity:go_default_library", "//submitqueue/extension/storage:go_default_library", "@com_github_data_dog_go_sqlmock//:go_default_library", diff --git a/submitqueue/extension/storage/mysql/batch_store_test.go b/submitqueue/extension/storage/mysql/batch_store_test.go index 7cd69aeff..145fd7620 100644 --- a/submitqueue/extension/storage/mysql/batch_store_test.go +++ b/submitqueue/extension/storage/mysql/batch_store_test.go @@ -205,7 +205,7 @@ func TestBatchStore_Update(t *testing.T) { Queue: "monorepo", Contains: []string{"monorepo/3", "monorepo/4"}, Dependencies: []string{"monorepo/batch/1", "monorepo/batch/2"}, - State: entity.BatchStateMerging, + State: entity.BatchStateLanding, Version: oldVersion, } containsJSON, err := json.Marshal(batch.Contains) diff --git a/submitqueue/extension/storage/mysql/request_store_test.go b/submitqueue/extension/storage/mysql/request_store_test.go index 80c05a59d..55f142423 100644 --- a/submitqueue/extension/storage/mysql/request_store_test.go +++ b/submitqueue/extension/storage/mysql/request_store_test.go @@ -27,7 +27,7 @@ import ( "github.com/stretchr/testify/require" "github.com/uber/submitqueue/platform/base/change" - "github.com/uber/submitqueue/platform/base/mergestrategy" + "github.com/uber/submitqueue/platform/base/landstrategy" "github.com/uber/submitqueue/submitqueue/entity" "github.com/uber/submitqueue/submitqueue/extension/storage" ) @@ -48,7 +48,7 @@ func TestRequestStore_Get(t *testing.T) { ID: "monorepo/1", Queue: "monorepo", Change: change.Change{URIs: []string{"github://github.example.com/uber/submitqueue/pull/123/deadbeef"}}, - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, State: entity.RequestStateStarted, Version: 1, } @@ -137,7 +137,7 @@ func TestRequestStore_Create(t *testing.T) { ID: "monorepo/1", Queue: "monorepo", Change: change.Change{URIs: []string{"github://github.example.com/uber/submitqueue/pull/123/deadbeef"}}, - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, State: entity.RequestStateStarted, Version: 1, } @@ -204,7 +204,7 @@ func TestRequestStore_Update(t *testing.T) { ID: "monorepo/1", Queue: "monorepo", Change: change.Change{URIs: []string{"github://github.example.com/uber/submitqueue/pull/456/cafebabe"}}, - LandStrategy: mergestrategy.MergeStrategySquashRebase, + LandStrategy: landstrategy.StrategySquashRebase, State: entity.RequestStateValidated, Version: oldVersion, } diff --git a/submitqueue/gateway/controller/BUILD.bazel b/submitqueue/gateway/controller/BUILD.bazel index 377136749..8d000922b 100644 --- a/submitqueue/gateway/controller/BUILD.bazel +++ b/submitqueue/gateway/controller/BUILD.bazel @@ -45,7 +45,7 @@ go_test( deps = [ "//api/submitqueue/gateway/protopb:go_default_library", "//platform/base/change:go_default_library", - "//platform/base/mergestrategy:go_default_library", + "//platform/base/landstrategy:go_default_library", "//platform/base/messagequeue:go_default_library", "//platform/consumer:go_default_library", "//platform/errs:go_default_library", diff --git a/submitqueue/gateway/controller/cancel.go b/submitqueue/gateway/controller/cancel.go index 644ae3448..2cd42459e 100644 --- a/submitqueue/gateway/controller/cancel.go +++ b/submitqueue/gateway/controller/cancel.go @@ -32,7 +32,7 @@ import ( // CancelController handles cancel business logic for the gateway. It validates the request, // records a RequestStatusCancelling log entry (intent-only — cancellation is best-effort -// and may still race a successful merge), publishes a CancelRequest to the cancel topic, +// and may still race a successful land), publishes a CancelRequest to the cancel topic, // and returns a response. The orchestrator-side cancel controller performs the actual // state transitions and emits the terminal RequestStatusCancelled log entry. type CancelController interface { @@ -66,7 +66,7 @@ func NewCancelController(logger *zap.SugaredLogger, scope tally.Scope, stores st // is performed asynchronously by the orchestrator cancel controller. Cancel is idempotent: // the orchestrator treats already-terminal requests as a no-op. // -// Cancellation is best-effort: a request that has already merged or that races to +// Cancellation is best-effort: a request that has already landed or that races to // completion before the cancel propagates may still land. The RequestStatusCancelling // entry written here records the user's intent; the terminal outcome is reflected by a // later RequestStatusCancelled (orchestrator side) or RequestStatusLanded entry. diff --git a/submitqueue/gateway/controller/land_test.go b/submitqueue/gateway/controller/land_test.go index c957ef23b..f62e4b7d5 100644 --- a/submitqueue/gateway/controller/land_test.go +++ b/submitqueue/gateway/controller/land_test.go @@ -24,7 +24,7 @@ import ( "github.com/stretchr/testify/require" "github.com/uber-go/tally" "github.com/uber/submitqueue/platform/base/change" - "github.com/uber/submitqueue/platform/base/mergestrategy" + "github.com/uber/submitqueue/platform/base/landstrategy" entityqueue "github.com/uber/submitqueue/platform/base/messagequeue" "github.com/uber/submitqueue/platform/consumer" "github.com/uber/submitqueue/platform/errs" @@ -103,7 +103,7 @@ func testLandRequest(queue string) entity.LandRequest { return entity.LandRequest{ Queue: queue, Change: change.Change{URIs: []string{"github://github.example.com/uber/test-repo/pull/123/c3a4d5e6f7890123456789abcdef0123456789ab"}}, - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, } } @@ -412,7 +412,7 @@ func TestLand_PublishesToQueue(t *testing.T) { req := entity.LandRequest{ Queue: "test-queue", Change: change.Change{URIs: []string{"github://github.example.com/uber/backend/pull/456/fedcba9876543210fedcba9876543210fedcba98"}}, - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, } result, err := controller.Land(ctx, req) @@ -467,7 +467,7 @@ func TestLand_PublishesToQueue(t *testing.T) { assert.Equal(t, "test-queue/123", deserializedReq.ID) assert.Equal(t, "test-queue", deserializedReq.Queue) assert.Equal(t, []string{"github://github.example.com/uber/backend/pull/456/fedcba9876543210fedcba9876543210fedcba98"}, deserializedReq.Change.URIs) - assert.Equal(t, mergestrategy.MergeStrategyRebase, deserializedReq.LandStrategy) + assert.Equal(t, landstrategy.StrategyRebase, deserializedReq.LandStrategy) } func TestLand_ReturnsErrorWhenPublishFails(t *testing.T) { diff --git a/submitqueue/orchestrator/BUILD.bazel b/submitqueue/orchestrator/BUILD.bazel index 35b1d581d..f806c7789 100644 --- a/submitqueue/orchestrator/BUILD.bazel +++ b/submitqueue/orchestrator/BUILD.bazel @@ -28,9 +28,9 @@ go_library( "//submitqueue/orchestrator/controller/conclude:go_default_library", "//submitqueue/orchestrator/controller/dependencyanalysis:go_default_library", "//submitqueue/orchestrator/controller/dlq:go_default_library", - "//submitqueue/orchestrator/controller/merge:go_default_library", - "//submitqueue/orchestrator/controller/mergeconflictsignal:go_default_library", - "//submitqueue/orchestrator/controller/mergesignal:go_default_library", + "//submitqueue/orchestrator/controller/land:go_default_library", + "//submitqueue/orchestrator/controller/landconflictsignal:go_default_library", + "//submitqueue/orchestrator/controller/landsignal:go_default_library", "//submitqueue/orchestrator/controller/speculate:go_default_library", "//submitqueue/orchestrator/controller/start:go_default_library", "//submitqueue/orchestrator/controller/validate:go_default_library", diff --git a/submitqueue/orchestrator/README.md b/submitqueue/orchestrator/README.md index 6dea30d7c..8bc3779f2 100644 --- a/submitqueue/orchestrator/README.md +++ b/submitqueue/orchestrator/README.md @@ -8,17 +8,17 @@ The pipeline is queue-driven: each stage consumes one topic, advances one entity - **start** — receives `LandRequest` from the gateway, persists the `Request` entity, and emits `Started`. - **cancel** — records cancellation intent and hands affected batches to speculation for best-effort cancellation. -- **validate** — checks for duplicates, resolves change metadata, and publishes a `MergeRequest` to Runway's `merge-conflict-check` topic. -- **merge-conflict-check-signal** — correlates the dry-run result, fails the request on conflict, or forwards it to batching. +- **validate** — checks for duplicates, resolves change metadata, and publishes a `LandRequest` to Runway's `land-conflict-check` topic. +- **land-conflict-check-signal** — correlates the dry-run result, fails the request on conflict, or forwards it to batching. - **batch** — creates an inert batch attempt and hands it to dependency analysis. - **dependency-analysis** — enrols requests, computes dependencies, and promotes the selected batch attempt. - **speculate** — reconciles queue-wide path state, decides outcomes, and allocates speculative builds. - **build** — triggers a CI build for a speculative path. - **buildsignal** — polls or receives CI state, records the result, wakes `speculate`, and holds non-terminal deliveries until the next poll. -- **merge** — publishes a committing `MergeRequest` to Runway's `runway-merge` topic. -- **merge-signal** — correlates the merge result and fans out to `conclude` and back to `speculate`. +- **land** — publishes a committing `LandRequest` to Runway's `runway-land` topic. +- **land-signal** — correlates the land result and fans out to `conclude` and back to `speculate`. - **conclude** — maps the terminal batch state to the request states. - **submitqueue-hook** — dispatches lifecycle hook events to configured integrations. - **DLQ reconcilers** — one per primary consumed topic, driving stuck requests/batches to a conservative terminal `failed` state. -The orchestrator publishes request-log entries to `log`, but does not consume or persist them; the gateway owns that stage. It also publishes full cross-service requests to Runway's merge-conflict-check and merge topics. +The orchestrator publishes request-log entries to `log`, but does not consume or persist them; the gateway owns that stage. It also publishes full cross-service requests to Runway's land-conflict-check and land topics. diff --git a/submitqueue/orchestrator/controller/README.md b/submitqueue/orchestrator/controller/README.md index 4fc03735a..9f69b9a4b 100644 --- a/submitqueue/orchestrator/controller/README.md +++ b/submitqueue/orchestrator/controller/README.md @@ -50,7 +50,7 @@ Such effects require a provider-supported idempotency key, a stable operation id The speculate topic is a dirty signal for a queue, not a command to transition only the named batch. A run reloads the queue's in-flight batches, dependency outcomes, path sets, and build results; admits any batches still in `Created`; commits outcomes to a fixed point; asks the configured speculator for new proposals; persists changed path sets; and dispatches pending builds. -A batch can advance to merge when one passed path matches all settled dependency outcomes. It can also advance while dependencies remain unsettled when passed paths cover every possible outcome of those dependencies, proving that the head passed regardless of how they finish. +A batch can advance to land when one passed path matches all settled dependency outcomes. It can also advance while dependencies remain unsettled when passed paths cover every possible outcome of those dependencies, proving that the head passed regardless of how they finish. Path-set changes are persisted before build dispatch. Terminal outcomes are committed before later decisions derive from them. Selected request-log announcements are intentionally published before their corresponding state write so replay cannot lose the observation. diff --git a/submitqueue/orchestrator/controller/batch/BUILD.bazel b/submitqueue/orchestrator/controller/batch/BUILD.bazel index 0270e7e89..4db4ef00a 100644 --- a/submitqueue/orchestrator/controller/batch/BUILD.bazel +++ b/submitqueue/orchestrator/controller/batch/BUILD.bazel @@ -25,7 +25,7 @@ go_test( embed = [":go_default_library"], deps = [ "//platform/base/change:go_default_library", - "//platform/base/mergestrategy:go_default_library", + "//platform/base/landstrategy:go_default_library", "//platform/base/messagequeue:go_default_library", "//platform/consumer:go_default_library", "//platform/consumer/mock:go_default_library", diff --git a/submitqueue/orchestrator/controller/batch/batch_test.go b/submitqueue/orchestrator/controller/batch/batch_test.go index 68bfc7820..b7d4c0a74 100644 --- a/submitqueue/orchestrator/controller/batch/batch_test.go +++ b/submitqueue/orchestrator/controller/batch/batch_test.go @@ -24,7 +24,7 @@ import ( "github.com/stretchr/testify/require" "github.com/uber-go/tally" "github.com/uber/submitqueue/platform/base/change" - "github.com/uber/submitqueue/platform/base/mergestrategy" + "github.com/uber/submitqueue/platform/base/landstrategy" entityqueue "github.com/uber/submitqueue/platform/base/messagequeue" "github.com/uber/submitqueue/platform/consumer" consumermock "github.com/uber/submitqueue/platform/consumer/mock" @@ -78,7 +78,7 @@ func testRequest() entity.Request { ID: "test-queue/123", Queue: "test-queue", Change: change.Change{URIs: []string{"github://github.example.com/uber/service/pull/456/abcdef0123456789abcdef0123456789abcdef01"}}, - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, State: entity.RequestStateStarted, Version: 1, } diff --git a/submitqueue/orchestrator/controller/cancel/cancel.go b/submitqueue/orchestrator/controller/cancel/cancel.go index 243e3af32..07493accf 100644 --- a/submitqueue/orchestrator/controller/cancel/cancel.go +++ b/submitqueue/orchestrator/controller/cancel/cancel.go @@ -26,14 +26,14 @@ // - The request is associated with one or more batch attempts — the controller // records cancellation intent on every cancellable attempt and hands each one // to speculate. Creating attempts are ignored because their dependency set is -// not yet resolved and nothing downstream can see them, while Merging and +// not yet resolved and nothing downstream can see them, while Landing and // terminal attempts retain their existing outcome for conclude to reconcile. // // The split exists so that the terminal write and the work that must precede // it (cancelling builds, respeculating dependents) live in the same controller // — speculate is the single writer of every non-Cancelling batch state and is // already wired with the build/dependent stores. Forward-progress controllers -// (build, buildsignal, merge) observe BatchStateCancelling via +// (build, buildsignal, land) observe BatchStateCancelling via // IsBatchStateHalted and short-circuit while speculate drives the batch to // its terminal state. // @@ -149,7 +149,7 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er // Step 1: record the cancellation intent on the request itself by transitioning // to RequestStateCancelling. This is non-terminal; forward-progress controllers // (validate, batch) treat it as halted, but conclude may still write a different - // terminal state if a concurrent merge or failure wins the race. + // terminal state if a concurrent land or failure wins the race. request, err = c.markCancelling(ctx, store, request) if err != nil { return err @@ -180,10 +180,10 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er firstErr = err } } - case batch.State == entity.BatchStateMerging: - // Merge owns the outcome once it has started. Conclude will reconcile the request with that outcome. + case batch.State == entity.BatchStateLanding: + // Land owns the outcome once it has started. Conclude will reconcile the request with that outcome. foundApplicableBatch = true - metrics.NamedCounter(c.metricsScope, opName, "batch_merging", 1) + metrics.NamedCounter(c.metricsScope, opName, "batch_landing", 1) case batch.State.IsTerminal(): // The terminal batch outcome wins; conclude may not have reconciled the request yet. foundApplicableBatch = true @@ -311,7 +311,7 @@ func (c *Controller) cancelBatch(ctx context.Context, store storage.Storage, bat if err != nil { metrics.NamedCounter(c.metricsScope, opName, "batch_update_errors", 1) // storage.ErrVersionMismatch here means the batch advanced concurrently - // (e.g. speculate / merge progressed). Returned as-is because the + // (e.g. speculate / land progressed). Returned as-is because the // sentinel is intrinsically retryable; the re-fetch will see the new state // and either short-circuit (already terminal) or attempt the transition // again. diff --git a/submitqueue/orchestrator/controller/cancel/cancel_test.go b/submitqueue/orchestrator/controller/cancel/cancel_test.go index 01274bafa..e5bc743e4 100644 --- a/submitqueue/orchestrator/controller/cancel/cancel_test.go +++ b/submitqueue/orchestrator/controller/cancel/cancel_test.go @@ -517,7 +517,7 @@ func TestProcess_NonCancellableBatchSuppressesRequestCancellation(t *testing.T) name string state entity.BatchState }{ - {name: "merging", state: entity.BatchStateMerging}, + {name: "landing", state: entity.BatchStateLanding}, {name: "succeeded", state: entity.BatchStateSucceeded}, {name: "failed", state: entity.BatchStateFailed}, {name: "cancelled", state: entity.BatchStateCancelled}, diff --git a/submitqueue/orchestrator/controller/conclude/conclude.go b/submitqueue/orchestrator/controller/conclude/conclude.go index 8f7c28d38..86432d831 100644 --- a/submitqueue/orchestrator/controller/conclude/conclude.go +++ b/submitqueue/orchestrator/controller/conclude/conclude.go @@ -106,7 +106,7 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er ) // Map batch terminal state to request state. - // We expect the batch to be in a terminal state as written by the merge + // We expect the batch to be in a terminal state as written by the land // controller (Succeeded) or the speculate controller (Failed via // failOnDependency, Cancelled via cancelBatch). requestState, err := batchStateToRequestState(batch.State) diff --git a/submitqueue/orchestrator/controller/dependencyanalysis/dependencyanalysis.go b/submitqueue/orchestrator/controller/dependencyanalysis/dependencyanalysis.go index cb36da1d8..66f8ec81d 100644 --- a/submitqueue/orchestrator/controller/dependencyanalysis/dependencyanalysis.go +++ b/submitqueue/orchestrator/controller/dependencyanalysis/dependencyanalysis.go @@ -219,7 +219,7 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er metrics.NamedCounter(c.metricsScope, opName, "reannounced", 1) default: - // Speculating or merging: the announcement landed and the batch has + // Speculating or landing: the announcement landed and the batch has // already moved past this stage. metrics.NamedCounter(c.metricsScope, opName, "already_admitted", 1) return nil @@ -246,7 +246,7 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er // with its own hand-off. This is where that is resolved: the topic is // partitioned by queue and consumed in order, so the first hand-off through // here enrols the request and the second finds it and stops. Without the check -// the same change would end up in two live batches, both admitted, both merged. +// the same change would end up in two live batches, both admitted, both landed. func (c *Controller) requestEnrolledInAnotherBatch(ctx context.Context, store storage.Storage, batch entity.Batch) (bool, error) { for _, requestID := range batch.Contains { existing, stale, err := corebatch.FindByRequestID(ctx, store, requestID) diff --git a/submitqueue/orchestrator/controller/dependencyanalysis/dependencyanalysis_test.go b/submitqueue/orchestrator/controller/dependencyanalysis/dependencyanalysis_test.go index 1a04e7032..c04619d17 100644 --- a/submitqueue/orchestrator/controller/dependencyanalysis/dependencyanalysis_test.go +++ b/submitqueue/orchestrator/controller/dependencyanalysis/dependencyanalysis_test.go @@ -410,7 +410,7 @@ func TestController_Process_HaltedBatchAcksWithoutPublishing(t *testing.T) { // A batch already admitted got its announcement; re-sending one would only buy // a redundant re-plan. func TestController_Process_AlreadyAdmittedAcksWithoutPublishing(t *testing.T) { - for _, state := range []entity.BatchState{entity.BatchStateSpeculating, entity.BatchStateMerging} { + for _, state := range []entity.BatchState{entity.BatchStateSpeculating, entity.BatchStateLanding} { t.Run(string(state), func(t *testing.T) { ctrl := gomock.NewController(t) diff --git a/submitqueue/orchestrator/controller/dlq/BUILD.bazel b/submitqueue/orchestrator/controller/dlq/BUILD.bazel index ffcbd2eb1..a78013a4e 100644 --- a/submitqueue/orchestrator/controller/dlq/BUILD.bazel +++ b/submitqueue/orchestrator/controller/dlq/BUILD.bazel @@ -6,9 +6,9 @@ go_library( "batch.go", "buildsignal.go", "dlq.go", + "landconflictsignal.go", + "landsignal.go", "log.go", - "mergeconflictsignal.go", - "mergesignal.go", "request.go", "speculate.go", ], @@ -35,9 +35,9 @@ go_test( "batch_test.go", "buildsignal_test.go", "dlq_test.go", + "landconflictsignal_test.go", + "landsignal_test.go", "log_test.go", - "mergeconflictsignal_test.go", - "mergesignal_test.go", "publisher_test.go", "request_test.go", "speculate_test.go", diff --git a/submitqueue/orchestrator/controller/dlq/README.md b/submitqueue/orchestrator/controller/dlq/README.md index befa1bb9f..a4dfe6ccf 100644 --- a/submitqueue/orchestrator/controller/dlq/README.md +++ b/submitqueue/orchestrator/controller/dlq/README.md @@ -31,7 +31,7 @@ Two controller shapes cover the eleven primary pipeline topics: | Controller | Topics | Decoded ID | Terminal state | |---|---|---|---| | `NewDLQRequestController` | `start`, `validate`, `batch`, `cancel`, `log` | `RequestID` | `RequestStateError` | -| `NewDLQBatchController` | `speculate`, `build`, `merge`, `conclude` | `BatchID` | `BatchStateFailed` + fan-out to member requests as `RequestStateError` | +| `NewDLQBatchController` | `speculate`, `build`, `land`, `conclude` | `BatchID` | `BatchStateFailed` + fan-out to member requests as `RequestStateError` | `buildsignal` carries a `Build` payload and has its own small dedicated controller. The split exists because the DLQ message payload shape mirrors the primary topic's payload shape (the queue framework preserves bytes verbatim under the `_dlq` topic name), so the decoder is what changes per topic — not the reconciliation step. The package-level `RequestIDDecoder` interface plus `DecodeLandRequestID` / `DecodeCancelRequestID` / `DecodeRequestID` cover the three payload shapes used by request-scoped topics. diff --git a/submitqueue/orchestrator/controller/dlq/batch.go b/submitqueue/orchestrator/controller/dlq/batch.go index 5111926bf..9bc25fb65 100644 --- a/submitqueue/orchestrator/controller/dlq/batch.go +++ b/submitqueue/orchestrator/controller/dlq/batch.go @@ -27,7 +27,7 @@ import ( ) // batchController is the DLQ reconciler for batch-scoped pipeline stages -// (build, merge, conclude). All three topics carry a BatchID payload, so this +// (build, land, conclude). All three topics carry a BatchID payload, so this // controller is registered three times — one per topic, each with the matching // DLQ topic key and consumer group. // @@ -39,7 +39,7 @@ import ( // // Blaming the batch on the message is right for these stages because their // work is that batch: whatever failed, it failed doing this batch's build, -// merge, or conclusion. The speculate stage is not like that — it re-plans a +// land, or conclusion. The speculate stage is not like that — it re-plans a // whole queue from a message that names one batch — so it has its own // reconciler; see speculate.go. type batchController struct { diff --git a/submitqueue/orchestrator/controller/dlq/batch_test.go b/submitqueue/orchestrator/controller/dlq/batch_test.go index a125271df..a75fea26b 100644 --- a/submitqueue/orchestrator/controller/dlq/batch_test.go +++ b/submitqueue/orchestrator/controller/dlq/batch_test.go @@ -33,11 +33,11 @@ func TestDLQBatchController_InterfaceAndAccessors(t *testing.T) { store := storagemock.NewMockStorage(ctrl) store.EXPECT().GetQueueBatchStateStore().Return(newQueueBatchStateStore(ctrl)).AnyTimes() - c := NewDLQBatchController(zaptest.NewLogger(t).Sugar(), testScope(), staticStorageFactory{store: store}, consumer.TopicRegistry{}, TopicKey(topickey.TopicKeyMerge), "orchestrator-merge-dlq") + c := NewDLQBatchController(zaptest.NewLogger(t).Sugar(), testScope(), staticStorageFactory{store: store}, consumer.TopicRegistry{}, TopicKey(topickey.TopicKeyLand), "orchestrator-land-dlq") - assert.Equal(t, "submitqueue-merge_dlq", c.Name()) - assert.Equal(t, consumer.TopicKey("submitqueue-merge_dlq"), c.TopicKey()) - assert.Equal(t, "orchestrator-merge-dlq", c.ConsumerGroup()) + assert.Equal(t, "submitqueue-land_dlq", c.Name()) + assert.Equal(t, consumer.TopicKey("submitqueue-land_dlq"), c.TopicKey()) + assert.Equal(t, "orchestrator-land-dlq", c.ConsumerGroup()) } func TestDLQBatchController_Process_FailsAndFansOut(t *testing.T) { @@ -46,7 +46,7 @@ func TestDLQBatchController_Process_FailsAndFansOut(t *testing.T) { batchStore := storagemock.NewMockBatchStore(ctrl) batch := entity.Batch{ ID: "q/batch/9", Queue: "q", Contains: []string{"q/1"}, - State: entity.BatchStateMerging, Version: 2, + State: entity.BatchStateLanding, Version: 2, } batchStore.EXPECT().Get(gomock.Any(), "q/batch/9").Return(batch, nil) batchStore.EXPECT().Update(gomock.Any(), batchWithState(batch, entity.BatchStateFailed), int32(2), int32(3)).Return(nil) @@ -67,7 +67,7 @@ func TestDLQBatchController_Process_FailsAndFansOut(t *testing.T) { store.EXPECT().GetBatchStore().Return(batchStore).AnyTimes() store.EXPECT().GetRequestStore().Return(requestStore).AnyTimes() - c := NewDLQBatchController(zaptest.NewLogger(t).Sugar(), testScope(), staticStorageFactory{store: store}, registry, TopicKey(topickey.TopicKeyMerge), "orchestrator-merge-dlq") + c := NewDLQBatchController(zaptest.NewLogger(t).Sugar(), testScope(), staticStorageFactory{store: store}, registry, TopicKey(topickey.TopicKeyLand), "orchestrator-land-dlq") payload, err := entity.BatchID{ID: "q/batch/9"}.ToBytes() require.NoError(t, err) @@ -81,7 +81,7 @@ func TestDLQBatchController_Process_MalformedPayloadFails(t *testing.T) { store := storagemock.NewMockStorage(ctrl) store.EXPECT().GetQueueBatchStateStore().Return(newQueueBatchStateStore(ctrl)).AnyTimes() - c := NewDLQBatchController(zaptest.NewLogger(t).Sugar(), testScope(), staticStorageFactory{store: store}, consumer.TopicRegistry{}, TopicKey(topickey.TopicKeyMerge), "orchestrator-merge-dlq") + c := NewDLQBatchController(zaptest.NewLogger(t).Sugar(), testScope(), staticStorageFactory{store: store}, consumer.TopicRegistry{}, TopicKey(topickey.TopicKeyLand), "orchestrator-land-dlq") delivery := newMockDelivery(ctrl, []byte("garbage")) err := c.Process(context.Background(), delivery) @@ -93,7 +93,7 @@ func TestDLQBatchController_Process_EmptyIDFails(t *testing.T) { store := storagemock.NewMockStorage(ctrl) store.EXPECT().GetQueueBatchStateStore().Return(newQueueBatchStateStore(ctrl)).AnyTimes() - c := NewDLQBatchController(zaptest.NewLogger(t).Sugar(), testScope(), staticStorageFactory{store: store}, consumer.TopicRegistry{}, TopicKey(topickey.TopicKeyMerge), "orchestrator-merge-dlq") + c := NewDLQBatchController(zaptest.NewLogger(t).Sugar(), testScope(), staticStorageFactory{store: store}, consumer.TopicRegistry{}, TopicKey(topickey.TopicKeyLand), "orchestrator-land-dlq") payload, err := entity.BatchID{ID: ""}.ToBytes() require.NoError(t, err) diff --git a/submitqueue/orchestrator/controller/dlq/dlq.go b/submitqueue/orchestrator/controller/dlq/dlq.go index ee9c6b387..3d99e4d1c 100644 --- a/submitqueue/orchestrator/controller/dlq/dlq.go +++ b/submitqueue/orchestrator/controller/dlq/dlq.go @@ -28,7 +28,7 @@ // new `{topic}_dlq` name). The DLQ controllers decode that payload to recover // the affected request or batch, then transition it to a terminal failed // state — Error for requests, Failed for batches — with an idempotent -// optimistic-locking write so concurrent activity (a late merge, a cancel +// optimistic-locking write so concurrent activity (a late land, a cancel // race) wins cleanly. Batch failures also fan out to the member requests so // the gateway no longer reports them as in-progress. package dlq diff --git a/submitqueue/orchestrator/controller/dlq/dlq_test.go b/submitqueue/orchestrator/controller/dlq/dlq_test.go index 8d7f63b98..d44bf3443 100644 --- a/submitqueue/orchestrator/controller/dlq/dlq_test.go +++ b/submitqueue/orchestrator/controller/dlq/dlq_test.go @@ -221,7 +221,7 @@ func TestFailBatch_TransitionsAndFansOut(t *testing.T) { batchStore := storagemock.NewMockBatchStore(ctrl) batch := entity.Batch{ ID: "q/batch/1", Queue: "q", Contains: []string{"q/1", "q/2"}, - State: entity.BatchStateMerging, Version: 4, + State: entity.BatchStateLanding, Version: 4, } batchStore.EXPECT().Get(gomock.Any(), "q/batch/1").Return(batch, nil) batchStore.EXPECT().Update(gomock.Any(), batchWithState(batch, entity.BatchStateFailed), int32(4), int32(5)).Return(nil) diff --git a/submitqueue/orchestrator/controller/dlq/mergeconflictsignal.go b/submitqueue/orchestrator/controller/dlq/landconflictsignal.go similarity index 73% rename from submitqueue/orchestrator/controller/dlq/mergeconflictsignal.go rename to submitqueue/orchestrator/controller/dlq/landconflictsignal.go index c51561288..a4cb87e1e 100644 --- a/submitqueue/orchestrator/controller/dlq/mergeconflictsignal.go +++ b/submitqueue/orchestrator/controller/dlq/landconflictsignal.go @@ -26,11 +26,11 @@ import ( "go.uber.org/zap" ) -// mergeConflictSignalController is the DLQ reconciler for the -// mergeconflictsignal topic. Its payload carries a runway -// MergeResult whose id is the request id echoed back, so +// landConflictSignalController is the DLQ reconciler for the +// landconflictsignal topic. Its payload carries a runway +// LandResult whose id is the request id echoed back, so // reconciliation fails that request directly via failRequest. -type mergeConflictSignalController struct { +type landConflictSignalController struct { logger *zap.SugaredLogger metricsScope tally.Scope stores storage.Factory @@ -39,12 +39,12 @@ type mergeConflictSignalController struct { consumerGroup string } -// Verify mergeConflictSignalController implements consumer.Controller at compile time. -var _ consumer.Controller = (*mergeConflictSignalController)(nil) +// Verify landConflictSignalController implements consumer.Controller at compile time. +var _ consumer.Controller = (*landConflictSignalController)(nil) -// NewDLQMergeConflictSignalController builds a DLQ controller for the -// mergeconflictsignal topic. -func NewDLQMergeConflictSignalController( +// NewDLQLandConflictSignalController builds a DLQ controller for the +// landconflictsignal topic. +func NewDLQLandConflictSignalController( logger *zap.SugaredLogger, scope tally.Scope, stores storage.Factory, @@ -53,7 +53,7 @@ func NewDLQMergeConflictSignalController( consumerGroup string, ) consumer.Controller { name := string(topicKey) + "_controller" - return &mergeConflictSignalController{ + return &landConflictSignalController{ logger: logger.Named(name), metricsScope: scope.SubScope(name), stores: stores, @@ -63,16 +63,16 @@ func NewDLQMergeConflictSignalController( } } -// Process reconciles a single DLQ delivery for the mergeconflictsignal topic. -func (c *mergeConflictSignalController) Process(ctx context.Context, delivery consumer.Delivery) error { +// Process reconciles a single DLQ delivery for the landconflictsignal topic. +func (c *landConflictSignalController) Process(ctx context.Context, delivery consumer.Delivery) error { const opName = "process" msg := delivery.Message() - result := &runwaymq.MergeResult{} + result := &runwaymq.LandResult{} if err := runwaymq.Unmarshal(msg.Payload, result); err != nil { metrics.NamedCounter(c.metricsScope, opName, "deserialize_errors", 1) - return fmt.Errorf("failed to decode merge conflict check result from dlq payload: %w", err) + return fmt.Errorf("failed to decode land conflict check result from dlq payload: %w", err) } store, err := c.stores.For(storage.Config{QueueName: result.GetQueueName()}) @@ -101,16 +101,16 @@ func (c *mergeConflictSignalController) Process(ctx context.Context, delivery co } // Name returns the controller name for logging and metrics. -func (c *mergeConflictSignalController) Name() string { +func (c *landConflictSignalController) Name() string { return string(c.topicKey) } // TopicKey returns the topic key this controller subscribes to. -func (c *mergeConflictSignalController) TopicKey() consumer.TopicKey { +func (c *landConflictSignalController) TopicKey() consumer.TopicKey { return c.topicKey } // ConsumerGroup returns the consumer group for offset tracking. -func (c *mergeConflictSignalController) ConsumerGroup() string { +func (c *landConflictSignalController) ConsumerGroup() string { return c.consumerGroup } diff --git a/submitqueue/orchestrator/controller/dlq/mergeconflictsignal_test.go b/submitqueue/orchestrator/controller/dlq/landconflictsignal_test.go similarity index 64% rename from submitqueue/orchestrator/controller/dlq/mergeconflictsignal_test.go rename to submitqueue/orchestrator/controller/dlq/landconflictsignal_test.go index bf23a1d6a..e177e554a 100644 --- a/submitqueue/orchestrator/controller/dlq/mergeconflictsignal_test.go +++ b/submitqueue/orchestrator/controller/dlq/landconflictsignal_test.go @@ -29,19 +29,19 @@ import ( "go.uber.org/zap/zaptest" ) -func TestDLQMergeConflictSignalController_InterfaceAndAccessors(t *testing.T) { +func TestDLQLandConflictSignalController_InterfaceAndAccessors(t *testing.T) { ctrl := gomock.NewController(t) store := storagemock.NewMockStorage(ctrl) store.EXPECT().GetQueueBatchStateStore().Return(newQueueBatchStateStore(ctrl)).AnyTimes() - c := NewDLQMergeConflictSignalController(zaptest.NewLogger(t).Sugar(), testScope(), staticStorageFactory{store: store}, consumer.TopicRegistry{}, TopicKey(runwaymq.TopicKeyMergeConflictCheckSignal), "orchestrator-mergeconflictsignal-dlq") + c := NewDLQLandConflictSignalController(zaptest.NewLogger(t).Sugar(), testScope(), staticStorageFactory{store: store}, consumer.TopicRegistry{}, TopicKey(runwaymq.TopicKeyLandConflictCheckSignal), "orchestrator-landconflictsignal-dlq") - assert.Equal(t, "merge-conflict-check-signal_dlq", c.Name()) - assert.Equal(t, consumer.TopicKey("merge-conflict-check-signal_dlq"), c.TopicKey()) - assert.Equal(t, "orchestrator-mergeconflictsignal-dlq", c.ConsumerGroup()) + assert.Equal(t, "land-conflict-check-signal_dlq", c.Name()) + assert.Equal(t, consumer.TopicKey("land-conflict-check-signal_dlq"), c.TopicKey()) + assert.Equal(t, "orchestrator-landconflictsignal-dlq", c.ConsumerGroup()) } -func TestDLQMergeConflictSignalController_Process_ReconcilesRequest(t *testing.T) { +func TestDLQLandConflictSignalController_Process_ReconcilesRequest(t *testing.T) { ctrl := gomock.NewController(t) requestStore := storagemock.NewMockRequestStore(ctrl) @@ -59,21 +59,21 @@ func TestDLQMergeConflictSignalController_Process_ReconcilesRequest(t *testing.T store.EXPECT().GetQueueBatchStateStore().Return(newQueueBatchStateStore(ctrl)).AnyTimes() store.EXPECT().GetRequestStore().Return(requestStore).AnyTimes() - c := NewDLQMergeConflictSignalController(zaptest.NewLogger(t).Sugar(), testScope(), staticStorageFactory{store: store}, registry, TopicKey(runwaymq.TopicKeyMergeConflictCheckSignal), "orchestrator-mergeconflictsignal-dlq") + c := NewDLQLandConflictSignalController(zaptest.NewLogger(t).Sugar(), testScope(), staticStorageFactory{store: store}, registry, TopicKey(runwaymq.TopicKeyLandConflictCheckSignal), "orchestrator-landconflictsignal-dlq") - payload, err := runwaymq.Marshal(&runwaymq.MergeResult{Id: "q/1", Outcome: runwaypb.Outcome_FAILED, Reason: "boom"}) + payload, err := runwaymq.Marshal(&runwaymq.LandResult{Id: "q/1", Outcome: runwaypb.Outcome_FAILED, Reason: "boom"}) require.NoError(t, err) delivery := newMockDelivery(ctrl, payload) require.NoError(t, c.Process(context.Background(), delivery)) } -func TestDLQMergeConflictSignalController_Process_MalformedPayloadFails(t *testing.T) { +func TestDLQLandConflictSignalController_Process_MalformedPayloadFails(t *testing.T) { ctrl := gomock.NewController(t) store := storagemock.NewMockStorage(ctrl) store.EXPECT().GetQueueBatchStateStore().Return(newQueueBatchStateStore(ctrl)).AnyTimes() - c := NewDLQMergeConflictSignalController(zaptest.NewLogger(t).Sugar(), testScope(), staticStorageFactory{store: store}, consumer.TopicRegistry{}, TopicKey(runwaymq.TopicKeyMergeConflictCheckSignal), "orchestrator-mergeconflictsignal-dlq") + c := NewDLQLandConflictSignalController(zaptest.NewLogger(t).Sugar(), testScope(), staticStorageFactory{store: store}, consumer.TopicRegistry{}, TopicKey(runwaymq.TopicKeyLandConflictCheckSignal), "orchestrator-landconflictsignal-dlq") delivery := newMockDelivery(ctrl, []byte("garbage")) require.Error(t, c.Process(context.Background(), delivery)) diff --git a/submitqueue/orchestrator/controller/dlq/mergesignal.go b/submitqueue/orchestrator/controller/dlq/landsignal.go similarity index 76% rename from submitqueue/orchestrator/controller/dlq/mergesignal.go rename to submitqueue/orchestrator/controller/dlq/landsignal.go index 8eb3d9e4f..6a2f13321 100644 --- a/submitqueue/orchestrator/controller/dlq/mergesignal.go +++ b/submitqueue/orchestrator/controller/dlq/landsignal.go @@ -26,11 +26,11 @@ import ( "go.uber.org/zap" ) -// mergeSignalController is the DLQ reconciler for the mergesignal topic. Its -// payload carries a runway MergeResult whose id is the batch id echoed back, so +// landSignalController is the DLQ reconciler for the landsignal topic. Its +// payload carries a runway LandResult whose id is the batch id echoed back, so // reconciliation fails that batch directly via failBatch (which also fans out // to the member requests). -type mergeSignalController struct { +type landSignalController struct { logger *zap.SugaredLogger metricsScope tally.Scope stores storage.Factory @@ -39,11 +39,11 @@ type mergeSignalController struct { consumerGroup string } -// Verify mergeSignalController implements consumer.Controller at compile time. -var _ consumer.Controller = (*mergeSignalController)(nil) +// Verify landSignalController implements consumer.Controller at compile time. +var _ consumer.Controller = (*landSignalController)(nil) -// NewDLQMergeSignalController builds a DLQ controller for the mergesignal topic. -func NewDLQMergeSignalController( +// NewDLQLandSignalController builds a DLQ controller for the landsignal topic. +func NewDLQLandSignalController( logger *zap.SugaredLogger, scope tally.Scope, stores storage.Factory, @@ -52,7 +52,7 @@ func NewDLQMergeSignalController( consumerGroup string, ) consumer.Controller { name := string(topicKey) + "_controller" - return &mergeSignalController{ + return &landSignalController{ logger: logger.Named(name), metricsScope: scope.SubScope(name), stores: stores, @@ -62,16 +62,16 @@ func NewDLQMergeSignalController( } } -// Process reconciles a single DLQ delivery for the mergesignal topic. -func (c *mergeSignalController) Process(ctx context.Context, delivery consumer.Delivery) error { +// Process reconciles a single DLQ delivery for the landsignal topic. +func (c *landSignalController) Process(ctx context.Context, delivery consumer.Delivery) error { const opName = "process" msg := delivery.Message() - result := &runwaymq.MergeResult{} + result := &runwaymq.LandResult{} if err := runwaymq.Unmarshal(msg.Payload, result); err != nil { metrics.NamedCounter(c.metricsScope, opName, "deserialize_errors", 1) - return fmt.Errorf("failed to decode merge result from dlq payload: %w", err) + return fmt.Errorf("failed to decode land result from dlq payload: %w", err) } store, err := c.stores.For(storage.Config{QueueName: result.GetQueueName()}) @@ -100,16 +100,16 @@ func (c *mergeSignalController) Process(ctx context.Context, delivery consumer.D } // Name returns the controller name for logging and metrics. -func (c *mergeSignalController) Name() string { +func (c *landSignalController) Name() string { return string(c.topicKey) } // TopicKey returns the topic key this controller subscribes to. -func (c *mergeSignalController) TopicKey() consumer.TopicKey { +func (c *landSignalController) TopicKey() consumer.TopicKey { return c.topicKey } // ConsumerGroup returns the consumer group for offset tracking. -func (c *mergeSignalController) ConsumerGroup() string { +func (c *landSignalController) ConsumerGroup() string { return c.consumerGroup } diff --git a/submitqueue/orchestrator/controller/dlq/mergesignal_test.go b/submitqueue/orchestrator/controller/dlq/landsignal_test.go similarity index 70% rename from submitqueue/orchestrator/controller/dlq/mergesignal_test.go rename to submitqueue/orchestrator/controller/dlq/landsignal_test.go index 69a122ae4..a038d36b3 100644 --- a/submitqueue/orchestrator/controller/dlq/mergesignal_test.go +++ b/submitqueue/orchestrator/controller/dlq/landsignal_test.go @@ -29,27 +29,27 @@ import ( "go.uber.org/zap/zaptest" ) -func TestDLQMergeSignalController_InterfaceAndAccessors(t *testing.T) { +func TestDLQLandSignalController_InterfaceAndAccessors(t *testing.T) { ctrl := gomock.NewController(t) store := storagemock.NewMockStorage(ctrl) store.EXPECT().GetQueueBatchStateStore().Return(newQueueBatchStateStore(ctrl)).AnyTimes() - c := NewDLQMergeSignalController(zaptest.NewLogger(t).Sugar(), testScope(), staticStorageFactory{store: store}, consumer.TopicRegistry{}, TopicKey(runwaymq.TopicKeyMergeSignal), "orchestrator-mergesignal-dlq") + c := NewDLQLandSignalController(zaptest.NewLogger(t).Sugar(), testScope(), staticStorageFactory{store: store}, consumer.TopicRegistry{}, TopicKey(runwaymq.TopicKeyLandSignal), "orchestrator-landsignal-dlq") - assert.Equal(t, "merge-signal_dlq", c.Name()) - assert.Equal(t, consumer.TopicKey("merge-signal_dlq"), c.TopicKey()) - assert.Equal(t, "orchestrator-mergesignal-dlq", c.ConsumerGroup()) + assert.Equal(t, "land-signal_dlq", c.Name()) + assert.Equal(t, consumer.TopicKey("land-signal_dlq"), c.TopicKey()) + assert.Equal(t, "orchestrator-landsignal-dlq", c.ConsumerGroup()) } // The payload id is the batch id echoed back, so reconciliation fails the batch // and fans out to its member requests via failBatch. -func TestDLQMergeSignalController_Process_ReconcilesBatch(t *testing.T) { +func TestDLQLandSignalController_Process_ReconcilesBatch(t *testing.T) { ctrl := gomock.NewController(t) batchStore := storagemock.NewMockBatchStore(ctrl) batch := entity.Batch{ ID: "q/batch/1", Queue: "q", Contains: []string{"q/1"}, - State: entity.BatchStateMerging, Version: 2, + State: entity.BatchStateLanding, Version: 2, } batchStore.EXPECT().Get(gomock.Any(), "q/batch/1").Return(batch, nil) batchStore.EXPECT().Update(gomock.Any(), batchWithState(batch, entity.BatchStateFailed), int32(2), int32(3)).Return(nil) @@ -70,21 +70,21 @@ func TestDLQMergeSignalController_Process_ReconcilesBatch(t *testing.T) { store.EXPECT().GetBatchStore().Return(batchStore).AnyTimes() store.EXPECT().GetRequestStore().Return(requestStore).AnyTimes() - c := NewDLQMergeSignalController(zaptest.NewLogger(t).Sugar(), testScope(), staticStorageFactory{store: store}, registry, TopicKey(runwaymq.TopicKeyMergeSignal), "orchestrator-mergesignal-dlq") + c := NewDLQLandSignalController(zaptest.NewLogger(t).Sugar(), testScope(), staticStorageFactory{store: store}, registry, TopicKey(runwaymq.TopicKeyLandSignal), "orchestrator-landsignal-dlq") - payload, err := runwaymq.Marshal(&runwaymq.MergeResult{Id: "q/batch/1", Outcome: runwaypb.Outcome_FAILED, Reason: "boom"}) + payload, err := runwaymq.Marshal(&runwaymq.LandResult{Id: "q/batch/1", Outcome: runwaypb.Outcome_FAILED, Reason: "boom"}) require.NoError(t, err) delivery := newMockDelivery(ctrl, payload) require.NoError(t, c.Process(context.Background(), delivery)) } -func TestDLQMergeSignalController_Process_MalformedPayloadFails(t *testing.T) { +func TestDLQLandSignalController_Process_MalformedPayloadFails(t *testing.T) { ctrl := gomock.NewController(t) store := storagemock.NewMockStorage(ctrl) store.EXPECT().GetQueueBatchStateStore().Return(newQueueBatchStateStore(ctrl)).AnyTimes() - c := NewDLQMergeSignalController(zaptest.NewLogger(t).Sugar(), testScope(), staticStorageFactory{store: store}, consumer.TopicRegistry{}, TopicKey(runwaymq.TopicKeyMergeSignal), "orchestrator-mergesignal-dlq") + c := NewDLQLandSignalController(zaptest.NewLogger(t).Sugar(), testScope(), staticStorageFactory{store: store}, consumer.TopicRegistry{}, TopicKey(runwaymq.TopicKeyLandSignal), "orchestrator-landsignal-dlq") delivery := newMockDelivery(ctrl, []byte("garbage")) require.Error(t, c.Process(context.Background(), delivery)) diff --git a/submitqueue/orchestrator/controller/dlq/request_test.go b/submitqueue/orchestrator/controller/dlq/request_test.go index adc34d064..f25269d21 100644 --- a/submitqueue/orchestrator/controller/dlq/request_test.go +++ b/submitqueue/orchestrator/controller/dlq/request_test.go @@ -213,7 +213,7 @@ func TestDLQRequestController_Process_SkipsRequestOwnedByLiveBatch(t *testing.T) for _, state := range []entity.BatchState{ entity.BatchStateCreated, entity.BatchStateSpeculating, - entity.BatchStateMerging, + entity.BatchStateLanding, entity.BatchStateCancelling, } { t.Run(string(state), func(t *testing.T) { diff --git a/submitqueue/orchestrator/controller/dlq/speculate.go b/submitqueue/orchestrator/controller/dlq/speculate.go index 7e6fe5876..9d6403a32 100644 --- a/submitqueue/orchestrator/controller/dlq/speculate.go +++ b/submitqueue/orchestrator/controller/dlq/speculate.go @@ -42,7 +42,7 @@ import ( // So this reconciler reads the failure's subjects and acts on those. It also // republishes to speculate afterwards, because a dead letter here consumes an // edge the queue needed. Speculation is driven only by messages; a batch -// admitted to Speculating produces no build to signal and no merge to conclude, +// admitted to Speculating produces no build to signal and no land to conclude, // so once the message that would have funded it is gone, nothing is left to // look at it again. Without the republish the failure of one batch silently // strands every other batch in the queue. diff --git a/submitqueue/orchestrator/controller/merge/BUILD.bazel b/submitqueue/orchestrator/controller/land/BUILD.bazel similarity index 84% rename from submitqueue/orchestrator/controller/merge/BUILD.bazel rename to submitqueue/orchestrator/controller/land/BUILD.bazel index a0eb8359a..c46623d12 100644 --- a/submitqueue/orchestrator/controller/merge/BUILD.bazel +++ b/submitqueue/orchestrator/controller/land/BUILD.bazel @@ -2,14 +2,14 @@ load("@rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", - srcs = ["merge.go"], - importpath = "github.com/uber/submitqueue/submitqueue/orchestrator/controller/merge", + srcs = ["land.go"], + importpath = "github.com/uber/submitqueue/submitqueue/orchestrator/controller/land", visibility = ["//visibility:public"], deps = [ "//api/base/change/protopb:go_default_library", - "//api/base/mergestrategy/protopb:go_default_library", + "//api/base/landstrategy/protopb:go_default_library", "//api/runway/messagequeue:go_default_library", - "//platform/base/mergestrategy:go_default_library", + "//platform/base/landstrategy:go_default_library", "//platform/consumer:go_default_library", "//platform/metrics:go_default_library", "//platform/publish:go_default_library", @@ -23,13 +23,13 @@ go_library( go_test( name = "go_default_test", - srcs = ["merge_test.go"], + srcs = ["land_test.go"], embed = [":go_default_library"], deps = [ - "//api/base/mergestrategy/protopb:go_default_library", + "//api/base/landstrategy/protopb:go_default_library", "//api/runway/messagequeue:go_default_library", "//platform/base/change:go_default_library", - "//platform/base/mergestrategy:go_default_library", + "//platform/base/landstrategy:go_default_library", "//platform/base/messagequeue:go_default_library", "//platform/consumer:go_default_library", "//platform/consumer/mock:go_default_library", diff --git a/submitqueue/orchestrator/controller/merge/merge.go b/submitqueue/orchestrator/controller/land/land.go similarity index 71% rename from submitqueue/orchestrator/controller/merge/merge.go rename to submitqueue/orchestrator/controller/land/land.go index a9657457b..e8da3435c 100644 --- a/submitqueue/orchestrator/controller/merge/merge.go +++ b/submitqueue/orchestrator/controller/land/land.go @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package merge implements the trigger stage for the asynchronous merge. It -// consumes a batch ready to land, builds the full merge request from the +// Package land implements the trigger stage for the asynchronous land. It +// consumes a batch ready to land, builds the full land request from the // batch's member requests (one step per request, in Contains order), and -// publishes it to runway's merge queue using the batch id as the client-owned -// correlation id. Runway performs the merge out of process and publishes the -// result to the merge-signal queue, which the mergesignal stage consumes and +// publishes it to runway's land queue using the batch id as the client-owned +// correlation id. Runway performs the land out of process and publishes the +// result to the land-signal queue, which the landsignal stage consumes and // correlates back to the batch by that id. -package merge +package land import ( "context" @@ -29,9 +29,9 @@ import ( "go.uber.org/zap" changepb "github.com/uber/submitqueue/api/base/change/protopb" - strategypb "github.com/uber/submitqueue/api/base/mergestrategy/protopb" + strategypb "github.com/uber/submitqueue/api/base/landstrategy/protopb" runwaymq "github.com/uber/submitqueue/api/runway/messagequeue" - "github.com/uber/submitqueue/platform/base/mergestrategy" + "github.com/uber/submitqueue/platform/base/landstrategy" "github.com/uber/submitqueue/platform/consumer" "github.com/uber/submitqueue/platform/metrics" "github.com/uber/submitqueue/platform/publish" @@ -40,13 +40,13 @@ import ( "github.com/uber/submitqueue/submitqueue/extension/storage" ) -// Controller handles merge queue messages. Implements consumer.Controller. +// Controller handles land queue messages. Implements consumer.Controller. // -// It loads the batch and its member requests, assembles the full merge request +// It loads the batch and its member requests, assembles the full land request // (one step per member request, in Contains order, each carrying that request's -// change and land strategy), and publishes it to runway's merge queue. Runway -// performs the merge out of process and returns the result on the merge-signal -// queue; the mergesignal stage consumes it and transitions the batch. This +// change and land strategy), and publishes it to runway's land queue. Runway +// performs the land out of process and returns the result on the land-signal +// queue; the landsignal stage consumes it and transitions the batch. This // controller therefore performs no state transition itself. type Controller struct { logger *zap.SugaredLogger @@ -61,9 +61,9 @@ type Controller struct { // Verify Controller implements consumer.Controller interface at compile time. var _ consumer.Controller = (*Controller)(nil) -// NewController creates a new merge controller for the orchestrator. -// runwayTopicKey is the runway-owned topic this controller publishes merge -// requests to (TopicKeyMerge). +// NewController creates a new land controller for the orchestrator. +// runwayTopicKey is the runway-owned topic this controller publishes land +// requests to (TopicKeyLand). func NewController( logger *zap.SugaredLogger, scope tally.Scope, @@ -74,8 +74,8 @@ func NewController( consumerGroup string, ) *Controller { return &Controller{ - logger: logger.Named("merge_controller"), - metricsScope: scope.SubScope("merge_controller"), + logger: logger.Named("land_controller"), + metricsScope: scope.SubScope("land_controller"), stores: stores, registry: registry, runwayTopicKey: runwayTopicKey, @@ -84,12 +84,12 @@ func NewController( } } -// Process publishes the full merge request to runway. Returns nil to ack +// Process publishes the full land request to runway. Returns nil to ack // (success), or error to nack/reject. // // Error classification: deserialize and storage failures are non-retryable // (reject to DLQ). The publish to runway is retryable — it is the hand-off that -// keeps the merge alive, so a transient enqueue blip should replay rather than +// keeps the land alive, so a transient enqueue blip should replay rather than // strand the batch. func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) error { const opName = "process" @@ -122,7 +122,7 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er return fmt.Errorf("payload queue %q does not match queue %q of batch %s", bid.Queue, batch.Queue, batch.ID) } - c.logger.Infow("received merge event", + c.logger.Infow("received land event", "batch_id", batch.ID, "queue", batch.Queue, "state", string(batch.State), @@ -131,27 +131,27 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er "partition_key", msg.PartitionKey, ) - // Short-circuit halted batches (terminal or cancelling): no merge should be + // Short-circuit halted batches (terminal or cancelling): no land should be // kicked off for a batch that will not proceed. Unlike the old synchronous - // merge there is no terminal re-fan-out here — the mergesignal stage owns the + // land there is no terminal re-fan-out here — the landsignal stage owns the // state transition and fan-out once runway's result returns, so a redelivery // at this stage simply acks. if entity.IsBatchStateHalted(batch.State) { metrics.NamedCounter(c.metricsScope, opName, "skipped_halted", 1) - c.logger.Infow("skipping merge for halted batch", + c.logger.Infow("skipping land for halted batch", "batch_id", batch.ID, "state", string(batch.State), ) return nil } - // Build the full payload runway needs to perform the merge. The batch id is + // Build the full payload runway needs to land the batch. The batch id is // the client-owned correlation id, so a redelivery republishes the same id // and runway dedupes on it; the result is matched straight back to the batch. - req, err := c.buildMergeRequest(ctx, store, batch) + req, err := c.buildLandRequest(ctx, store, batch) if err != nil { metrics.NamedCounter(c.metricsScope, opName, "storage_errors", 1) - return fmt.Errorf("failed to build merge request for batch %s: %w", batch.ID, err) + return fmt.Errorf("failed to build land request for batch %s: %w", batch.ID, err) } // Report that the members are landing before the request goes out, so a @@ -166,10 +166,10 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er if err := c.publish(ctx, c.runwayTopicKey, req, batch.Queue); err != nil { metrics.NamedCounter(c.metricsScope, opName, "publish_errors", 1) - return fmt.Errorf("failed to publish to runway merge: %w", err) + return fmt.Errorf("failed to publish to runway land: %w", err) } - c.logger.Infow("published merge to runway", + c.logger.Infow("published land to runway", "batch_id", batch.ID, "steps", len(req.Steps), "topic_key", c.runwayTopicKey, @@ -178,55 +178,55 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er return nil // Success - message will be acked } -// buildMergeRequest loads the batch's member requests and assembles the runway -// merge request: one MergeStep per request, in Contains order, attributed by +// buildLandRequest loads the batch's member requests and assembles the runway +// land request: one LandStep per request, in Contains order, attributed by // request id and carrying that request's change and land strategy. -func (c *Controller) buildMergeRequest(ctx context.Context, store storage.Storage, batch entity.Batch) (*runwaymq.MergeRequest, error) { - steps := make([]*runwaymq.MergeStep, 0, len(batch.Contains)) +func (c *Controller) buildLandRequest(ctx context.Context, store storage.Storage, batch entity.Batch) (*runwaymq.LandRequest, error) { + steps := make([]*runwaymq.LandStep, 0, len(batch.Contains)) for _, requestID := range batch.Contains { request, err := store.GetRequestStore().Get(ctx, requestID) if err != nil { return nil, fmt.Errorf("failed to get request %s: %w", requestID, err) } - steps = append(steps, &runwaymq.MergeStep{ + steps = append(steps, &runwaymq.LandStep{ StepId: request.ID, Change: &changepb.Change{Uris: request.Change.URIs}, Strategy: toProtoStrategy(request.LandStrategy), }) } - return &runwaymq.MergeRequest{ + return &runwaymq.LandRequest{ Id: batch.ID, QueueName: batch.Queue, Steps: steps, }, nil } -// toProtoStrategy maps the shared mergestrategy.MergeStrategy entity to the +// toProtoStrategy maps the shared landstrategy.Strategy entity to the // proto Strategy enum carried on the wire. An unknown strategy maps to DEFAULT, // letting runway apply the queue's configured default. -func toProtoStrategy(s mergestrategy.MergeStrategy) strategypb.Strategy { +func toProtoStrategy(s landstrategy.Strategy) strategypb.Strategy { switch s { - case mergestrategy.MergeStrategyRebase: + case landstrategy.StrategyRebase: return strategypb.Strategy_REBASE - case mergestrategy.MergeStrategySquashRebase: + case landstrategy.StrategySquashRebase: return strategypb.Strategy_SQUASH_REBASE - case mergestrategy.MergeStrategyMerge: + case landstrategy.StrategyMerge: return strategypb.Strategy_MERGE default: return strategypb.Strategy_DEFAULT } } -// publish serializes the runway merge request and publishes it to the given +// publish serializes the runway land request and publishes it to the given // topic key, partitioned by queue. // -// The correlation ID is the message ID with no cause: a batch is asked to merge +// The correlation ID is the message ID with no cause: a batch is asked to land // once, so a redelivery that re-asks is meant to dedup rather than have Runway -// merge the same batch twice. -func (c *Controller) publish(ctx context.Context, key consumer.TopicKey, req *runwaymq.MergeRequest, partitionKey string) error { +// land the same batch twice. +func (c *Controller) publish(ctx context.Context, key consumer.TopicKey, req *runwaymq.LandRequest, partitionKey string) error { payload, err := runwaymq.Marshal(req) if err != nil { - return fmt.Errorf("failed to serialize merge request: %w", err) + return fmt.Errorf("failed to serialize land request: %w", err) } if err := publish.Message(ctx, c.registry, key, publish.IntentID(req.GetId()), payload, partitionKey); err != nil { @@ -238,7 +238,7 @@ func (c *Controller) publish(ctx context.Context, key consumer.TopicKey, req *ru // Name returns the controller name for logging and metrics. func (c *Controller) Name() string { - return "merge" + return "land" } // TopicKey returns the topic key this controller subscribes to. diff --git a/submitqueue/orchestrator/controller/merge/merge_test.go b/submitqueue/orchestrator/controller/land/land_test.go similarity index 87% rename from submitqueue/orchestrator/controller/merge/merge_test.go rename to submitqueue/orchestrator/controller/land/land_test.go index 90c63cd6c..55eac2987 100644 --- a/submitqueue/orchestrator/controller/merge/merge_test.go +++ b/submitqueue/orchestrator/controller/land/land_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package merge +package land import ( "context" @@ -25,10 +25,10 @@ import ( "go.uber.org/mock/gomock" "go.uber.org/zap/zaptest" - strategypb "github.com/uber/submitqueue/api/base/mergestrategy/protopb" + strategypb "github.com/uber/submitqueue/api/base/landstrategy/protopb" runwaymq "github.com/uber/submitqueue/api/runway/messagequeue" "github.com/uber/submitqueue/platform/base/change" - "github.com/uber/submitqueue/platform/base/mergestrategy" + "github.com/uber/submitqueue/platform/base/landstrategy" entityqueue "github.com/uber/submitqueue/platform/base/messagequeue" "github.com/uber/submitqueue/platform/consumer" consumermock "github.com/uber/submitqueue/platform/consumer/mock" @@ -67,15 +67,15 @@ func newController(t *testing.T, store *storagemock.MockStorage, registry consum tally.NoopScope, staticStorageFactory{store: store}, registry, - runwaymq.TopicKeyMerge, - topickey.TopicKeyMerge, - "orchestrator-merge", + runwaymq.TopicKeyLand, + topickey.TopicKeyLand, + "orchestrator-land", ) } // publishes records what a controller run published, in order and by topic. The // controller writes to two topics now — the request-log fan-out and the runway -// merge request — so tests need to tell them apart and to see which came first. +// land request — so tests need to tell them apart and to see which came first. type publishes struct { inOrder []string byTopic map[string][]entityqueue.Message @@ -103,7 +103,7 @@ func newRegistry(t *testing.T, ctrl *gomock.Controller, failTopic string) (consu q.EXPECT().Publisher().Return(pub).AnyTimes() registry, err := consumer.NewTopicRegistry([]consumer.TopicConfig{ - {Key: runwaymq.TopicKeyMerge, Name: "runway-merge", Queue: q}, + {Key: runwaymq.TopicKeyLand, Name: "runway-land", Queue: q}, {Key: topickey.TopicKeyLog, Name: "log", Queue: q}, }) require.NoError(t, err) @@ -115,16 +115,16 @@ func TestNewController(t *testing.T) { store := storagemock.NewMockStorage(ctrl) q := queuemock.NewMockQueue(ctrl) registry, err := consumer.NewTopicRegistry( - []consumer.TopicConfig{{Key: runwaymq.TopicKeyMerge, Name: "runway-merge", Queue: q}}, + []consumer.TopicConfig{{Key: runwaymq.TopicKeyLand, Name: "runway-land", Queue: q}}, ) require.NoError(t, err) c := newController(t, store, registry) require.NotNil(t, c) - assert.Equal(t, topickey.TopicKeyMerge, c.TopicKey()) - assert.Equal(t, "orchestrator-merge", c.ConsumerGroup()) - assert.Equal(t, "merge", c.Name()) + assert.Equal(t, topickey.TopicKeyLand, c.TopicKey()) + assert.Equal(t, "orchestrator-land", c.ConsumerGroup()) + assert.Equal(t, "land", c.Name()) var _ consumer.Controller = c } @@ -136,19 +136,19 @@ func TestProcess_PublishesFullPayloadToRunway(t *testing.T) { ID: "test-queue/1", Queue: "test-queue", Change: change.Change{URIs: []string{"github://github.example.com/uber/repo/pull/1/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}, - LandStrategy: mergestrategy.MergeStrategySquashRebase, + LandStrategy: landstrategy.StrategySquashRebase, } req2 := entity.Request{ ID: "test-queue/2", Queue: "test-queue", Change: change.Change{URIs: []string{"github://github.example.com/uber/repo/pull/2/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}}, - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, } batch := entity.Batch{ ID: batchID, Queue: "test-queue", Contains: []string{req1.ID, req2.ID}, - State: entity.BatchStateMerging, + State: entity.BatchStateLanding, Version: 4, } @@ -167,7 +167,7 @@ func TestProcess_PublishesFullPayloadToRunway(t *testing.T) { pub := queuemock.NewMockPublisher(ctrl) pub.EXPECT().Publish(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( func(_ context.Context, topic string, msg entityqueue.Message) error { - if topic != "runway-merge" { + if topic != "runway-land" { return nil } gotTopic = topic @@ -178,7 +178,7 @@ func TestProcess_PublishesFullPayloadToRunway(t *testing.T) { q := queuemock.NewMockQueue(ctrl) q.EXPECT().Publisher().Return(pub).AnyTimes() registry, err := consumer.NewTopicRegistry([]consumer.TopicConfig{ - {Key: runwaymq.TopicKeyMerge, Name: "runway-merge", Queue: q}, + {Key: runwaymq.TopicKeyLand, Name: "runway-land", Queue: q}, {Key: topickey.TopicKeyLog, Name: "log", Queue: q}, }) require.NoError(t, err) @@ -187,8 +187,8 @@ func TestProcess_PublishesFullPayloadToRunway(t *testing.T) { require.NoError(t, c.Process(context.Background(), newDelivery(t, ctrl, batchID, batch.Queue))) // Full payload published to runway, keyed by the batch id (the correlation id). - assert.Equal(t, "runway-merge", gotTopic) - got := &runwaymq.MergeRequest{} + assert.Equal(t, "runway-land", gotTopic) + got := &runwaymq.LandRequest{} require.NoError(t, runwaymq.Unmarshal(gotPayload, got)) assert.Equal(t, batch.ID, got.Id) assert.Equal(t, batch.Queue, got.QueueName) @@ -221,7 +221,7 @@ func TestProcess_HaltedBatchSkips(t *testing.T) { batchStore.EXPECT().Get(gomock.Any(), batchID).Return(batch, nil) // No request-store reads and no publish for a halted batch: the - // members are told nothing and runway is not asked to merge. + // members are told nothing and runway is not asked to land. store := storagemock.NewMockStorage(ctrl) store.EXPECT().GetBatchStore().Return(batchStore).AnyTimes() @@ -236,19 +236,19 @@ func TestProcess_HaltedBatchSkips(t *testing.T) { // TestProcess_ReportsLandingBeforeDispatch covers the request-log half of this // stage: every member of the batch is told it is landing, and it is told before -// the merge request goes out. The ordering is what makes a lost log entry +// the land request goes out. The ordering is what makes a lost log entry // recoverable — a failure here nacks with nothing announced to runway, and the // redelivery re-publishes under the same occurrence, which the queue dedupes. func TestProcess_ReportsLandingBeforeDispatch(t *testing.T) { ctrl := gomock.NewController(t) const batchID = "test-queue/batch/3" - req1 := entity.Request{ID: "test-queue/1", Queue: "test-queue", LandStrategy: mergestrategy.MergeStrategyRebase} - req2 := entity.Request{ID: "test-queue/2", Queue: "test-queue", LandStrategy: mergestrategy.MergeStrategyRebase} + req1 := entity.Request{ID: "test-queue/1", Queue: "test-queue", LandStrategy: landstrategy.StrategyRebase} + req2 := entity.Request{ID: "test-queue/2", Queue: "test-queue", LandStrategy: landstrategy.StrategyRebase} batch := entity.Batch{ ID: batchID, Queue: "test-queue", Contains: []string{req1.ID, req2.ID}, - State: entity.BatchStateMerging, Version: 2, + State: entity.BatchStateLanding, Version: 2, } batchStore := storagemock.NewMockBatchStore(ctrl) @@ -266,7 +266,7 @@ func TestProcess_ReportsLandingBeforeDispatch(t *testing.T) { require.NoError(t, c.Process(context.Background(), newDelivery(t, ctrl, batchID, batch.Queue))) // One entry per member, then the dispatch. - assert.Equal(t, []string{"log", "log", "runway-merge"}, rec.inOrder) + assert.Equal(t, []string{"log", "log", "runway-land"}, rec.inOrder) logs := rec.byTopic["log"] require.Len(t, logs, 2) @@ -288,7 +288,7 @@ func TestProcess_PublishFailureReturnsError(t *testing.T) { name string failTopic string }{ - {name: "runway dispatch fails", failTopic: "runway-merge"}, + {name: "runway dispatch fails", failTopic: "runway-land"}, {name: "request log fails", failTopic: "log"}, } @@ -297,8 +297,8 @@ func TestProcess_PublishFailureReturnsError(t *testing.T) { ctrl := gomock.NewController(t) const batchID = "test-queue/batch/2" - req := entity.Request{ID: "test-queue/1", Queue: "test-queue", LandStrategy: mergestrategy.MergeStrategyRebase} - batch := entity.Batch{ID: batchID, Queue: "test-queue", Contains: []string{req.ID}, State: entity.BatchStateMerging, Version: 1} + req := entity.Request{ID: "test-queue/1", Queue: "test-queue", LandStrategy: landstrategy.StrategyRebase} + batch := entity.Batch{ID: batchID, Queue: "test-queue", Contains: []string{req.ID}, State: entity.BatchStateLanding, Version: 1} batchStore := storagemock.NewMockBatchStore(ctrl) batchStore.EXPECT().Get(gomock.Any(), batchID).Return(batch, nil) @@ -315,9 +315,9 @@ func TestProcess_PublishFailureReturnsError(t *testing.T) { require.Error(t, c.Process(context.Background(), newDelivery(t, ctrl, batchID, batch.Queue))) // A failed log publish must stop the run before runway hears about - // the merge, so the redelivery can repair the log entry. + // the land, so the redelivery can repair the log entry. if tt.failTopic == "log" { - assert.Empty(t, rec.byTopic["runway-merge"]) + assert.Empty(t, rec.byTopic["runway-land"]) } }) } @@ -336,7 +336,7 @@ func TestProcess_BatchStoreGetFailureNotRetryable(t *testing.T) { q := queuemock.NewMockQueue(ctrl) registry, err := consumer.NewTopicRegistry( - []consumer.TopicConfig{{Key: runwaymq.TopicKeyMerge, Name: "runway-merge", Queue: q}}, + []consumer.TopicConfig{{Key: runwaymq.TopicKeyLand, Name: "runway-land", Queue: q}}, ) require.NoError(t, err) diff --git a/submitqueue/orchestrator/controller/mergeconflictsignal/BUILD.bazel b/submitqueue/orchestrator/controller/landconflictsignal/BUILD.bazel similarity index 93% rename from submitqueue/orchestrator/controller/mergeconflictsignal/BUILD.bazel rename to submitqueue/orchestrator/controller/landconflictsignal/BUILD.bazel index 94f0c2827..2a7edbab5 100644 --- a/submitqueue/orchestrator/controller/mergeconflictsignal/BUILD.bazel +++ b/submitqueue/orchestrator/controller/landconflictsignal/BUILD.bazel @@ -2,8 +2,8 @@ load("@rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", - srcs = ["mergeconflictsignal.go"], - importpath = "github.com/uber/submitqueue/submitqueue/orchestrator/controller/mergeconflictsignal", + srcs = ["landconflictsignal.go"], + importpath = "github.com/uber/submitqueue/submitqueue/orchestrator/controller/landconflictsignal", visibility = ["//visibility:public"], deps = [ "//api/runway/messagequeue:go_default_library", @@ -22,7 +22,7 @@ go_library( go_test( name = "go_default_test", - srcs = ["mergeconflictsignal_test.go"], + srcs = ["landconflictsignal_test.go"], embed = [":go_default_library"], deps = [ "//api/runway/messagequeue:go_default_library", diff --git a/submitqueue/orchestrator/controller/mergeconflictsignal/mergeconflictsignal.go b/submitqueue/orchestrator/controller/landconflictsignal/landconflictsignal.go similarity index 87% rename from submitqueue/orchestrator/controller/mergeconflictsignal/mergeconflictsignal.go rename to submitqueue/orchestrator/controller/landconflictsignal/landconflictsignal.go index dbfb69b5c..dac3dfba0 100644 --- a/submitqueue/orchestrator/controller/mergeconflictsignal/mergeconflictsignal.go +++ b/submitqueue/orchestrator/controller/landconflictsignal/landconflictsignal.go @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package mergeconflictsignal consumes merge-conflict check results from runway's +// Package landconflictsignal consumes land-conflict check results from runway's // signal queue, correlates them to the request by the echoed id, and either -// advances the request to the batch stage (mergeable) or fails it (conflicted). +// advances the request to the batch stage (landable) or fails it (conflicted). // Unlike buildsignal it is purely result-driven — runway pushes the result, so // there is no poll loop or self-reschedule. -package mergeconflictsignal +package landconflictsignal import ( "context" @@ -36,7 +36,7 @@ import ( "go.uber.org/zap" ) -// Controller handles mergeconflictsignal queue messages. Implements consumer.Controller. +// Controller handles landconflictsignal queue messages. Implements consumer.Controller. type Controller struct { logger *zap.SugaredLogger metricsScope tally.Scope @@ -49,7 +49,7 @@ type Controller struct { // Verify Controller implements consumer.Controller interface at compile time. var _ consumer.Controller = (*Controller)(nil) -// NewController creates a new mergeconflictsignal controller for the orchestrator. +// NewController creates a new landconflictsignal controller for the orchestrator. func NewController( logger *zap.SugaredLogger, scope tally.Scope, @@ -59,8 +59,8 @@ func NewController( consumerGroup string, ) *Controller { return &Controller{ - logger: logger.Named("mergeconflictsignal_controller"), - metricsScope: scope.SubScope("mergeconflictsignal_controller"), + logger: logger.Named("landconflictsignal_controller"), + metricsScope: scope.SubScope("landconflictsignal_controller"), stores: stores, registry: registry, topicKey: topicKey, @@ -71,7 +71,7 @@ func NewController( // Process consumes a runway check result and advances or fails the request. // Returns nil to ack, or error to nack/reject. // -// A not-mergeable verdict is an expected outcome of the check, not a failure: +// A not-landable verdict is an expected outcome of the check, not a failure: // the request is driven to terminal Error inline and the message is acked. Only // infrastructure faults — deserialize, storage, the terminal transition, and the // batch publish — return an error and reject to the DLQ, where the request is @@ -83,10 +83,10 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er // The runway result carries full data (it crosses the service boundary). Its // id is the request id echoed back, so correlate straight to the request. - result := &runwaymq.MergeResult{} + result := &runwaymq.LandResult{} if err := runwaymq.Unmarshal(msg.Payload, result); err != nil { metrics.NamedCounter(c.metricsScope, opName, "deserialize_errors", 1) - return fmt.Errorf("failed to deserialize merge conflict check result: %w", err) + return fmt.Errorf("failed to deserialize land conflict check result: %w", err) } store, err := c.stores.For(storage.Config{QueueName: result.GetQueueName()}) @@ -102,9 +102,9 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er return fmt.Errorf("failed to get request %s: %w", result.Id, err) } - c.logger.Infow("received mergeconflict signal", + c.logger.Infow("received landconflict signal", "request_id", request.ID, - "mergeable", result.Outcome == runwaypb.Outcome_SUCCEEDED, + "landable", result.Outcome == runwaypb.Outcome_SUCCEEDED, "attempt", delivery.Attempt(), "partition_key", msg.PartitionKey, ) @@ -112,7 +112,7 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er // Short-circuit halted requests: the cancel path owns driving them terminal. if entity.IsRequestStateHalted(request.State) { metrics.NamedCounter(c.metricsScope, opName, "skipped_halted", 1) - c.logger.Infow("skipping mergeconflict signal for halted request", + c.logger.Infow("skipping landconflict signal for halted request", "request_id", request.ID, "state", string(request.State), ) @@ -120,8 +120,8 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er } if result.Outcome != runwaypb.Outcome_SUCCEEDED { - metrics.NamedCounter(c.metricsScope, opName, "not_mergeable", 1) - c.logger.Infow("request not mergeable", + metrics.NamedCounter(c.metricsScope, opName, "not_landable", 1) + c.logger.Infow("request not landable", "request_id", request.ID, "reason", result.Reason, ) @@ -132,7 +132,7 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er return nil } - // Advance the request to Validated now that the merge-conflict check passed. + // Advance the request to Validated now that the land-conflict check passed. newVersion := request.Version + 1 request.State = entity.RequestStateValidated if err := store.GetRequestStore().Update(ctx, request, request.Version, newVersion); err != nil { @@ -161,7 +161,7 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er } // failRequest drives the request to terminal RequestStateError and records the -// conflict reason on the request log. A not-mergeable verdict is an expected +// conflict reason on the request log. A not-landable verdict is an expected // terminal outcome of the check, so the request is concluded here directly. // // Idempotent under at-least-once delivery: a redelivery whose request is already @@ -216,7 +216,7 @@ func (c *Controller) publishRequestID(ctx context.Context, key consumer.TopicKey // Name returns the controller name for logging and metrics. func (c *Controller) Name() string { - return "mergeconflictsignal" + return "landconflictsignal" } // TopicKey returns the topic key this controller subscribes to. diff --git a/submitqueue/orchestrator/controller/mergeconflictsignal/mergeconflictsignal_test.go b/submitqueue/orchestrator/controller/landconflictsignal/landconflictsignal_test.go similarity index 90% rename from submitqueue/orchestrator/controller/mergeconflictsignal/mergeconflictsignal_test.go rename to submitqueue/orchestrator/controller/landconflictsignal/landconflictsignal_test.go index c71dd9fb7..92a706999 100644 --- a/submitqueue/orchestrator/controller/mergeconflictsignal/mergeconflictsignal_test.go +++ b/submitqueue/orchestrator/controller/landconflictsignal/landconflictsignal_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package mergeconflictsignal +package landconflictsignal import ( "context" @@ -47,7 +47,7 @@ func requestWithState(request entity.Request, state entity.RequestState) entity. return request } -func resultPayload(t *testing.T, res runwaymq.MergeResult) []byte { +func resultPayload(t *testing.T, res runwaymq.LandResult) []byte { payload, err := runwaymq.Marshal(&res) require.NoError(t, err) return payload @@ -65,7 +65,7 @@ const ( testQueue = "test-queue" ) -func TestProcess_MergeablePublishesToBatch(t *testing.T) { +func TestProcess_LandablePublishesToBatch(t *testing.T) { ctrl := gomock.NewController(t) reqStore := storagemock.NewMockRequestStore(ctrl) @@ -97,9 +97,9 @@ func TestProcess_MergeablePublishesToBatch(t *testing.T) { require.NoError(t, err) controller := NewController(zaptest.NewLogger(t).Sugar(), tally.NoopScope, staticStorageFactory{store: store}, registry, - runwaymq.TopicKeyMergeConflictCheckSignal, "orchestrator-mergeconflictsignal") + runwaymq.TopicKeyLandConflictCheckSignal, "orchestrator-landconflictsignal") - res := runwaymq.MergeResult{Id: testRequestID, Outcome: runwaypb.Outcome_SUCCEEDED} + res := runwaymq.LandResult{Id: testRequestID, Outcome: runwaypb.Outcome_SUCCEEDED} msg := entityqueue.NewMessage(testRequestID, resultPayload(t, res), testQueue, nil) require.NoError(t, controller.Process(context.Background(), newDelivery(ctrl, msg))) @@ -119,7 +119,7 @@ func TestProcess_MergeablePublishesToBatch(t *testing.T) { assert.Equal(t, testRequestID, rid.ID) } -func TestProcess_NotMergeableMarksRequestError(t *testing.T) { +func TestProcess_NotLandableMarksRequestError(t *testing.T) { ctrl := gomock.NewController(t) reqStore := storagemock.NewMockRequestStore(ctrl) @@ -154,11 +154,11 @@ func TestProcess_NotMergeableMarksRequestError(t *testing.T) { require.NoError(t, err) controller := NewController(zaptest.NewLogger(t).Sugar(), tally.NoopScope, staticStorageFactory{store: store}, registry, - runwaymq.TopicKeyMergeConflictCheckSignal, "orchestrator-mergeconflictsignal") + runwaymq.TopicKeyLandConflictCheckSignal, "orchestrator-landconflictsignal") - res := runwaymq.MergeResult{Id: testRequestID, Outcome: runwaypb.Outcome_FAILED, Reason: "conflict in foo.go"} + res := runwaymq.LandResult{Id: testRequestID, Outcome: runwaypb.Outcome_FAILED, Reason: "conflict in foo.go"} msg := entityqueue.NewMessage(testRequestID, resultPayload(t, res), testQueue, nil) - // Not-mergeable is an expected terminal outcome, so Process acks (no error). + // Not-landable is an expected terminal outcome, so Process acks (no error). require.NoError(t, controller.Process(context.Background(), newDelivery(ctrl, msg))) // The single publish is the terminal log entry carrying the conflict reason. @@ -181,7 +181,7 @@ func TestFailRequest_UpdateFailureLeavesRequestUnchanged(t *testing.T) { store.EXPECT().GetRequestStore().Return(reqStore) controller := NewController(zaptest.NewLogger(t).Sugar(), tally.NoopScope, staticStorageFactory{store: store}, consumer.TopicRegistry{}, - runwaymq.TopicKeyMergeConflictCheckSignal, "orchestrator-mergeconflictsignal") + runwaymq.TopicKeyLandConflictCheckSignal, "orchestrator-landconflictsignal") err := controller.failRequest(context.Background(), store, request, "conflict") require.Error(t, err) @@ -209,9 +209,9 @@ func TestProcess_HaltedRequestSkips(t *testing.T) { require.NoError(t, err) controller := NewController(zaptest.NewLogger(t).Sugar(), tally.NoopScope, staticStorageFactory{store: store}, registry, - runwaymq.TopicKeyMergeConflictCheckSignal, "orchestrator-mergeconflictsignal") + runwaymq.TopicKeyLandConflictCheckSignal, "orchestrator-landconflictsignal") - res := runwaymq.MergeResult{Id: testRequestID, Outcome: runwaypb.Outcome_SUCCEEDED} + res := runwaymq.LandResult{Id: testRequestID, Outcome: runwaypb.Outcome_SUCCEEDED} msg := entityqueue.NewMessage(testRequestID, resultPayload(t, res), testQueue, nil) require.NoError(t, controller.Process(context.Background(), newDelivery(ctrl, msg))) } diff --git a/submitqueue/orchestrator/controller/mergesignal/BUILD.bazel b/submitqueue/orchestrator/controller/landsignal/BUILD.bazel similarity index 94% rename from submitqueue/orchestrator/controller/mergesignal/BUILD.bazel rename to submitqueue/orchestrator/controller/landsignal/BUILD.bazel index 9163af3db..74e0e7f04 100644 --- a/submitqueue/orchestrator/controller/mergesignal/BUILD.bazel +++ b/submitqueue/orchestrator/controller/landsignal/BUILD.bazel @@ -2,8 +2,8 @@ load("@rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", - srcs = ["mergesignal.go"], - importpath = "github.com/uber/submitqueue/submitqueue/orchestrator/controller/mergesignal", + srcs = ["landsignal.go"], + importpath = "github.com/uber/submitqueue/submitqueue/orchestrator/controller/landsignal", visibility = ["//visibility:public"], deps = [ "//api/runway/messagequeue:go_default_library", @@ -22,7 +22,7 @@ go_library( go_test( name = "go_default_test", - srcs = ["mergesignal_test.go"], + srcs = ["landsignal_test.go"], embed = [":go_default_library"], deps = [ "//api/runway/messagequeue:go_default_library", diff --git a/submitqueue/orchestrator/controller/mergesignal/mergesignal.go b/submitqueue/orchestrator/controller/landsignal/landsignal.go similarity index 83% rename from submitqueue/orchestrator/controller/mergesignal/mergesignal.go rename to submitqueue/orchestrator/controller/landsignal/landsignal.go index 7611dd837..5c96553ed 100644 --- a/submitqueue/orchestrator/controller/mergesignal/mergesignal.go +++ b/submitqueue/orchestrator/controller/landsignal/landsignal.go @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package mergesignal consumes merge results from runway's merge-signal queue, +// Package landsignal consumes land results from runway's land-signal queue, // correlates them to the batch by the echoed id, and transitions the batch to a -// terminal state — Succeeded when runway merged the batch, Failed when it could +// terminal state — Succeeded when runway landed the batch, Failed when it could // not — then fans the batch out to conclude (so member requests pick up the -// outcome) and speculate (so dependents can re-plan). Like mergeconflictsignal +// outcome) and speculate (so dependents can re-plan). Like landconflictsignal // it is purely result-driven — runway pushes the result, so there is no poll // loop or self-reschedule. -package mergesignal +package landsignal import ( "context" @@ -38,7 +38,7 @@ import ( "go.uber.org/zap" ) -// Controller handles mergesignal queue messages. Implements consumer.Controller. +// Controller handles landsignal queue messages. Implements consumer.Controller. type Controller struct { logger *zap.SugaredLogger metricsScope tally.Scope @@ -51,7 +51,7 @@ type Controller struct { // Verify Controller implements consumer.Controller interface at compile time. var _ consumer.Controller = (*Controller)(nil) -// NewController creates a new mergesignal controller for the orchestrator. +// NewController creates a new landsignal controller for the orchestrator. func NewController( logger *zap.SugaredLogger, scope tally.Scope, @@ -61,8 +61,8 @@ func NewController( consumerGroup string, ) *Controller { return &Controller{ - logger: logger.Named("mergesignal_controller"), - metricsScope: scope.SubScope("mergesignal_controller"), + logger: logger.Named("landsignal_controller"), + metricsScope: scope.SubScope("landsignal_controller"), stores: stores, registry: registry, topicKey: topicKey, @@ -70,10 +70,10 @@ func NewController( } } -// Process consumes a runway merge result and advances or fails the batch. +// Process consumes a runway land result and advances or fails the batch. // Returns nil to ack, or error to nack/reject. // -// A not-merged verdict is an expected outcome of the merge, not a failure: the +// A not-landed verdict is an expected outcome of the land, not a failure: the // batch is driven to terminal Failed inline and the message is acked. Only // infrastructure faults — deserialize, storage, the state transition, and the // fan-out publishes — return an error and reject to the DLQ, where the batch is @@ -85,10 +85,10 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er // The runway result carries full data (it crosses the service boundary). Its // id is the batch id echoed back, so correlate straight to the batch. - result := &runwaymq.MergeResult{} + result := &runwaymq.LandResult{} if err := runwaymq.Unmarshal(msg.Payload, result); err != nil { metrics.NamedCounter(c.metricsScope, opName, "deserialize_errors", 1) - return fmt.Errorf("failed to deserialize merge result: %w", err) + return fmt.Errorf("failed to deserialize land result: %w", err) } store, err := c.stores.For(storage.Config{QueueName: result.GetQueueName()}) @@ -104,9 +104,9 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er return fmt.Errorf("failed to get batch %s: %w", result.Id, err) } - c.logger.Infow("received merge signal", + c.logger.Infow("received land signal", "batch_id", batch.ID, - "merged", result.Outcome == runwaypb.Outcome_SUCCEEDED, + "landed", result.Outcome == runwaypb.Outcome_SUCCEEDED, "state", string(batch.State), "version", batch.Version, "attempt", delivery.Attempt(), @@ -115,22 +115,22 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er // Cancelling: the cancel path (via speculate) owns the terminal write and the // downstream fan-out for a batch the user asked to cancel. Silently ack — do - // not transition (a racing terminal merge result must not override the + // not transition (a racing terminal land result must not override the // cancel) and do not fan out. if batch.State == entity.BatchStateCancelling { metrics.NamedCounter(c.metricsScope, opName, "skipped_cancelling", 1) return nil } - // A merge failure's reason travels to conclude on the fan-out message, not on + // A land failure's reason travels to conclude on the fan-out message, not on // the batch, so it reaches the request's terminal log without becoming durable - // batch state. Empty on the merged path. Computed before the idempotency check + // batch state. Empty on the landed path. Computed before the idempotency check // so a redelivered failed batch re-fans-out with its reason intact. var failureReason string if result.Outcome != runwaypb.Outcome_SUCCEEDED { failureReason = result.Reason if failureReason == "" { - failureReason = "merge failed" + failureReason = "land failed" } } @@ -150,14 +150,14 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er var newState entity.BatchState if result.Outcome == runwaypb.Outcome_SUCCEEDED { newState = entity.BatchStateSucceeded - c.logger.Infow("merged batch", + c.logger.Infow("landed batch", "batch_id", batch.ID, "steps", result.Steps, ) } else { - metrics.NamedCounter(c.metricsScope, opName, "not_merged", 1) + metrics.NamedCounter(c.metricsScope, opName, "not_landed", 1) newState = entity.BatchStateFailed - c.logger.Warnw("batch merge failed", + c.logger.Warnw("batch land failed", "batch_id", batch.ID, "reason", result.Reason, ) @@ -175,7 +175,7 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er // fanout publishes the batch ID to conclude (so requests are updated) and to // speculate (so dependents can re-evaluate now that this batch is done). // -// Both messages name the merge as their cause. Without it the speculate +// Both messages name the land as their cause. Without it the speculate // publish would reuse the bare batch ID, which the batch controller already // published at creation, and the queue would drop this one as a duplicate for // as long as that row survives — leaving dependents unwoken. Conclude is @@ -187,11 +187,11 @@ func (c *Controller) fanout(ctx context.Context, batchID, queue, failureReason s if failureReason != "" { concludeMeta = map[string]string{topickey.MetadataKeyFailureReason: failureReason} } - if err := c.publish(ctx, topickey.TopicKeyConclude, publish.IntentID(batchID, "conclude", "merged"), batchID, queue, concludeMeta); err != nil { + if err := c.publish(ctx, topickey.TopicKeyConclude, publish.IntentID(batchID, "conclude", "landed"), batchID, queue, concludeMeta); err != nil { metrics.NamedCounter(c.metricsScope, "process", "publish_conclude_errors", 1) return fmt.Errorf("failed to publish to conclude: %w", err) } - if err := c.publish(ctx, topickey.TopicKeySpeculate, publish.IntentID(batchID, "merged"), batchID, queue, nil); err != nil { + if err := c.publish(ctx, topickey.TopicKeySpeculate, publish.IntentID(batchID, "landed"), batchID, queue, nil); err != nil { metrics.NamedCounter(c.metricsScope, "process", "publish_speculate_errors", 1) return fmt.Errorf("failed to publish to speculate: %w", err) } @@ -216,7 +216,7 @@ func (c *Controller) publish(ctx context.Context, key consumer.TopicKey, msgID, // Name returns the controller name for logging and metrics. func (c *Controller) Name() string { - return "mergesignal" + return "landsignal" } // TopicKey returns the topic key this controller subscribes to. diff --git a/submitqueue/orchestrator/controller/mergesignal/mergesignal_test.go b/submitqueue/orchestrator/controller/landsignal/landsignal_test.go similarity index 90% rename from submitqueue/orchestrator/controller/mergesignal/mergesignal_test.go rename to submitqueue/orchestrator/controller/landsignal/landsignal_test.go index 73b78c3e3..777e79873 100644 --- a/submitqueue/orchestrator/controller/mergesignal/mergesignal_test.go +++ b/submitqueue/orchestrator/controller/landsignal/landsignal_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package mergesignal +package landsignal import ( "context" @@ -60,7 +60,7 @@ const ( testQueue = "test-queue" ) -func resultPayload(t *testing.T, res runwaymq.MergeResult) []byte { +func resultPayload(t *testing.T, res runwaymq.LandResult) []byte { payload, err := runwaymq.Marshal(&res) require.NoError(t, err) return payload @@ -99,8 +99,8 @@ func newController(t *testing.T, store *storagemock.MockStorage, registry consum tally.NoopScope, staticStorageFactory{store: store}, registry, - runwaymq.TopicKeyMergeSignal, - "orchestrator-mergesignal", + runwaymq.TopicKeyLandSignal, + "orchestrator-landsignal", ) } @@ -111,13 +111,13 @@ func TestNewController(t *testing.T) { var got []string c := newController(t, store, recordingRegistry(t, ctrl, &got)) - assert.Equal(t, consumer.TopicKey(runwaymq.TopicKeyMergeSignal), c.TopicKey()) - assert.Equal(t, "orchestrator-mergesignal", c.ConsumerGroup()) - assert.Equal(t, "mergesignal", c.Name()) + assert.Equal(t, consumer.TopicKey(runwaymq.TopicKeyLandSignal), c.TopicKey()) + assert.Equal(t, "orchestrator-landsignal", c.ConsumerGroup()) + assert.Equal(t, "landsignal", c.Name()) var _ consumer.Controller = c } -func TestProcess_MergedAdvancesBatch(t *testing.T) { +func TestProcess_LandedAdvancesBatch(t *testing.T) { ctrl := gomock.NewController(t) batchStore := storagemock.NewMockBatchStore(ctrl) @@ -126,7 +126,7 @@ func TestProcess_MergedAdvancesBatch(t *testing.T) { Queue: testQueue, Contains: []string{"test-queue/1"}, Dependencies: []string{"test-queue/batch/0"}, - State: entity.BatchStateMerging, + State: entity.BatchStateLanding, Version: 1, } batchStore.EXPECT().Get(gomock.Any(), testBatchID).Return(batch, nil) @@ -139,7 +139,7 @@ func TestProcess_MergedAdvancesBatch(t *testing.T) { var got []string c := newController(t, store, recordingRegistry(t, ctrl, &got)) - res := runwaymq.MergeResult{ + res := runwaymq.LandResult{ Id: testBatchID, Outcome: runwaypb.Outcome_SUCCEEDED, Steps: []*runwaymq.StepResult{{StepId: "test-queue/1", Outputs: []*runwaymq.StepOutput{{Id: "deadbeef"}}}}, @@ -152,7 +152,7 @@ func TestProcess_MergedAdvancesBatch(t *testing.T) { assert.ElementsMatch(t, []string{"conclude", "speculate"}, got) } -// The fan-out after a merge must not reuse the bare batch ID. +// The fan-out after a land must not reuse the bare batch ID. // // The batch's own announcement to speculate uses exactly that ID, and the // queue deduplicates on (topic, partition key, message ID) against every row it @@ -169,7 +169,7 @@ func TestProcess_FanoutDoesNotCollideWithTheBatchAnnouncement(t *testing.T) { Queue: testQueue, Contains: []string{"test-queue/1"}, Dependencies: []string{"test-queue/batch/0"}, - State: entity.BatchStateMerging, + State: entity.BatchStateLanding, Version: 1, } batchStore.EXPECT().Get(gomock.Any(), testBatchID).Return(batch, nil) @@ -195,7 +195,7 @@ func TestProcess_FanoutDoesNotCollideWithTheBatchAnnouncement(t *testing.T) { }) require.NoError(t, err) - res := runwaymq.MergeResult{Id: testBatchID, Outcome: runwaypb.Outcome_SUCCEEDED} + res := runwaymq.LandResult{Id: testBatchID, Outcome: runwaypb.Outcome_SUCCEEDED} msg := entityqueue.NewMessage(testBatchID, resultPayload(t, res), testQueue, nil) require.NoError(t, newController(t, store, registry).Process(context.Background(), newDelivery(ctrl, msg))) @@ -205,7 +205,7 @@ func TestProcess_FanoutDoesNotCollideWithTheBatchAnnouncement(t *testing.T) { assert.NotEqual(t, byTopic["speculate"], byTopic["conclude"]) } -func TestProcess_NotMergedMarksBatchFailed(t *testing.T) { +func TestProcess_NotLandedMarksBatchFailed(t *testing.T) { ctrl := gomock.NewController(t) batchStore := storagemock.NewMockBatchStore(ctrl) @@ -214,7 +214,7 @@ func TestProcess_NotMergedMarksBatchFailed(t *testing.T) { Queue: testQueue, Contains: []string{"test-queue/1"}, Dependencies: []string{"test-queue/batch/0"}, - State: entity.BatchStateMerging, + State: entity.BatchStateLanding, Version: 3, } batchStore.EXPECT().Get(gomock.Any(), testBatchID).Return(batch, nil) @@ -242,14 +242,14 @@ func TestProcess_NotMergedMarksBatchFailed(t *testing.T) { }) require.NoError(t, err) - res := runwaymq.MergeResult{Id: testBatchID, Outcome: runwaypb.Outcome_FAILED, Reason: "conflict in foo.go"} + res := runwaymq.LandResult{Id: testBatchID, Outcome: runwaypb.Outcome_FAILED, Reason: "conflict in foo.go"} msg := entityqueue.NewMessage(testBatchID, resultPayload(t, res), testQueue, nil) - // Not-merged is an expected terminal outcome, so Process acks (no error). + // Not-landed is an expected terminal outcome, so Process acks (no error). require.NoError(t, newController(t, store, registry).Process(context.Background(), newDelivery(ctrl, msg))) require.Contains(t, byTopic, "conclude") require.Contains(t, byTopic, "speculate") - // The merge reason rides the conclude message so conclude can stamp it on + // The land failure reason rides the conclude message so conclude can stamp it on // the request's terminal log; the speculate wake-up carries none. assert.Equal(t, "conflict in foo.go", byTopic["conclude"].Metadata[topickey.MetadataKeyFailureReason]) assert.Empty(t, byTopic["speculate"].Metadata[topickey.MetadataKeyFailureReason]) @@ -270,7 +270,7 @@ func TestProcess_CancellingShortCircuit(t *testing.T) { var got []string c := newController(t, store, recordingRegistry(t, ctrl, &got)) - res := runwaymq.MergeResult{Id: testBatchID, Outcome: runwaypb.Outcome_SUCCEEDED} + res := runwaymq.LandResult{Id: testBatchID, Outcome: runwaypb.Outcome_SUCCEEDED} msg := entityqueue.NewMessage(testBatchID, resultPayload(t, res), testQueue, nil) require.NoError(t, c.Process(context.Background(), newDelivery(ctrl, msg))) assert.Empty(t, got) @@ -292,7 +292,7 @@ func TestProcess_TerminalReFansOut(t *testing.T) { var got []string c := newController(t, store, recordingRegistry(t, ctrl, &got)) - res := runwaymq.MergeResult{Id: testBatchID, Outcome: runwaypb.Outcome_SUCCEEDED} + res := runwaymq.LandResult{Id: testBatchID, Outcome: runwaypb.Outcome_SUCCEEDED} msg := entityqueue.NewMessage(testBatchID, resultPayload(t, res), testQueue, nil) require.NoError(t, c.Process(context.Background(), newDelivery(ctrl, msg))) assert.ElementsMatch(t, []string{"conclude", "speculate"}, got) @@ -323,7 +323,7 @@ func TestProcess_StorageErrorRejects(t *testing.T) { var got []string c := newController(t, store, recordingRegistry(t, ctrl, &got)) - res := runwaymq.MergeResult{Id: testBatchID, Outcome: runwaypb.Outcome_SUCCEEDED} + res := runwaymq.LandResult{Id: testBatchID, Outcome: runwaypb.Outcome_SUCCEEDED} msg := entityqueue.NewMessage(testBatchID, resultPayload(t, res), testQueue, nil) require.Error(t, c.Process(context.Background(), newDelivery(ctrl, msg))) } diff --git a/submitqueue/orchestrator/controller/speculate/check.go b/submitqueue/orchestrator/controller/speculate/check.go index 14528c913..3e0607ba4 100644 --- a/submitqueue/orchestrator/controller/speculate/check.go +++ b/submitqueue/orchestrator/controller/speculate/check.go @@ -47,10 +47,10 @@ const ( // each drop. // // The Speculator is an extension, so its output is untrusted input: it decides -// which paths run, never whether a batch merges or fails. Every rule here +// which paths run, never whether a batch lands or fails. Every rule here // protects an invariant the extension could otherwise break — acting on a batch // that is finalizing, resurrecting a path a resolved dependency has ruled out, -// or discarding a passed build the queue is about to merge on. A proposal that +// or discarding a passed build the queue is about to use for landing. A proposal that // trips one of these is a bug in the Speculator, not a normal outcome, which is // why the caller counts them. func filterProposals(proposals []entity.Speculation, snap snapshot) ([]entity.Speculation, []rejection) { @@ -130,9 +130,9 @@ func rejectionReason(proposal entity.Speculation, snap snapshot) (rejection, boo // combination its assumptions name. With the length check and position-wise // equality, a missing, extra, or duplicate dependency is also impossible. // -// A malformed path is not merely suboptimal, it is unmergeable — the merge -// preconditions are read off the path's assumptions (see mergeablePath), so a -// path missing a dependency would let its head merge without waiting for it. +// A malformed path is not merely suboptimal, it is unlandable — the land +// preconditions are read off the path's assumptions (see landablePath), so a +// path missing a dependency would let its head land without waiting for it. func isWellFormed(path entity.SpeculationPath, head entity.Batch) bool { if path.Head != head.ID { return false diff --git a/submitqueue/orchestrator/controller/speculate/check_test.go b/submitqueue/orchestrator/controller/speculate/check_test.go index ee2e25f5f..3700ff56a 100644 --- a/submitqueue/orchestrator/controller/speculate/check_test.go +++ b/submitqueue/orchestrator/controller/speculate/check_test.go @@ -82,9 +82,9 @@ func TestFilterProposals_Rejects(t *testing.T) { want: rejectUnknownHead, }, { - name: "head already merging", + name: "head already landing", proposal: entity.Speculation{Path: valid, Action: entity.PathActionBuild}, - snap: checkSnapshot(entity.BatchStateMerging), + snap: checkSnapshot(entity.BatchStateLanding), want: rejectHeadNotSpeculating, }, { diff --git a/submitqueue/orchestrator/controller/speculate/dispatch.go b/submitqueue/orchestrator/controller/speculate/dispatch.go index c489ace90..5e3883f87 100644 --- a/submitqueue/orchestrator/controller/speculate/dispatch.go +++ b/submitqueue/orchestrator/controller/speculate/dispatch.go @@ -34,7 +34,7 @@ import ( // // It walks every in-flight batch, not only the speculating ones. Proposals // apply to speculating heads alone and are simply absent for the rest, but -// observations are not: a merging or cancelling head's paths keep holding CI +// observations are not: a landing or cancelling head's paths keep holding CI // slots until their builds stop, and this is the only writer that can record // that they have. Batches already finalized arrive here clean — // commitOutcome persisted their set with their outcome — so only their diff --git a/submitqueue/orchestrator/controller/speculate/doc.go b/submitqueue/orchestrator/controller/speculate/doc.go index a8927cdbb..79ec04ee7 100644 --- a/submitqueue/orchestrator/controller/speculate/doc.go +++ b/submitqueue/orchestrator/controller/speculate/doc.go @@ -23,8 +23,8 @@ // Batches in a queue depend on the batches ahead of them, so without // speculation everything is serial: C waits for B, B waits for A. Speculation // builds a batch against a guess about how its dependencies turn out. When -// the guess holds, the batch merges the moment the guessed-on dependencies -// land. If passed paths cover every possible outcome, the batch can merge +// the guess holds, the batch lands the moment the guessed-on dependencies +// land. If passed paths cover every possible outcome, the batch can land // before those dependencies settle. // // # Paths @@ -48,11 +48,11 @@ // Fund both and every future is covered: // // - While A is still unresolved, both P1 and P2 passing lets B bypass A and -// merge immediately: either possible future has already been validated. -// - A succeeds and P1 passed: B merges the moment A lands. P2's guess +// land immediately: either possible future has already been validated. +// - A succeeds and P1 passed: B lands the moment A lands. P2's guess // ("A fails") is broken — it can no longer come true — so its build is // cancelled to free the slot. -// - A fails and P2 passed: B merges without A, again with no new build. +// - A fails and P2 passed: B lands without A, again with no new build. // P1's guess is broken. // - A resolved either way, and every unbroken path failed: no future // remains in which B passes, so B fails. @@ -79,7 +79,7 @@ // until its build stops. A path is broken once a dependency's actual result // proves one of its assumptions wrong: its guess can no longer come true, so // its build is cancelled to free the slot. A path is superseded when its head -// becomes mergeable, so any still-running siblings are cancelled too. +// becomes landable, so any still-running siblings are cancelled too. // // Cancelling is intent, not fact: the build keeps its slot until CI actually // stops it, and only an observation of that stop (or proof nothing was ever @@ -91,7 +91,7 @@ // // # The life of a batch, as seen from here // -// Created ──admit──► Speculating ──┬── merge or bypass ──► Merging +// Created ──admit──► Speculating ──┬── land or bypass ──► Landing // └── fail ─────────────► Failed // user cancel (cancel stage): // ... ──► Cancelling ── every path stopped ──► Cancelled diff --git a/submitqueue/orchestrator/controller/speculate/finalize.go b/submitqueue/orchestrator/controller/speculate/finalize.go index d823c93e5..0b08f8fa3 100644 --- a/submitqueue/orchestrator/controller/speculate/finalize.go +++ b/submitqueue/orchestrator/controller/speculate/finalize.go @@ -33,11 +33,11 @@ import ( // snap.speculating holding the heads still open to new work. // // Everything here is a fact, not a choice: a path a resolved dependency ruled -// out is dead, a head whose passed builds establish a merge verdict merges, +// out is dead, a head whose passed builds establish a land verdict lands, // and a batch the user cancelled is finished once its last build stops. // Finalizing before the Speculator is asked is what keeps its work from // being wasted — asked first, it would propose builds for a head that is -// already merging. +// already landing. // // Outcomes cascade, and the head loop runs to a fixed point to collapse a // whole cascade into this one run: @@ -55,7 +55,7 @@ import ( // Deciding the whole cascade up front and writing afterwards would enact // dependents of an outcome whose own write then lost its compare-and-swap — // and the loser of that race is not always benign: a cancellation loses -// precisely to a merge that got there first, which leaves the batch +// precisely to a land that got there first, which leaves the batch // *succeeded*, after its dependents were already failed on the assumption it // was cancelled. Committing per generation costs no extra reads — the // snapshot is read once, and the writes are ones this run makes anyway. @@ -95,11 +95,11 @@ func (c *Controller) finalize(ctx context.Context, snap *snapshot) error { } bypassed := false - if decision == outcomeMerge { + if decision == outcomeLand { // The winning path carries the head out of the queue; its // siblings cannot help it any more and are still holding CI // slots the rest of the queue could use. - winner, ok := mergeablePath(set, *snap) + winner, ok := landablePath(set, *snap) if !ok { winner, bypassed = bypassablePath(batch, set, *snap) } @@ -159,8 +159,8 @@ func (c *Controller) reportSpeculation( ) error { after, hasPassed := livePassedPath(set, snap) - // A merge is decided on the same live passed path, so an ungated report - // would claim a wait on every head that merges straight through. + // A land is decided on the same live passed path, so an ungated report + // would claim a wait on every head that lands straight through. event, path := entity.RequestEventWaiting, after switch { case hasPassed && decision == outcomeWait: @@ -195,7 +195,7 @@ func (c *Controller) reportSpeculation( // moment it is asked to be — its builds hold their CI slots until they // actually stop — so the run marks the paths, the poll loop asks the runner // to stop them, and whichever later run sees them stopped finishes the -// job. That is why cancellation is best effort, and why a merge that wins the +// job. That is why cancellation is best effort, and why a land that wins the // race still prevails. func (c *Controller) finalizeCancellations(ctx context.Context, snap *snapshot, nowMs int64) error { // TODO(respeculate-collateral): re-enqueue Land for every request in batch.Contains @@ -283,9 +283,9 @@ func (c *Controller) commitOutcome(ctx context.Context, snap *snapshot, batch en // durable — see commitOutcome — because everything concluded about the // batches stacked on this one is derived from it. // -// Only a terminal outcome is recorded. Merging is not terminal — a head +// Only a terminal outcome is recorded. Landing is not terminal — a head // stacked on this one assumed it would *succeed*, and it has not yet — so a -// merge outcome resolves nothing for anybody else. +// land outcome resolves nothing for anybody else. func (c *Controller) recordOutcome(snap *snapshot, batchID string, decision outcome) { state, terminal := decision.terminalState() if !terminal { @@ -307,8 +307,8 @@ func (c *Controller) applyOutcome(ctx context.Context, store storage.Storage, ba var state entity.BatchState switch decision { - case outcomeMerge: - state = entity.BatchStateMerging + case outcomeLand: + state = entity.BatchStateLanding case outcomeFail, outcomeCancel: state, _ = decision.terminalState() @@ -347,8 +347,8 @@ func (c *Controller) applyOutcome(ctx context.Context, store storage.Storage, ba ) switch decision { - case outcomeMerge: - if err := c.dispatchMerge(ctx, batch); err != nil { + case outcomeLand: + if err := c.dispatchLand(ctx, batch); err != nil { return true, err } @@ -362,7 +362,7 @@ func (c *Controller) applyOutcome(ctx context.Context, store storage.Storage, ba } // Named for the run that decided it, so a redelivery re-deriving the // same outcome does not conclude the batch twice, and so it stays - // distinct from the conclude mergesignal sends for a merged batch. + // distinct from the conclude landsignal sends for a landed batch. // A conclude that goes missing is recovered by fanout, which is // deliberately un-deduplicated. if err := c.publishBatchIDWithMetadata(ctx, topickey.TopicKeyConclude, publish.IntentID(batch.ID, "conclude", "speculate"), batch.ID, batch.Queue, batch.Queue, concludeMeta); err != nil { @@ -373,11 +373,11 @@ func (c *Controller) applyOutcome(ctx context.Context, store storage.Storage, ba return true, nil } -// dispatchMerge reports speculation finished and hands the batch to the merge -// stage. The stable ID means a redelivery or the Merging self-heal dedupes -// against the request already sent instead of merging twice; the status goes +// dispatchLand reports speculation finished and hands the batch to the land +// stage. The stable ID means a redelivery or the Landing self-heal dedupes +// against the request already sent instead of landing twice; the status goes // first so it cannot be timestamped after the landing the dispatch triggers. -func (c *Controller) dispatchMerge(ctx context.Context, batch entity.Batch) error { +func (c *Controller) dispatchLand(ctx context.Context, batch entity.Batch) error { if err := corerequest.PublishBatchLogs(ctx, c.registry, batch.Queue, batch.Contains, entity.RequestStatusSpeculated, batch.ID, map[string]string{"batch_id": batch.ID}, ); err != nil { @@ -385,9 +385,9 @@ func (c *Controller) dispatchMerge(ctx context.Context, batch entity.Batch) erro return fmt.Errorf("failed to publish request logs for batch %s: %w", batch.ID, err) } - if err := c.publishBatchID(ctx, topickey.TopicKeyMerge, publish.IntentID(batch.ID, "merge-dispatch"), batch.ID, batch.Queue, batch.Queue); err != nil { + if err := c.publishBatchID(ctx, topickey.TopicKeyLand, publish.IntentID(batch.ID, "land-dispatch"), batch.ID, batch.Queue, batch.Queue); err != nil { metrics.NamedCounter(c.metricsScope, opName, "publish_errors", 1) - return fmt.Errorf("failed to publish batch %s to merge: %w", batch.ID, err) + return fmt.Errorf("failed to publish batch %s to land: %w", batch.ID, err) } return nil } @@ -399,7 +399,7 @@ func (c *Controller) dispatchMerge(ctx context.Context, batch entity.Batch) erro // A batch named by a message is repaired through it: a redelivery re-publishes // from one of Process's self-heal branches, and a persistent failure // dead-letters by name. A cascade-decided batch has neither, and finalize only -// walks heads still speculating — so a merged one would never reach Runway, +// walks heads still speculating — so a batch in Landing would never reach Runway, // and a terminal one would leave its requests unreconciled. // // Distinct per publish: the guarantee being bought is that a message exists at @@ -456,7 +456,7 @@ func cancelBrokenPathsInSet(set *entity.SpeculationPathSet, snap snapshot, nowMs }) } -// supersede stops every path other than the winner once the head can merge. +// supersede stops every path other than the winner once the head can land. // Its live siblings cannot help any more but still hold CI slots the rest of // the queue could use. func supersede(set *entity.SpeculationPathSet, winnerID string, nowMs int64) bool { diff --git a/submitqueue/orchestrator/controller/speculate/outcome.go b/submitqueue/orchestrator/controller/speculate/outcome.go index cd6c0c2c7..c7a64a525 100644 --- a/submitqueue/orchestrator/controller/speculate/outcome.go +++ b/submitqueue/orchestrator/controller/speculate/outcome.go @@ -25,10 +25,10 @@ type outcome string const ( // outcomeWait means the batch's outcome is not decided yet. outcomeWait outcome = "wait" - // outcomeMerge means the head can be handed to the merge stage: either a + // outcomeLand means the head can be handed to the land stage: either a // passed path's assumptions have all come true, or passed paths cover every // possible outcome of its unsettled dependencies. - outcomeMerge outcome = "merge" + outcomeLand outcome = "land" // outcomeFail means no future remains in which the head could pass. outcomeFail outcome = "fail" // outcomeCancel means a batch the user asked to cancel has had every path @@ -37,8 +37,8 @@ const ( ) // terminalState returns the batch state an outcome writes, and whether the -// outcome leaves the batch terminal. Merge is the odd one out: it hands the -// batch to the merge stage, which owns the terminal write that follows. +// outcome leaves the batch terminal. Land is the odd one out: it hands the +// batch to the land stage, which owns the terminal write that follows. func (v outcome) terminalState() (entity.BatchState, bool) { switch v { case outcomeFail: @@ -52,11 +52,11 @@ func (v outcome) terminalState() (entity.BatchState, bool) { // decide returns the run's outcome on one open head, from the snapshot alone. func decide(head entity.Batch, set entity.SpeculationPathSet, snap snapshot) outcome { - if _, ok := mergeablePath(set, snap); ok { - return outcomeMerge + if _, ok := landablePath(set, snap); ok { + return outcomeLand } if _, ok := bypassablePath(head, set, snap); ok { - return outcomeMerge + return outcomeLand } if hasNoViableFuture(head, set, snap) { return outcomeFail @@ -64,23 +64,23 @@ func decide(head entity.Batch, set entity.SpeculationPathSet, snap snapshot) out return outcomeWait } -// mergeablePath returns a passed path whose merge preconditions are met: every +// landablePath returns a passed path whose land preconditions are met: every // guess it made about a dependency has been borne out by that dependency's // actual state. // // This is what makes speculation pay — not by shortening the list the head // waits on, but by having already done the work. The build ran against the // guess while the dependencies were still resolving, so when they land the way -// the path assumed there is nothing left to run and the head merges at once. +// the path assumed there is nothing left to run and the head lands at once. // -// A guess that has not been settled yet is not a licence to merge, whichever +// A guess that has not been settled yet is not a licence to land, whichever // way it points. A path that assumed a dependency would fail was built without // that dependency's changes, so landing it while the dependency is still live // puts a combination on the trunk that no build ever validated — which is the -// one thing the queue exists to prevent. The dependency merging is not enough -// either: a merge can fail, so "on its way in" is still an open question, and +// one thing the queue exists to prevent. The dependency landing is not enough +// either: a land can fail, so "on its way in" is still an open question, and // the head waits for the answer. -func mergeablePath(set entity.SpeculationPathSet, snap snapshot) (entity.SpeculationPathEntry, bool) { +func landablePath(set entity.SpeculationPathSet, snap snapshot) (entity.SpeculationPathEntry, bool) { for _, entry := range set.Paths { if entry.Status != entity.SpeculationPathStatusPassed { continue @@ -115,10 +115,10 @@ func passedEntry(set entity.SpeculationPathSet) (entity.SpeculationPathEntry, bo // consistent with how its dependencies are resolving, whether or not they have // finished resolving. // -// It is mergeablePath without the settled requirement, and the difference +// It is landablePath without the settled requirement, and the difference // between the two is exactly the head's waiting room: work this head had to do // is done, and all that is left is other batches finishing. Reported rather -// than acted on — nothing may merge on a path this loose, and decide is +// than acted on — nothing may land on a path this loose, and decide is // deliberately not built on it. func livePassedPath(set entity.SpeculationPathSet, snap snapshot) (entity.SpeculationPathEntry, bool) { for _, entry := range set.Paths { diff --git a/submitqueue/orchestrator/controller/speculate/outcome_test.go b/submitqueue/orchestrator/controller/speculate/outcome_test.go index f1b6fd591..e14dced6e 100644 --- a/submitqueue/orchestrator/controller/speculate/outcome_test.go +++ b/submitqueue/orchestrator/controller/speculate/outcome_test.go @@ -30,10 +30,10 @@ func setOf(entries ...entity.SpeculationPathEntry) entity.SpeculationPathSet { return entity.SpeculationPathSet{Head: head, Paths: entries} } -// The payoff case: a head merges as soon as the dependencies its passed build +// The payoff case: a head lands as soon as the dependencies its passed build // was stacked on have landed, without waiting for the ones it was built // without or told to ignore. -func TestMergeablePath(t *testing.T) { +func TestLandablePath(t *testing.T) { const ( succeeds = entity.DependencyAssumptionSucceeds fails = entity.DependencyAssumptionFails @@ -49,23 +49,23 @@ func TestMergeablePath(t *testing.T) { // dep2 is settled the way its assumption expects throughout, so dep1 // is the only thing under test. { - name: "waits for an assumed-succeeding dependency to merge", + name: "waits for an assumed-succeeding dependency to land", assumption: [2]entity.DependencyAssumption{succeeds, fails}, dep1State: entity.BatchStateSpeculating, dep2State: entity.BatchStateFailed, want: false, }, { - name: "merges once it has", + name: "lands once it has", assumption: [2]entity.DependencyAssumption{succeeds, fails}, dep1State: entity.BatchStateSucceeded, dep2State: entity.BatchStateFailed, want: true, }, { - name: "an assumed-succeeding dependency waits out its merge", + name: "an assumed-succeeding dependency waits out its land", assumption: [2]entity.DependencyAssumption{succeeds, fails}, - dep1State: entity.BatchStateMerging, + dep1State: entity.BatchStateLanding, dep2State: entity.BatchStateFailed, want: false, }, @@ -77,9 +77,9 @@ func TestMergeablePath(t *testing.T) { want: false, }, { - name: "still waits while that dependency is merging", + name: "still waits while that dependency is landing", assumption: [2]entity.DependencyAssumption{fails, fails}, - dep1State: entity.BatchStateMerging, + dep1State: entity.BatchStateLanding, dep2State: entity.BatchStateFailed, want: false, }, @@ -91,14 +91,14 @@ func TestMergeablePath(t *testing.T) { want: false, }, { - name: "merges once it has failed", + name: "lands once it has failed", assumption: [2]entity.DependencyAssumption{fails, fails}, dep1State: entity.BatchStateFailed, dep2State: entity.BatchStateFailed, want: true, }, { - name: "merges once it has been cancelled", + name: "lands once it has been cancelled", assumption: [2]entity.DependencyAssumption{fails, fails}, dep1State: entity.BatchStateCancelled, dep2State: entity.BatchStateFailed, @@ -120,14 +120,14 @@ func TestMergeablePath(t *testing.T) { dep2 = entity.BatchStateSpeculating } set := setOf(passedPath(tt.assumption[0], tt.assumption[1])) - _, ok := mergeablePath(set, snapWith(tt.dep1State, dep2)) + _, ok := landablePath(set, snapWith(tt.dep1State, dep2)) assert.Equal(t, tt.want, ok) }) } } // A path that has not passed cannot carry the head out of the queue. -func TestMergeablePath_IgnoresUnpassedPaths(t *testing.T) { +func TestLandablePath_IgnoresUnpassedPaths(t *testing.T) { for _, status := range []entity.SpeculationPathStatus{ entity.SpeculationPathStatusPending, entity.SpeculationPathStatusBuilding, @@ -138,19 +138,19 @@ func TestMergeablePath_IgnoresUnpassedPaths(t *testing.T) { t.Run(string(status), func(t *testing.T) { set := setOf(entryFor( pathOver(entity.DependencyAssumptionSucceeds, entity.DependencyAssumptionSucceeds), status)) - _, ok := mergeablePath(set, snapWith(entity.BatchStateSucceeded, entity.BatchStateSucceeded)) + _, ok := landablePath(set, snapWith(entity.BatchStateSucceeded, entity.BatchStateSucceeded)) assert.False(t, ok) }) } } // A passed build whose assumptions reality has since contradicted is not a -// licence to merge — it verified a world that did not happen. -func TestMergeablePath_ExcludesBrokenPassedPath(t *testing.T) { +// licence to land — it verified a world that did not happen. +func TestLandablePath_ExcludesBrokenPassedPath(t *testing.T) { set := setOf(passedPath(entity.DependencyAssumptionFails, entity.DependencyAssumptionFails)) // The path was built without dep1, but dep1 landed after all. - _, ok := mergeablePath(set, snapWith(entity.BatchStateSucceeded, entity.BatchStateSpeculating)) + _, ok := landablePath(set, snapWith(entity.BatchStateSucceeded, entity.BatchStateSpeculating)) assert.False(t, ok) } @@ -180,7 +180,7 @@ func TestBypassablePath(t *testing.T) { head: headBatch, set: setOf(allPaths...), dep1State: entity.BatchStateSpeculating, - dep2State: entity.BatchStateMerging, + dep2State: entity.BatchStateLanding, want: true, }, { @@ -266,7 +266,7 @@ func TestBypassablePath(t *testing.T) { dep2State: entity.BatchStateSpeculating, }, { - name: "leaves fully settled dependencies to strict merge", + name: "leaves fully settled dependencies to strict land", head: headBatch, set: setOf(allPaths...), dep1State: entity.BatchStateSucceeded, @@ -285,10 +285,10 @@ func TestBypassablePath(t *testing.T) { } } -// livePassedPath is mergeablePath without the settled requirement, and the gap +// livePassedPath is landablePath without the settled requirement, and the gap // between the two is the head's waiting room: its own work is done and all that // remains is other batches finishing. That window is reported to the members, -// so it has to be recognised while mergeablePath still says no. +// so it has to be recognised while landablePath still says no. func TestLivePassedPath(t *testing.T) { const ( succeeds = entity.DependencyAssumptionSucceeds @@ -404,12 +404,12 @@ func TestDecide(t *testing.T) { pathOver(entity.DependencyAssumptionSucceeds, entity.DependencyAssumptionFails), entity.SpeculationPathStatusBuilding) - assert.Equal(t, outcomeMerge, decide(headBatch, setOf(passed), allResolved)) + assert.Equal(t, outcomeLand, decide(headBatch, setOf(passed), allResolved)) assert.Equal(t, outcomeFail, decide(headBatch, setOf(failed), allResolved)) assert.Equal(t, outcomeWait, decide(headBatch, setOf(building), allResolved)) // A passed path wins over a failed sibling: one way through is enough. - assert.Equal(t, outcomeMerge, decide(headBatch, setOf(failed, passed), allResolved)) + assert.Equal(t, outcomeLand, decide(headBatch, setOf(failed, passed), allResolved)) allUnresolved := snapWith(entity.BatchStateSpeculating, entity.BatchStateSpeculating) fullCoverage := setOf( @@ -418,7 +418,7 @@ func TestDecide(t *testing.T) { passedPath(entity.DependencyAssumptionFails, entity.DependencyAssumptionSucceeds), passedPath(entity.DependencyAssumptionFails, entity.DependencyAssumptionFails), ) - assert.Equal(t, outcomeMerge, decide(headBatch, fullCoverage, allUnresolved)) + assert.Equal(t, outcomeLand, decide(headBatch, fullCoverage, allUnresolved)) } // Once a path has passed, its siblings cannot help the head but are still diff --git a/submitqueue/orchestrator/controller/speculate/run.go b/submitqueue/orchestrator/controller/speculate/run.go index afc642cce..dba882c63 100644 --- a/submitqueue/orchestrator/controller/speculate/run.go +++ b/submitqueue/orchestrator/controller/speculate/run.go @@ -49,7 +49,7 @@ func (c *Controller) run(ctx context.Context, store storage.Storage, trigger ent if len(snap.speculating) == 0 { // No head is open to new work, so there is nothing to ask the // Speculator. The dispatch step still runs: what the build stages saw about a - // merging or cancelling head's paths has to be persisted so those + // landing or cancelling head's paths has to be persisted so those // paths stop counting against the budget. return c.dispatch(ctx, trigger.Queue, snap, nil) } @@ -292,7 +292,7 @@ func terminalPathStatus(status entity.BuildStatus) entity.SpeculationPathStatus // head's dependencies are the facts its paths are built from, so a dependency // withheld is one the Speculator has to plan around blind. Every in-flight // path set goes over for the same reason: a path holds its CI slot until its -// build actually stops, so a merging head's superseded siblings and a +// build actually stops, so a landing head's superseded siblings and a // cancelling head's live builds spend the budget just like a speculating // head's do. Hiding either would let the allocator count occupied slots as // free and oversubscribe CI. @@ -301,7 +301,7 @@ func terminalPathStatus(status entity.BuildStatus) entity.SpeculationPathStatus // head, and check rejects any proposal aimed at a head that is not // speculating. A head this run has just decided still reads as Speculating // here, but it can only rebuild the path that already passed — see -// mergeablePath — which the allocator skips as finished. +// landablePath — which the allocator skips as finished. func (c *Controller) ask(ctx context.Context, queue string, snap snapshot) ([]entity.Speculation, error) { spec, err := c.speculators.For(speculator.Config{QueueName: queue}) if err != nil { diff --git a/submitqueue/orchestrator/controller/speculate/run_test.go b/submitqueue/orchestrator/controller/speculate/run_test.go index 2c96b74e8..29afa7745 100644 --- a/submitqueue/orchestrator/controller/speculate/run_test.go +++ b/submitqueue/orchestrator/controller/speculate/run_test.go @@ -187,7 +187,7 @@ func newRunHarness(t *testing.T, ctrl *gomock.Controller, spec *scriptedSpeculat registry, err := consumer.NewTopicRegistry([]consumer.TopicConfig{ {Key: topickey.TopicKeyBuild, Name: "build", Queue: q}, - {Key: topickey.TopicKeyMerge, Name: "submitqueue-merge", Queue: q}, + {Key: topickey.TopicKeyLand, Name: "submitqueue-land", Queue: q}, {Key: topickey.TopicKeyConclude, Name: "conclude", Queue: q}, {Key: topickey.TopicKeySpeculate, Name: "speculate", Queue: q}, {Key: topickey.TopicKeyLog, Name: "log", Queue: q}, @@ -279,20 +279,20 @@ func TestRun_PassesSnapshotToSpeculator(t *testing.T) { ctrl := gomock.NewController(t) spec := &scriptedSpeculator{} - merging := entity.Batch{ID: "q/batch/merging", Queue: "q", State: entity.BatchStateMerging, Version: 1} - h := newRunHarness(t, ctrl, spec, []entity.Batch{speculatingHead(), merging}) + landing := entity.Batch{ID: "q/batch/landing", Queue: "q", State: entity.BatchStateLanding, Version: 1} + h := newRunHarness(t, ctrl, spec, []entity.Batch{speculatingHead(), landing}) h.noBuildsDispatched() h.batches.EXPECT().Get(gomock.Any(), dep1).Return(entity.Batch{ID: dep1, State: entity.BatchStateSucceeded}, nil) h.batches.EXPECT().Get(gomock.Any(), dep2).Return(entity.Batch{ID: dep2, State: entity.BatchStateSpeculating}, nil) existing := entity.SpeculationPathSet{Head: head, Version: 3} h.pathSets.EXPECT().Get(gomock.Any(), head).Return(existing, nil) - h.pathSets.EXPECT().Get(gomock.Any(), merging.ID).Return(entity.SpeculationPathSet{}, storage.ErrNotFound) + h.pathSets.EXPECT().Get(gomock.Any(), landing.ID).Return(entity.SpeculationPathSet{}, storage.ErrNotFound) require.NoError(t, h.run(head)) require.Equal(t, 1, spec.calls) - assert.ElementsMatch(t, []string{dep1, dep2, head, merging.ID}, h.speculatedOver(), + assert.ElementsMatch(t, []string{dep1, dep2, head, landing.ID}, h.speculatedOver(), "every batch the run read, in no particular order") require.Len(t, spec.gotSets, 1) assert.Equal(t, int32(3), spec.gotSets[0].Version) @@ -525,7 +525,7 @@ func TestRun_DoesNotReReadFinishedPaths(t *testing.T) { spec := &scriptedSpeculator{} h := newRunHarness(t, ctrl, spec, []entity.Batch{speculatingHead()}) - // dep1 is unresolved, so the passed path cannot merge — this test is about + // dep1 is unresolved, so the passed path cannot land — this test is about // observation being skipped, not about outcomes. h.batches.EXPECT().Get(gomock.Any(), dep1).Return(entity.Batch{ID: dep1, State: entity.BatchStateSpeculating}, nil) h.batches.EXPECT().Get(gomock.Any(), dep2).Return(entity.Batch{ID: dep2, State: entity.BatchStateFailed}, nil) @@ -633,7 +633,7 @@ func TestRun_DecidedHeadIsNotSpeculatedOn(t *testing.T) { h := newRunHarness(t, ctrl, spec, []entity.Batch{speculatingHead()}) h.noBuildsDispatched() - // Both dependencies resolved the way the passed path assumed, so it merges. + // Both dependencies resolved the way the passed path assumed, so it lands. h.batches.EXPECT().Get(gomock.Any(), dep1).Return(entity.Batch{ID: dep1, State: entity.BatchStateSucceeded}, nil) h.batches.EXPECT().Get(gomock.Any(), dep2).Return(entity.Batch{ID: dep2, State: entity.BatchStateFailed}, nil) h.pathSets.EXPECT().Get(gomock.Any(), head).Return(entity.SpeculationPathSet{ @@ -642,26 +642,26 @@ func TestRun_DecidedHeadIsNotSpeculatedOn(t *testing.T) { Version: 1, }, nil) - // The head moves to merging. Its set is untouched: the only path is the + // The head moves to landing. Its set is untouched: the only path is the // winner, and no proposal was applied. h.batches.EXPECT(). - Update(gomock.Any(), updateTo{id: head, state: entity.BatchStateMerging}, int32(1), int32(2)). + Update(gomock.Any(), updateTo{id: head, state: entity.BatchStateLanding}, int32(1), int32(2)). Return(nil) require.NoError(t, h.run(head)) assert.Zero(t, spec.calls, "a decided head leaves nothing to speculate about") - assert.Equal(t, []string{"submitqueue-merge"}, h.published) - assert.Contains(t, h.filed, entity.QueueBatchState{Queue: "q", State: entity.BatchStateMerging, BatchID: head}, + assert.Equal(t, []string{"submitqueue-land"}, h.published) + assert.Contains(t, h.filed, entity.QueueBatchState{Queue: "q", State: entity.BatchStateLanding, BatchID: head}, "the outcome must file the head under its new state") assert.Contains(t, h.unfiled, entity.QueueBatchState{State: entity.BatchStateSpeculating, BatchID: head}, "and drop the record under the state it left") } -// The churn this ordering removes: a mergeable head must not gain a funded path +// The churn this ordering removes: a landable head must not gain a funded path // that the same run immediately cancels — the dispatch would have started CI // for work nothing waits for. -func TestRun_MergeableHeadGainsNoNewPath(t *testing.T) { +func TestRun_LandableHeadGainsNoNewPath(t *testing.T) { ctrl := gomock.NewController(t) passed := pathOver(entity.DependencyAssumptionSucceeds, entity.DependencyAssumptionFails) other := pathOver(entity.DependencyAssumptionFails, entity.DependencyAssumptionFails) @@ -689,13 +689,13 @@ func TestRun_MergeableHeadGainsNoNewPath(t *testing.T) { h.pathSets.EXPECT().Update(gomock.Any(), gomock.Any(), int32(1), int32(2)). DoAndReturn(func(_ context.Context, s entity.SpeculationPathSet, _, _ int32) error { - require.Len(t, s.Paths, 2, "no path was funded for a head that is merging") + require.Len(t, s.Paths, 2, "no path was funded for a head that is landing") assert.Equal(t, entity.SpeculationPathStatusPassed, s.Paths[0].Status) assert.Equal(t, entity.SpeculationPathStatusCancelling, s.Paths[1].Status) return nil }) h.batches.EXPECT(). - Update(gomock.Any(), updateTo{id: head, state: entity.BatchStateMerging}, int32(1), int32(2)). + Update(gomock.Any(), updateTo{id: head, state: entity.BatchStateLanding}, int32(1), int32(2)). Return(nil) require.NoError(t, h.run(head)) @@ -705,7 +705,7 @@ func TestRun_MergeableHeadGainsNoNewPath(t *testing.T) { // The dispatch is what takes a batch out of this stage's hands, so sending it // before the write would let Runway act on an outcome a lost compare-and-swap // refused to record. -func TestRun_MergeableHeadDispatchesAfterTheStateWrite(t *testing.T) { +func TestRun_LandableHeadDispatchesAfterTheStateWrite(t *testing.T) { ctrl := gomock.NewController(t) passed := pathOver(entity.DependencyAssumptionSucceeds, entity.DependencyAssumptionSucceeds) @@ -721,21 +721,21 @@ func TestRun_MergeableHeadDispatchesAfterTheStateWrite(t *testing.T) { var publishedBeforeWrite []string h.batches.EXPECT(). - Update(gomock.Any(), updateTo{id: head, state: entity.BatchStateMerging}, int32(1), int32(2)). + Update(gomock.Any(), updateTo{id: head, state: entity.BatchStateLanding}, int32(1), int32(2)). DoAndReturn(func(context.Context, entity.Batch, int32, int32) error { publishedBeforeWrite = append([]string(nil), h.published...) return nil }) require.NoError(t, h.run(head)) - assert.Equal(t, []string{"submitqueue-merge"}, h.published) + assert.Equal(t, []string{"submitqueue-land"}, h.published) assert.Empty(t, publishedBeforeWrite, - "nothing may reach the merge stage before the state it acts on is written") + "nothing may reach the land stage before the state it acts on is written") } // The other half: a lost write means another writer owns the batch, so the // dispatch it would have justified is never sent. -func TestRun_MergeableHeadDispatchesNothingWhenTheStateCASLoses(t *testing.T) { +func TestRun_LandableHeadDispatchesNothingWhenTheStateCASLoses(t *testing.T) { ctrl := gomock.NewController(t) passed := pathOver(entity.DependencyAssumptionSucceeds, entity.DependencyAssumptionSucceeds) @@ -749,7 +749,7 @@ func TestRun_MergeableHeadDispatchesNothingWhenTheStateCASLoses(t *testing.T) { Version: 1, }, nil).AnyTimes() h.batches.EXPECT(). - Update(gomock.Any(), updateTo{id: head, state: entity.BatchStateMerging}, int32(1), int32(2)). + Update(gomock.Any(), updateTo{id: head, state: entity.BatchStateLanding}, int32(1), int32(2)). Return(storage.ErrVersionMismatch) require.NoError(t, h.run(head)) @@ -999,44 +999,44 @@ func TestRun_CancellingLostPathSetRaceSkipsTheTerminalWrite(t *testing.T) { } // The allocator rations a budget measured in occupied CI slots, and a path -// holds its slot until its build actually stops. A merging head's superseded +// holds its slot until its build actually stops. A landing head's superseded // siblings are still running, so hiding their set would let the allocator count // those slots as free and oversubscribe CI. func TestRun_SpeculatorSeesPathSetsOfNonOpenHeads(t *testing.T) { ctrl := gomock.NewController(t) spec := &scriptedSpeculator{} - merging := entity.Batch{ - ID: "q/batch/merging", Queue: "q", State: entity.BatchStateMerging, Version: 1, + landing := entity.Batch{ + ID: "q/batch/landing", Queue: "q", State: entity.BatchStateLanding, Version: 1, } open := entity.Batch{ID: head, Queue: "q", State: entity.BatchStateSpeculating, Version: 1} - h := newRunHarness(t, ctrl, spec, []entity.Batch{open, merging}) + h := newRunHarness(t, ctrl, spec, []entity.Batch{open, landing}) h.noBuildsDispatched() h.pathSets.EXPECT().Get(gomock.Any(), head). Return(entity.SpeculationPathSet{Head: head, Version: 1}, nil) - h.pathSets.EXPECT().Get(gomock.Any(), merging.ID).Return(entity.SpeculationPathSet{ - Head: merging.ID, + h.pathSets.EXPECT().Get(gomock.Any(), landing.ID).Return(entity.SpeculationPathSet{ + Head: landing.ID, Paths: []entity.SpeculationPathEntry{ {ID: "still-running", Status: entity.SpeculationPathStatusCancelling, Attempt: 1}, }, Version: 1, }, nil) - // The undispatched cancelling path is marked cancelled, so the merging head's set is + // The undispatched cancelling path is marked cancelled, so the landing head's set is // rewritten even though it is closed to new work. h.pathSets.EXPECT().Update(gomock.Any(), gomock.Any(), int32(1), int32(2)). DoAndReturn(func(_ context.Context, s entity.SpeculationPathSet, _, _ int32) error { - assert.Equal(t, merging.ID, s.Head) + assert.Equal(t, landing.ID, s.Head) assert.Equal(t, entity.SpeculationPathStatusCancelled, s.Paths[0].Status) return nil }) require.NoError(t, h.run(head)) - assert.ElementsMatch(t, []entity.Batch{open, merging}, spec.gotBatches, + assert.ElementsMatch(t, []entity.Batch{open, landing}, spec.gotBatches, "a closed head is still a fact the open ones are planned against") require.Len(t, spec.gotSets, 2, "every in-flight path set counts against the budget") - assert.Equal(t, merging.ID, spec.gotSets[1].Head) + assert.Equal(t, landing.ID, spec.gotSets[1].Head) } // A queue whose only in-flight head is closed to new work still has to be @@ -1047,13 +1047,13 @@ func TestRun_PersistsObservationsWithNoOpenHead(t *testing.T) { ctrl := gomock.NewController(t) spec := &scriptedSpeculator{} - merging := entity.Batch{ - ID: "q/batch/merging", Queue: "q", State: entity.BatchStateMerging, Version: 1, + landing := entity.Batch{ + ID: "q/batch/landing", Queue: "q", State: entity.BatchStateLanding, Version: 1, } - h := newRunHarness(t, ctrl, spec, []entity.Batch{merging}) - h.pathSets.EXPECT().Get(gomock.Any(), merging.ID).Return(entity.SpeculationPathSet{ - Head: merging.ID, + h := newRunHarness(t, ctrl, spec, []entity.Batch{landing}) + h.pathSets.EXPECT().Get(gomock.Any(), landing.ID).Return(entity.SpeculationPathSet{ + Head: landing.ID, Paths: []entity.SpeculationPathEntry{ {ID: "p1", Status: entity.SpeculationPathStatusCancelling, Attempt: 1}, }, @@ -1110,9 +1110,9 @@ func TestFanout_MintsADistinctMessageIDPerPublish(t *testing.T) { assert.NotEqual(t, ids[0], ids[1]) } -// The merge dispatch is the mirror image: a batch merges once, so the ID has +// The land dispatch is the mirror image: a batch lands once, so the ID has // to be stable across the redelivery and the self-heal that both re-derive it. -func TestDispatchMerge_ReusesOneMessageIDPerBatch(t *testing.T) { +func TestDispatchLand_ReusesOneMessageIDPerBatch(t *testing.T) { ctrl := gomock.NewController(t) var ids []string @@ -1127,7 +1127,7 @@ func TestDispatchMerge_ReusesOneMessageIDPerBatch(t *testing.T) { q.EXPECT().Publisher().Return(pub).AnyTimes() registry, err := consumer.NewTopicRegistry([]consumer.TopicConfig{ - {Key: topickey.TopicKeyMerge, Name: "submitqueue-merge", Queue: q}, + {Key: topickey.TopicKeyLand, Name: "submitqueue-land", Queue: q}, }) require.NoError(t, err) @@ -1137,8 +1137,8 @@ func TestDispatchMerge_ReusesOneMessageIDPerBatch(t *testing.T) { ) batch := entity.Batch{ID: head, Queue: "q"} - require.NoError(t, c.dispatchMerge(context.Background(), batch)) - require.NoError(t, c.dispatchMerge(context.Background(), batch)) + require.NoError(t, c.dispatchLand(context.Background(), batch)) + require.NoError(t, c.dispatchLand(context.Background(), batch)) require.Len(t, ids, 2) assert.Equal(t, ids[0], ids[1]) @@ -1176,7 +1176,7 @@ func cascadePair(t *testing.T, ctrl *gomock.Controller, prerequisiteState entity // An outcome is only a fact once it commits. When the prerequisite's state write // loses its race, nothing derived from that outcome may be enacted — the winner // may have written something else entirely. A cancellation loses precisely to a -// merge that got there first, which leaves the batch succeeded, and a dependent +// land that got there first, which leaves the batch succeeded, and a dependent // broken by that success must not already have been failed on the assumption // it was cancelled. func TestRun_CascadeStopsWhenThePrerequisiteStateCASLoses(t *testing.T) { @@ -1343,26 +1343,26 @@ func TestRun_TriggerBatchNeedsNoRecoverySignal(t *testing.T) { assert.Equal(t, []string{"conclude"}, h.published) } -// Merging needs the same signal for the same reason: the write drops the batch +// Landing needs the same signal for the same reason: the write drops the batch // out of the speculating set, so nothing else would ever dispatch it. -func TestRun_CascadeDerivedBatchIsGivenARecoverySignalBeforeItMerges(t *testing.T) { +func TestRun_CascadeDerivedBatchIsGivenARecoverySignalBeforeItLands(t *testing.T) { ctrl := gomock.NewController(t) - merging := entity.Batch{ID: "q/batch/derived", Queue: "q", State: entity.BatchStateSpeculating, Version: 1} - h := newRunHarness(t, ctrl, &scriptedSpeculator{}, []entity.Batch{merging}) + landing := entity.Batch{ID: "q/batch/derived", Queue: "q", State: entity.BatchStateSpeculating, Version: 1} + h := newRunHarness(t, ctrl, &scriptedSpeculator{}, []entity.Batch{landing}) h.noBuildsDispatched() // No dependencies, so the passed path has nothing left to settle. - passedPath := entity.SpeculationPath{Head: merging.ID} - h.pathSets.EXPECT().Get(gomock.Any(), merging.ID).Return(entity.SpeculationPathSet{ - Head: merging.ID, + passedPath := entity.SpeculationPath{Head: landing.ID} + h.pathSets.EXPECT().Get(gomock.Any(), landing.ID).Return(entity.SpeculationPathSet{ + Head: landing.ID, Paths: []entity.SpeculationPathEntry{entryFor(passedPath, entity.SpeculationPathStatusPassed)}, Version: 1, }, nil).AnyTimes() var publishedBeforeWrite []string h.batches.EXPECT(). - Update(gomock.Any(), updateTo{id: merging.ID, state: entity.BatchStateMerging}, int32(1), int32(2)). + Update(gomock.Any(), updateTo{id: landing.ID, state: entity.BatchStateLanding}, int32(1), int32(2)). DoAndReturn(func(context.Context, entity.Batch, int32, int32) error { publishedBeforeWrite = append([]string(nil), h.published...) return nil @@ -1372,7 +1372,7 @@ func TestRun_CascadeDerivedBatchIsGivenARecoverySignalBeforeItMerges(t *testing. require.NoError(t, h.run(head)) assert.Equal(t, []string{"speculate"}, publishedBeforeWrite) - assert.Equal(t, []string{"speculate", "submitqueue-merge"}, h.published) + assert.Equal(t, []string{"speculate", "submitqueue-land"}, h.published) } // A cancelling path whose build is still running finishes only when CI actually @@ -1424,7 +1424,7 @@ func TestRun_ReportsPassedPathWhileWaiting(t *testing.T) { h := newRunHarness(t, ctrl, spec, []entity.Batch{memberHead()}) h.noBuildsDispatched() // dep1 has landed the way the path assumed; dep2 has not answered yet, so - // the head cannot merge but has nothing of its own left to run. + // the head cannot land but has nothing of its own left to run. h.batches.EXPECT().Get(gomock.Any(), dep1).Return(entity.Batch{ID: dep1, State: entity.BatchStateSucceeded}, nil) h.batches.EXPECT().Get(gomock.Any(), dep2).Return(entity.Batch{ID: dep2, State: entity.BatchStateSpeculating}, nil) @@ -1436,7 +1436,7 @@ func TestRun_ReportsPassedPathWhileWaiting(t *testing.T) { }, nil).AnyTimes() require.NoError(t, h.run(head)) - assert.Empty(t, h.published, "an unsettled head merges nowhere") + assert.Empty(t, h.published, "an unsettled head lands nowhere") require.Len(t, h.logs, 1) assert.Equal(t, "q/1", h.logs[0].RequestID) @@ -1524,10 +1524,10 @@ func TestRun_ReportsInvalidatedWhenTheDependencyFailsInTheSameRun(t *testing.T) assert.Contains(t, got, entity.RequestEventInvalidated) } -// A merge is decided on the same live passed path a wait would be reported +// A land is decided on the same live passed path a wait would be reported // from, so without the gate every landed request would carry a wait it never // had. -func TestRun_MergingHeadReportsSpeculatedAndNoWait(t *testing.T) { +func TestRun_LandingHeadReportsSpeculatedAndNoWait(t *testing.T) { ctrl := gomock.NewController(t) passed := pathOver(entity.DependencyAssumptionSucceeds, entity.DependencyAssumptionSucceeds) @@ -1541,7 +1541,7 @@ func TestRun_MergingHeadReportsSpeculatedAndNoWait(t *testing.T) { Version: 1, }, nil).AnyTimes() h.batches.EXPECT(). - Update(gomock.Any(), updateTo{id: head, state: entity.BatchStateMerging}, int32(1), int32(2)).Return(nil) + Update(gomock.Any(), updateTo{id: head, state: entity.BatchStateLanding}, int32(1), int32(2)).Return(nil) require.NoError(t, h.run(head)) @@ -1561,7 +1561,7 @@ func TestRun_BypassesUnsettledDependenciesWithFullCoverage(t *testing.T) { h := newRunHarness(t, ctrl, &scriptedSpeculator{}, []entity.Batch{memberHead()}) h.batches.EXPECT().Get(gomock.Any(), dep1).Return(entity.Batch{ID: dep1, State: entity.BatchStateSpeculating}, nil) - h.batches.EXPECT().Get(gomock.Any(), dep2).Return(entity.Batch{ID: dep2, State: entity.BatchStateMerging}, nil) + h.batches.EXPECT().Get(gomock.Any(), dep2).Return(entity.Batch{ID: dep2, State: entity.BatchStateLanding}, nil) h.pathSets.EXPECT().Get(gomock.Any(), head).Return(entity.SpeculationPathSet{ Head: head, Paths: []entity.SpeculationPathEntry{ @@ -1573,11 +1573,11 @@ func TestRun_BypassesUnsettledDependenciesWithFullCoverage(t *testing.T) { Version: 1, }, nil).AnyTimes() h.batches.EXPECT(). - Update(gomock.Any(), updateTo{id: head, state: entity.BatchStateMerging}, int32(1), int32(2)).Return(nil) + Update(gomock.Any(), updateTo{id: head, state: entity.BatchStateLanding}, int32(1), int32(2)).Return(nil) require.NoError(t, h.run(head)) - assert.Equal(t, []string{"submitqueue-merge"}, h.published) + assert.Equal(t, []string{"submitqueue-land"}, h.published) assert.Zero(t, h.spec.calls) require.Len(t, h.logs, 1) assert.Equal(t, entity.RequestStatusSpeculated, h.logs[0].Status) @@ -1587,15 +1587,15 @@ func TestRun_BypassesUnsettledDependenciesWithFullCoverage(t *testing.T) { assert.EqualValues(t, 1, counter.Value()) } -// The merge stage publishes landing as its first act on the dispatch. Both +// The land stage publishes landing as its first act on the dispatch. Both // statuses are non-terminal, so the summary is decided on timestamp alone and // a speculated sent afterwards would beat the landing it precedes. -func TestRun_SpeculatedIsReportedBeforeTheMergeDispatch(t *testing.T) { +func TestRun_SpeculatedIsReportedBeforeTheLandDispatch(t *testing.T) { ctrl := gomock.NewController(t) passed := pathOver(entity.DependencyAssumptionSucceeds, entity.DependencyAssumptionSucceeds) h := newRunHarness(t, ctrl, &scriptedSpeculator{}, []entity.Batch{memberHead()}) - h.failPublishTo("submitqueue-merge") + h.failPublishTo("submitqueue-land") h.noBuildsDispatched() h.batches.EXPECT().Get(gomock.Any(), dep1).Return(entity.Batch{ID: dep1, State: entity.BatchStateSucceeded}, nil) h.batches.EXPECT().Get(gomock.Any(), dep2).Return(entity.Batch{ID: dep2, State: entity.BatchStateSucceeded}, nil) @@ -1605,7 +1605,7 @@ func TestRun_SpeculatedIsReportedBeforeTheMergeDispatch(t *testing.T) { Version: 1, }, nil).AnyTimes() h.batches.EXPECT(). - Update(gomock.Any(), updateTo{id: head, state: entity.BatchStateMerging}, int32(1), int32(2)).Return(nil) + Update(gomock.Any(), updateTo{id: head, state: entity.BatchStateLanding}, int32(1), int32(2)).Return(nil) require.Error(t, h.run(head)) diff --git a/submitqueue/orchestrator/controller/speculate/snapshot.go b/submitqueue/orchestrator/controller/speculate/snapshot.go index 2e43b3892..a1677d12d 100644 --- a/submitqueue/orchestrator/controller/speculate/snapshot.go +++ b/submitqueue/orchestrator/controller/speculate/snapshot.go @@ -33,7 +33,7 @@ type snapshot struct { // of one of them. batches map[string]entity.Batch // inFlight is the queue's in-flight batches in queue order, whatever their - // state. This is what the dispatch step walks: a merging or cancelling head + // state. This is what the dispatch step walks: a landing or cancelling head // is closed to new work, but its paths still hold CI slots and their // observations still need persisting. inFlight []entity.Batch diff --git a/submitqueue/orchestrator/controller/speculate/speculate.go b/submitqueue/orchestrator/controller/speculate/speculate.go index ec7c90425..9385e7ada 100644 --- a/submitqueue/orchestrator/controller/speculate/speculate.go +++ b/submitqueue/orchestrator/controller/speculate/speculate.go @@ -79,12 +79,12 @@ func NewController( // // - Created: the batch is admitted first, which makes it visible to the // Speculator (proposals may only target Speculating heads). Reaching an -// outcome on it in the same run is safe: a merge needs a passed path, and +// outcome on it in the same run is safe: a land needs a passed path, and // a head admitted this instant has no paths at all. // - Already terminal: its conclude publish is repeated in case a previous // one was lost — idempotent on the batch ID — and the run that follows is // how dependents learn of an outcome no run has seen yet (a batch -// finalized by another stage, e.g. the merge signal recording a landed +// finalized by another stage, e.g. the land signal recording a landed // push, was never seen breaking the paths that bet against it). // // Everything else — funding paths, cancelling broken ones, driving a @@ -137,11 +137,11 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er } } - // A Merging batch has left the set finalize walks, so a message naming it + // A Landing batch has left the set finalize walks, so a message naming it // is the only thing that will look at it again. - if batch.State == entity.BatchStateMerging { - metrics.NamedCounter(c.metricsScope, opName, "self_heal_merging", 1) - if err := c.dispatchMerge(ctx, batch); err != nil { + if batch.State == entity.BatchStateLanding { + metrics.NamedCounter(c.metricsScope, opName, "self_heal_landing", 1) + if err := c.dispatchLand(ctx, batch); err != nil { return c.attributed(err, entity.BatchSubject(batch.ID)) } } @@ -209,7 +209,7 @@ func (c *Controller) fanout(ctx context.Context, batchID, queue string) error { // // Callers choose msgID, because this controller publishes for two different // kinds of reason. A hand-off that happens once in a batch's life — dispatching -// it to merge, concluding it — names its cause with publish.IntentID, so a +// it to land, concluding it — names its cause with publish.IntentID, so a // redelivery that re-derives the same decision is deduplicated instead of // enacting it twice. A repeat-until-effective nudge — a dispatch re-sent until // the build stage records it, a fan-out repeated in case an earlier one was diff --git a/submitqueue/orchestrator/controller/speculate/speculate_test.go b/submitqueue/orchestrator/controller/speculate/speculate_test.go index 1fe4ef271..19aaf5b35 100644 --- a/submitqueue/orchestrator/controller/speculate/speculate_test.go +++ b/submitqueue/orchestrator/controller/speculate/speculate_test.go @@ -155,7 +155,7 @@ func newProcHarness(t *testing.T, ctrl *gomock.Controller, publishErr error) *pr registry, err := consumer.NewTopicRegistry([]consumer.TopicConfig{ {Key: topickey.TopicKeyBuild, Name: "build", Queue: q}, - {Key: topickey.TopicKeyMerge, Name: "submitqueue-merge", Queue: q}, + {Key: topickey.TopicKeyLand, Name: "submitqueue-land", Queue: q}, {Key: topickey.TopicKeyConclude, Name: "conclude", Queue: q}, {Key: topickey.TopicKeySpeculate, Name: "speculate", Queue: q}, {Key: topickey.TopicKeyLog, Name: "log", Queue: q}, @@ -204,7 +204,7 @@ func TestProcess_AdmitsCreatedBatch(t *testing.T) { h.listsInFlight() require.NoError(t, h.process(t, ctrl, batch.ID)) - assert.Empty(t, h.published, "a batch cannot merge on the message that admitted it") + assert.Empty(t, h.published, "a batch cannot land on the message that admitted it") // Admission is the first thing a member hears after being batched: without // it the request reads "batched" for the whole of speculation. @@ -276,19 +276,19 @@ func TestProcess_TerminalReplansQueue(t *testing.T) { "the dependent must be re-planned against the terminal outcome, which it can only be weighed against if the terminal batch comes too") } -// A Merging batch has left the speculating set, so a message naming it is the +// A Landing batch has left the speculating set, so a message naming it is the // only thing that will look at it again: it re-sends the dispatch to repair // one lost after the state write. -func TestProcess_MergingSelfHeals(t *testing.T) { +func TestProcess_LandingSelfHeals(t *testing.T) { ctrl := gomock.NewController(t) h := newProcHarness(t, ctrl, nil) - batch := testBatch(entity.BatchStateMerging) + batch := testBatch(entity.BatchStateLanding) h.batches.EXPECT().Get(gomock.Any(), batch.ID).Return(batch, nil) h.listsInFlight() require.NoError(t, h.process(t, ctrl, batch.ID)) - assert.Equal(t, []string{"submitqueue-merge"}, h.published) + assert.Equal(t, []string{"submitqueue-land"}, h.published) } func TestProcess_Errors(t *testing.T) { diff --git a/submitqueue/orchestrator/controller/start/BUILD.bazel b/submitqueue/orchestrator/controller/start/BUILD.bazel index b86bcd768..88ccf7618 100644 --- a/submitqueue/orchestrator/controller/start/BUILD.bazel +++ b/submitqueue/orchestrator/controller/start/BUILD.bazel @@ -24,7 +24,7 @@ go_test( embed = [":go_default_library"], deps = [ "//platform/base/change:go_default_library", - "//platform/base/mergestrategy:go_default_library", + "//platform/base/landstrategy:go_default_library", "//platform/base/messagequeue:go_default_library", "//platform/consumer:go_default_library", "//platform/consumer/mock:go_default_library", diff --git a/submitqueue/orchestrator/controller/start/start_test.go b/submitqueue/orchestrator/controller/start/start_test.go index 12b88450d..722321d01 100644 --- a/submitqueue/orchestrator/controller/start/start_test.go +++ b/submitqueue/orchestrator/controller/start/start_test.go @@ -23,7 +23,7 @@ import ( "github.com/stretchr/testify/require" "github.com/uber-go/tally" "github.com/uber/submitqueue/platform/base/change" - "github.com/uber/submitqueue/platform/base/mergestrategy" + "github.com/uber/submitqueue/platform/base/landstrategy" entityqueue "github.com/uber/submitqueue/platform/base/messagequeue" "github.com/uber/submitqueue/platform/consumer" consumermock "github.com/uber/submitqueue/platform/consumer/mock" @@ -114,7 +114,7 @@ func TestController_Process_Success(t *testing.T) { ID: "test-queue/123", Queue: "test-queue", Change: change.Change{URIs: []string{"github://github.example.com/uber/service/pull/456/abcdef0123456789abcdef0123456789abcdef01"}}, - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, }) require.NoError(t, controller.Process(context.Background(), delivery)) @@ -155,7 +155,7 @@ func TestController_Process_ConstructsRequestWithStateAndVersion(t *testing.T) { ID: "test-queue/42", Queue: "test-queue", Change: change.Change{URIs: []string{"github://github.example.com/uber/service/pull/1/abcdef0123456789abcdef0123456789abcdef01"}}, - LandStrategy: mergestrategy.MergeStrategySquashRebase, + LandStrategy: landstrategy.StrategySquashRebase, } delivery := makeDelivery(t, ctrl, landRequest) require.NoError(t, controller.Process(context.Background(), delivery)) @@ -171,11 +171,11 @@ func TestController_Process_ConstructsRequestWithStateAndVersion(t *testing.T) { func TestController_Process_AllStrategies(t *testing.T) { tests := []struct { name string - strategy mergestrategy.MergeStrategy + strategy landstrategy.Strategy }{ - {"rebase", mergestrategy.MergeStrategyRebase}, - {"squash rebase", mergestrategy.MergeStrategySquashRebase}, - {"merge", mergestrategy.MergeStrategyMerge}, + {"rebase", landstrategy.StrategyRebase}, + {"squash rebase", landstrategy.StrategySquashRebase}, + {"merge", landstrategy.StrategyMerge}, } for _, tt := range tests { @@ -203,7 +203,7 @@ func TestController_Process_PublishFailure(t *testing.T) { ID: "test-queue/123", Queue: "test-queue", Change: change.Change{URIs: []string{"github://github.example.com/uber/service/pull/1/789abc1234567890abcdef1234567890abcdef12"}}, - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, }) assert.Error(t, controller.Process(context.Background(), delivery)) @@ -223,7 +223,7 @@ func TestController_Process_StorageFailure(t *testing.T) { ID: "test-queue/123", Queue: "test-queue", Change: change.Change{URIs: []string{"github://github.example.com/uber/service/pull/1/789abc1234567890abcdef1234567890abcdef12"}}, - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, }) err := controller.Process(context.Background(), delivery) @@ -245,7 +245,7 @@ func TestController_Process_AlreadyExistsSucceeds(t *testing.T) { ID: "test-queue/123", Queue: "test-queue", Change: change.Change{URIs: []string{"github://github.example.com/uber/service/pull/1/789abc1234567890abcdef1234567890abcdef12"}}, - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, }) require.NoError(t, controller.Process(context.Background(), delivery)) diff --git a/submitqueue/orchestrator/controller/validate/BUILD.bazel b/submitqueue/orchestrator/controller/validate/BUILD.bazel index af59d4255..2ce85e37c 100644 --- a/submitqueue/orchestrator/controller/validate/BUILD.bazel +++ b/submitqueue/orchestrator/controller/validate/BUILD.bazel @@ -7,9 +7,9 @@ go_library( visibility = ["//visibility:public"], deps = [ "//api/base/change/protopb:go_default_library", - "//api/base/mergestrategy/protopb:go_default_library", + "//api/base/landstrategy/protopb:go_default_library", "//api/runway/messagequeue:go_default_library", - "//platform/base/mergestrategy:go_default_library", + "//platform/base/landstrategy:go_default_library", "//platform/consumer:go_default_library", "//platform/metrics:go_default_library", "//platform/publish:go_default_library", @@ -28,10 +28,10 @@ go_test( srcs = ["validate_test.go"], embed = [":go_default_library"], deps = [ - "//api/base/mergestrategy/protopb:go_default_library", + "//api/base/landstrategy/protopb:go_default_library", "//api/runway/messagequeue:go_default_library", "//platform/base/change:go_default_library", - "//platform/base/mergestrategy:go_default_library", + "//platform/base/landstrategy:go_default_library", "//platform/base/messagequeue:go_default_library", "//platform/consumer:go_default_library", "//platform/consumer/mock:go_default_library", diff --git a/submitqueue/orchestrator/controller/validate/validate.go b/submitqueue/orchestrator/controller/validate/validate.go index 9eacab3cf..95b53f758 100644 --- a/submitqueue/orchestrator/controller/validate/validate.go +++ b/submitqueue/orchestrator/controller/validate/validate.go @@ -22,9 +22,9 @@ import ( "github.com/uber-go/tally" changepb "github.com/uber/submitqueue/api/base/change/protopb" - strategypb "github.com/uber/submitqueue/api/base/mergestrategy/protopb" + strategypb "github.com/uber/submitqueue/api/base/landstrategy/protopb" runwaymq "github.com/uber/submitqueue/api/runway/messagequeue" - "github.com/uber/submitqueue/platform/base/mergestrategy" + "github.com/uber/submitqueue/platform/base/landstrategy" "github.com/uber/submitqueue/platform/consumer" coremetrics "github.com/uber/submitqueue/platform/metrics" "github.com/uber/submitqueue/platform/publish" @@ -38,8 +38,8 @@ import ( // Controller handles validate queue messages. // It consumes requests, performs local validation checks (duplicate detection via the change store -// and change metadata fetch), then kicks off the asynchronous merge-conflict check by publishing the -// full check request to runway's merge-conflict-check queue. Validation logic is extensible to +// and change metadata fetch), then kicks off the asynchronous land-conflict check by publishing the +// full check request to runway's land-conflict-check queue. Validation logic is extensible to // support additional checks. Implements consumer.Controller. type Controller struct { logger *zap.SugaredLogger @@ -57,8 +57,8 @@ type Controller struct { var _ consumer.Controller = (*Controller)(nil) // NewController creates a new validate controller for the orchestrator. -// runwayTopicKey is the runway-owned topic the merge-conflict check request is -// published to (TopicKeyMergeConflictCheck). +// runwayTopicKey is the runway-owned topic the land-conflict check request is +// published to (TopicKeyLandConflictCheck). // validators is an optional factory for custom validation checks; pass nil to skip. func NewController( logger *zap.SugaredLogger, @@ -86,7 +86,7 @@ func NewController( // Process processes a validate delivery from the queue. // Runs duplicate detection, change metadata fetch, and change claiming, then kicks off the -// asynchronous merge-conflict check by publishing the full check request to runway. +// asynchronous land-conflict check by publishing the full check request to runway. // Returns nil to ack (success or non-retryable rejection), error to nack (retry). func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) error { msg := delivery.Message() @@ -142,7 +142,7 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er } // Report that validation has begun. This stage is not instantaneous — the - // merge-conflict check below is an async round trip to runway — so without + // land-conflict check below is an async round trip to runway — so without // this the request reads "started" for the whole of it. No occurrence: a // request is validated once, and a redelivery is a retry of that one event. logEntry := entity.NewRequestStatusLog(request.Queue, request.ID, entity.RequestStatusValidating, 0, "", nil) @@ -209,7 +209,7 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er } // Claim each URI in the change store with its provider details. The claim is - // created here — after duplicate detection and the merge/provider checks — so a + // created here — after duplicate detection, change-provider lookup, and custom validation — so a // rejected request never leaves a claim, and the record is written once with its // details (immutable thereafter; no separate enrichment update). Create is // idempotent per (queue, uri, request_id), so redelivery is a no-op. @@ -218,15 +218,15 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er return fmt.Errorf("failed to claim change records for request %s: %w", request.ID, err) } - // Kick off the asynchronous merge-conflict check: hand the full check request - // to runway via its merge-conflict-check queue, keyed by the request id (the + // Kick off the asynchronous land-conflict check: hand the full check request + // to runway via its land-conflict-check queue, keyed by the request id (the // client-owned correlation id) so a redelivery republishes the same id and the // result correlates straight back. At validate time the check is a single step // (candidate vs target branch). - req := &runwaymq.MergeRequest{ + req := &runwaymq.LandRequest{ Id: request.ID, QueueName: request.Queue, - Steps: []*runwaymq.MergeStep{ + Steps: []*runwaymq.LandStep{ { StepId: request.ID, Change: &changepb.Change{Uris: request.Change.URIs}, @@ -234,12 +234,12 @@ func (c *Controller) Process(ctx context.Context, delivery consumer.Delivery) er }, }, } - if err := c.publishMergeCheck(ctx, req); err != nil { + if err := c.publishLandConflictCheck(ctx, req); err != nil { coremetrics.NamedCounter(c.metricsScope, "process", "publish_errors", 1) - return fmt.Errorf("failed to publish to runway merge-conflict-check: %w", err) + return fmt.Errorf("failed to publish to runway land-conflict-check: %w", err) } - c.logger.Infow("published merge conflict check to runway", + c.logger.Infow("published land conflict check to runway", "request_id", request.ID, "topic_key", c.runwayTopicKey, ) @@ -309,16 +309,16 @@ func (c *Controller) checkDuplicate(ctx context.Context, store storage.Storage, return "", nil } -// publishMergeCheck serializes the runway check request and publishes it to the -// runway merge-conflict-check topic, partitioned by queue. +// publishLandConflictCheck serializes the runway check request and publishes it to the +// runway land-conflict-check topic, partitioned by queue. // // The correlation ID is the message ID with no cause: a request is checked once, // so a redelivery that re-asks is meant to dedup rather than have Runway run the // same check twice. -func (c *Controller) publishMergeCheck(ctx context.Context, req *runwaymq.MergeRequest) error { +func (c *Controller) publishLandConflictCheck(ctx context.Context, req *runwaymq.LandRequest) error { payload, err := runwaymq.Marshal(req) if err != nil { - return fmt.Errorf("failed to serialize merge conflict check request: %w", err) + return fmt.Errorf("failed to serialize land conflict check request: %w", err) } if err := publish.Message(ctx, c.registry, c.runwayTopicKey, publish.IntentID(req.GetId()), payload, req.GetQueueName()); err != nil { @@ -328,16 +328,16 @@ func (c *Controller) publishMergeCheck(ctx context.Context, req *runwaymq.MergeR return nil } -// toProtoStrategy maps the shared mergestrategy.MergeStrategy entity to the proto +// toProtoStrategy maps the shared landstrategy.Strategy entity to the proto // Strategy enum carried on the wire. An unknown strategy maps to DEFAULT, letting // runway apply the queue's configured default. -func toProtoStrategy(s mergestrategy.MergeStrategy) strategypb.Strategy { +func toProtoStrategy(s landstrategy.Strategy) strategypb.Strategy { switch s { - case mergestrategy.MergeStrategyRebase: + case landstrategy.StrategyRebase: return strategypb.Strategy_REBASE - case mergestrategy.MergeStrategySquashRebase: + case landstrategy.StrategySquashRebase: return strategypb.Strategy_SQUASH_REBASE - case mergestrategy.MergeStrategyMerge: + case landstrategy.StrategyMerge: return strategypb.Strategy_MERGE default: return strategypb.Strategy_DEFAULT diff --git a/submitqueue/orchestrator/controller/validate/validate_test.go b/submitqueue/orchestrator/controller/validate/validate_test.go index 4778d408c..69979d2dc 100644 --- a/submitqueue/orchestrator/controller/validate/validate_test.go +++ b/submitqueue/orchestrator/controller/validate/validate_test.go @@ -22,10 +22,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/uber-go/tally" - strategypb "github.com/uber/submitqueue/api/base/mergestrategy/protopb" + strategypb "github.com/uber/submitqueue/api/base/landstrategy/protopb" runwaymq "github.com/uber/submitqueue/api/runway/messagequeue" "github.com/uber/submitqueue/platform/base/change" - "github.com/uber/submitqueue/platform/base/mergestrategy" + "github.com/uber/submitqueue/platform/base/landstrategy" entityqueue "github.com/uber/submitqueue/platform/base/messagequeue" "github.com/uber/submitqueue/platform/consumer" consumermock "github.com/uber/submitqueue/platform/consumer/mock" @@ -126,7 +126,7 @@ func newTestController( registry, err := consumer.NewTopicRegistry( []consumer.TopicConfig{ - {Key: runwaymq.TopicKeyMergeConflictCheck, Name: "merge-conflict-check", Queue: mockQ}, + {Key: runwaymq.TopicKeyLandConflictCheck, Name: "land-conflict-check", Queue: mockQ}, {Key: topickey.TopicKeyLog, Name: "log", Queue: mockQ}, }, ) @@ -136,7 +136,7 @@ func newTestController( cpFactory := changeprovidermock.NewMockFactory(ctrl) cpFactory.EXPECT().For(gomock.Any()).Return(cp, nil).AnyTimes() - return NewController(logger, scope, staticStorageFactory{store: store}, registry, cpFactory, nil, runwaymq.TopicKeyMergeConflictCheck, topickey.TopicKeyValidate, "orchestrator-validate") + return NewController(logger, scope, staticStorageFactory{store: store}, registry, cpFactory, nil, runwaymq.TopicKeyLandConflictCheck, topickey.TopicKeyValidate, "orchestrator-validate") } func TestNewController(t *testing.T) { @@ -145,7 +145,7 @@ func TestNewController(t *testing.T) { ID: "test-queue/123", Queue: "test-queue", Change: change.Change{URIs: []string{"github://github.example.com/uber/service/pull/456/abcdef0123456789abcdef0123456789abcdef01"}}, - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, State: entity.RequestStateStarted, Version: 1, } @@ -165,7 +165,7 @@ func TestController_Process_Success(t *testing.T) { ID: "test-queue/123", Queue: "test-queue", Change: change.Change{URIs: []string{"github://github.example.com/uber/service/pull/456/abcdef0123456789abcdef0123456789abcdef01"}}, - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, State: entity.RequestStateStarted, Version: 1, } @@ -180,8 +180,8 @@ func TestController_Process_Success(t *testing.T) { require.NoError(t, controller.Process(context.Background(), delivery)) } -// TestController_Process_PublishesCheckToRunway verifies the full merge-conflict -// check request is published to runway's merge-conflict-check queue (keyed by +// TestController_Process_PublishesCheckToRunway verifies the full land-conflict +// check request is published to runway's land-conflict-check queue (keyed by // the request id, the client-owned correlation id) on the happy path. func TestController_Process_PublishesCheckToRunway(t *testing.T) { ctrl := gomock.NewController(t) @@ -190,7 +190,7 @@ func TestController_Process_PublishesCheckToRunway(t *testing.T) { ID: "test-queue/123", Queue: "test-queue", Change: change.Change{URIs: []string{"github://github.example.com/uber/service/pull/456/abcdef0123456789abcdef0123456789abcdef01"}}, - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, State: entity.RequestStateStarted, Version: 1, } @@ -204,7 +204,7 @@ func TestController_Process_PublishesCheckToRunway(t *testing.T) { mockPub := queuemock.NewMockPublisher(ctrl) mockPub.EXPECT().Publish(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( func(ctx context.Context, topic string, msg entityqueue.Message) error { - if topic != "merge-conflict-check" { + if topic != "land-conflict-check" { return nil } gotTopic = topic @@ -216,7 +216,7 @@ func TestController_Process_PublishesCheckToRunway(t *testing.T) { mockQ.EXPECT().Publisher().Return(mockPub).AnyTimes() registry, err := consumer.NewTopicRegistry( []consumer.TopicConfig{ - {Key: runwaymq.TopicKeyMergeConflictCheck, Name: "merge-conflict-check", Queue: mockQ}, + {Key: runwaymq.TopicKeyLandConflictCheck, Name: "land-conflict-check", Queue: mockQ}, {Key: topickey.TopicKeyLog, Name: "log", Queue: mockQ}, }, ) @@ -224,7 +224,7 @@ func TestController_Process_PublishesCheckToRunway(t *testing.T) { cpFactory := changeprovidermock.NewMockFactory(ctrl) cpFactory.EXPECT().For(gomock.Any()).Return(&mockChangeProvider{}, nil).AnyTimes() - controller := NewController(logger, tally.NoopScope, staticStorageFactory{store: store}, registry, cpFactory, nil, runwaymq.TopicKeyMergeConflictCheck, topickey.TopicKeyValidate, "orchestrator-validate") + controller := NewController(logger, tally.NoopScope, staticStorageFactory{store: store}, registry, cpFactory, nil, runwaymq.TopicKeyLandConflictCheck, topickey.TopicKeyValidate, "orchestrator-validate") msg := entityqueue.NewMessage(request.ID, requestIDPayload(t, request.ID), request.Queue, nil) delivery := consumermock.NewMockDelivery(ctrl) @@ -234,8 +234,8 @@ func TestController_Process_PublishesCheckToRunway(t *testing.T) { require.NoError(t, controller.Process(context.Background(), delivery)) // Full payload published to runway, keyed by the request id (the correlation id). - assert.Equal(t, "merge-conflict-check", gotTopic) - got := &runwaymq.MergeRequest{} + assert.Equal(t, "land-conflict-check", gotTopic) + got := &runwaymq.LandRequest{} require.NoError(t, runwaymq.Unmarshal(gotPayload, got)) assert.Equal(t, request.ID, got.Id) assert.Equal(t, request.Queue, got.QueueName) @@ -259,7 +259,7 @@ func TestController_Process_ClaimsChangeRecordsWithDetails(t *testing.T) { ID: "test-queue/123", Queue: "test-queue", Change: change.Change{URIs: []string{uri}}, - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, State: entity.RequestStateStarted, Version: 1, } @@ -320,7 +320,7 @@ func TestController_Process_PublishFailure(t *testing.T) { ID: "test-queue/123", Queue: "test-queue", Change: change.Change{URIs: []string{"github://github.example.com/uber/service/pull/1/789abc1234567890abcdef1234567890abcdef12"}}, - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, State: entity.RequestStateStarted, Version: 1, } @@ -468,7 +468,7 @@ func TestController_Process_DuplicateDetection(t *testing.T) { ID: newRequestID, Queue: queueName, Change: change.Change{URIs: uris}, - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, State: entity.RequestStateStarted, Version: 1, } @@ -535,7 +535,7 @@ func TestController_Process_ChangeStoreQueryFailure(t *testing.T) { ID: "test-queue/123", Queue: "test-queue", Change: change.Change{URIs: []string{"github://github.example.com/uber/service/pull/1/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}, - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, State: entity.RequestStateStarted, Version: 1, } @@ -602,7 +602,7 @@ func TestController_Process_CustomValidatorPasses(t *testing.T) { ID: "test-queue/123", Queue: "test-queue", Change: change.Change{URIs: []string{"github://uber/service/pull/456/abcdef0123456789abcdef0123456789abcdef01"}}, - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, State: entity.RequestStateStarted, Version: 1, } @@ -617,7 +617,7 @@ func TestController_Process_CustomValidatorPasses(t *testing.T) { mockQ.EXPECT().Publisher().Return(mockPub).AnyTimes() registry, err := consumer.NewTopicRegistry( []consumer.TopicConfig{ - {Key: runwaymq.TopicKeyMergeConflictCheck, Name: "merge-conflict-check", Queue: mockQ}, + {Key: runwaymq.TopicKeyLandConflictCheck, Name: "land-conflict-check", Queue: mockQ}, {Key: topickey.TopicKeyLog, Name: "log", Queue: mockQ}, }, ) @@ -634,7 +634,7 @@ func TestController_Process_CustomValidatorPasses(t *testing.T) { QueueName: request.Queue, }).Return(mockValidator, nil) - controller := NewController(logger, tally.NoopScope, staticStorageFactory{store: store}, registry, cpFactory, mockValidatorFactory, runwaymq.TopicKeyMergeConflictCheck, topickey.TopicKeyValidate, "orchestrator-validate") + controller := NewController(logger, tally.NoopScope, staticStorageFactory{store: store}, registry, cpFactory, mockValidatorFactory, runwaymq.TopicKeyLandConflictCheck, topickey.TopicKeyValidate, "orchestrator-validate") msg := entityqueue.NewMessage(request.ID, requestIDPayload(t, request.ID), request.Queue, nil) delivery := consumermock.NewMockDelivery(ctrl) @@ -651,7 +651,7 @@ func TestController_Process_CustomValidatorFails(t *testing.T) { ID: "test-queue/123", Queue: "test-queue", Change: change.Change{URIs: []string{"github://uber/service/pull/456/abcdef0123456789abcdef0123456789abcdef01"}}, - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, State: entity.RequestStateStarted, Version: 1, } @@ -676,7 +676,7 @@ func TestController_Process_CustomValidatorFails(t *testing.T) { mockQ.EXPECT().Publisher().Return(mockPub).AnyTimes() registry, err := consumer.NewTopicRegistry( []consumer.TopicConfig{ - {Key: runwaymq.TopicKeyMergeConflictCheck, Name: "merge-conflict-check", Queue: mockQ}, + {Key: runwaymq.TopicKeyLandConflictCheck, Name: "land-conflict-check", Queue: mockQ}, {Key: topickey.TopicKeyLog, Name: "log", Queue: mockQ}, }, ) @@ -693,7 +693,7 @@ func TestController_Process_CustomValidatorFails(t *testing.T) { QueueName: request.Queue, }).Return(mockValidator, nil) - controller := NewController(logger, tally.NoopScope, staticStorageFactory{store: store}, registry, cpFactory, mockValidatorFactory, runwaymq.TopicKeyMergeConflictCheck, topickey.TopicKeyValidate, "orchestrator-validate") + controller := NewController(logger, tally.NoopScope, staticStorageFactory{store: store}, registry, cpFactory, mockValidatorFactory, runwaymq.TopicKeyLandConflictCheck, topickey.TopicKeyValidate, "orchestrator-validate") msg := entityqueue.NewMessage(request.ID, requestIDPayload(t, request.ID), request.Queue, nil) delivery := consumermock.NewMockDelivery(ctrl) @@ -715,7 +715,7 @@ func TestController_Process_CustomValidatorFailure_TerminationPublishFails(t *te ID: "test-queue/123", Queue: "test-queue", Change: change.Change{URIs: []string{"github://uber/service/pull/456/abcdef0123456789abcdef0123456789abcdef01"}}, - LandStrategy: mergestrategy.MergeStrategyRebase, + LandStrategy: landstrategy.StrategyRebase, State: entity.RequestStateStarted, Version: 1, } @@ -740,7 +740,7 @@ func TestController_Process_CustomValidatorFailure_TerminationPublishFails(t *te mockQ.EXPECT().Publisher().Return(mockPub).AnyTimes() registry, err := consumer.NewTopicRegistry( []consumer.TopicConfig{ - {Key: runwaymq.TopicKeyMergeConflictCheck, Name: "merge-conflict-check", Queue: mockQ}, + {Key: runwaymq.TopicKeyLandConflictCheck, Name: "land-conflict-check", Queue: mockQ}, {Key: topickey.TopicKeyLog, Name: "log", Queue: mockQ}, }, ) @@ -756,7 +756,7 @@ func TestController_Process_CustomValidatorFailure_TerminationPublishFails(t *te QueueName: request.Queue, }).Return(mockValidator, nil) - controller := NewController(zaptest.NewLogger(t).Sugar(), tally.NoopScope, staticStorageFactory{store: store}, registry, cpFactory, mockValidatorFactory, runwaymq.TopicKeyMergeConflictCheck, topickey.TopicKeyValidate, "orchestrator-validate") + controller := NewController(zaptest.NewLogger(t).Sugar(), tally.NoopScope, staticStorageFactory{store: store}, registry, cpFactory, mockValidatorFactory, runwaymq.TopicKeyLandConflictCheck, topickey.TopicKeyValidate, "orchestrator-validate") msg := entityqueue.NewMessage(request.ID, requestIDPayload(t, request.ID), request.Queue, nil) delivery := consumermock.NewMockDelivery(ctrl) delivery.EXPECT().Message().Return(msg).AnyTimes() diff --git a/submitqueue/orchestrator/pipeline.go b/submitqueue/orchestrator/pipeline.go index 0e8732799..4c0deb989 100644 --- a/submitqueue/orchestrator/pipeline.go +++ b/submitqueue/orchestrator/pipeline.go @@ -43,9 +43,9 @@ import ( "github.com/uber/submitqueue/submitqueue/orchestrator/controller/conclude" "github.com/uber/submitqueue/submitqueue/orchestrator/controller/dependencyanalysis" "github.com/uber/submitqueue/submitqueue/orchestrator/controller/dlq" - "github.com/uber/submitqueue/submitqueue/orchestrator/controller/merge" - "github.com/uber/submitqueue/submitqueue/orchestrator/controller/mergeconflictsignal" - "github.com/uber/submitqueue/submitqueue/orchestrator/controller/mergesignal" + "github.com/uber/submitqueue/submitqueue/orchestrator/controller/land" + "github.com/uber/submitqueue/submitqueue/orchestrator/controller/landconflictsignal" + "github.com/uber/submitqueue/submitqueue/orchestrator/controller/landsignal" "github.com/uber/submitqueue/submitqueue/orchestrator/controller/speculate" "github.com/uber/submitqueue/submitqueue/orchestrator/controller/start" "github.com/uber/submitqueue/submitqueue/orchestrator/controller/validate" @@ -95,9 +95,9 @@ type Deps struct { // // Pipeline: // -// start → cancel → validate ⇢ (runway) ⇢ mergeconflictsignal → batch → speculate → build → buildsignal ─┐ +// start → cancel → validate ⇢ (runway) ⇢ landconflictsignal → batch → speculate → build → buildsignal ─┐ // ↑ ↘ ↻ poll │ -// │ merge → conclude │ +// │ land → conclude │ // │ │ │ // └─────┴───────────────────────┘ var Stages = []pipeline.Stage[Deps]{ @@ -128,21 +128,21 @@ var Stages = []pipeline.Stage[Deps]{ Name: "validate", ConsumerGroup: "orchestrator", New: func(d Deps, sc pipeline.StageContext) (consumer.Controller, error) { - return validate.NewController(d.Logger, d.Scope, d.Storage, sc.Registry, d.ChangeProvider, d.Validator, runwaymq.TopicKeyMergeConflictCheck, sc.TopicKey, sc.ConsumerGroup), nil + return validate.NewController(d.Logger, d.Scope, d.Storage, sc.Registry, d.ChangeProvider, d.Validator, runwaymq.TopicKeyLandConflictCheck, sc.TopicKey, sc.ConsumerGroup), nil }, DLQ: func(d Deps, sc pipeline.StageContext) (consumer.Controller, error) { return dlq.NewDLQRequestController(d.Logger, d.Scope, d.Storage, sc.Registry, dlq.DecodeRequestID, sc.TopicKey, sc.ConsumerGroup), nil }, }, { - Key: runwaymq.TopicKeyMergeConflictCheckSignal, - Name: "merge-conflict-check-signal", + Key: runwaymq.TopicKeyLandConflictCheckSignal, + Name: "land-conflict-check-signal", ConsumerGroup: "orchestrator", New: func(d Deps, sc pipeline.StageContext) (consumer.Controller, error) { - return mergeconflictsignal.NewController(d.Logger, d.Scope, d.Storage, sc.Registry, sc.TopicKey, sc.ConsumerGroup), nil + return landconflictsignal.NewController(d.Logger, d.Scope, d.Storage, sc.Registry, sc.TopicKey, sc.ConsumerGroup), nil }, DLQ: func(d Deps, sc pipeline.StageContext) (consumer.Controller, error) { - return dlq.NewDLQMergeConflictSignalController(d.Logger, d.Scope, d.Storage, sc.Registry, sc.TopicKey, sc.ConsumerGroup), nil + return dlq.NewDLQLandConflictSignalController(d.Logger, d.Scope, d.Storage, sc.Registry, sc.TopicKey, sc.ConsumerGroup), nil }, }, { @@ -201,25 +201,25 @@ var Stages = []pipeline.Stage[Deps]{ }, }, { - Key: topickey.TopicKeyMerge, - Name: "submitqueue-merge", + Key: topickey.TopicKeyLand, + Name: "submitqueue-land", ConsumerGroup: "orchestrator", New: func(d Deps, sc pipeline.StageContext) (consumer.Controller, error) { - return merge.NewController(d.Logger, d.Scope, d.Storage, sc.Registry, runwaymq.TopicKeyMerge, sc.TopicKey, sc.ConsumerGroup), nil + return land.NewController(d.Logger, d.Scope, d.Storage, sc.Registry, runwaymq.TopicKeyLand, sc.TopicKey, sc.ConsumerGroup), nil }, DLQ: func(d Deps, sc pipeline.StageContext) (consumer.Controller, error) { return dlq.NewDLQBatchController(d.Logger, d.Scope, d.Storage, sc.Registry, sc.TopicKey, sc.ConsumerGroup), nil }, }, { - Key: runwaymq.TopicKeyMergeSignal, - Name: "merge-signal", + Key: runwaymq.TopicKeyLandSignal, + Name: "land-signal", ConsumerGroup: "orchestrator", New: func(d Deps, sc pipeline.StageContext) (consumer.Controller, error) { - return mergesignal.NewController(d.Logger, d.Scope, d.Storage, sc.Registry, sc.TopicKey, sc.ConsumerGroup), nil + return landsignal.NewController(d.Logger, d.Scope, d.Storage, sc.Registry, sc.TopicKey, sc.ConsumerGroup), nil }, DLQ: func(d Deps, sc pipeline.StageContext) (consumer.Controller, error) { - return dlq.NewDLQMergeSignalController(d.Logger, d.Scope, d.Storage, sc.Registry, sc.TopicKey, sc.ConsumerGroup), nil + return dlq.NewDLQLandSignalController(d.Logger, d.Scope, d.Storage, sc.Registry, sc.TopicKey, sc.ConsumerGroup), nil }, }, { @@ -257,10 +257,10 @@ var Stages = []pipeline.Stage[Deps]{ var PublishOnlyTopics = []pipeline.PublishOnlyTopic{ // Log: the orchestrator emits request-log entries; the gateway consumes them. {Key: topickey.TopicKeyLog, Name: "log"}, - // Merge-conflict check: the orchestrator publishes check requests to runway. - {Key: runwaymq.TopicKeyMergeConflictCheck, Name: "merge-conflict-check"}, - // Merge: the orchestrator publishes merge requests to runway. - {Key: runwaymq.TopicKeyMerge, Name: "runway-merge"}, + // Land-conflict check: the orchestrator publishes check requests to runway. + {Key: runwaymq.TopicKeyLandConflictCheck, Name: "land-conflict-check"}, + // Land: the orchestrator publishes land requests to runway. + {Key: runwaymq.TopicKeyLand, Name: "runway-land"}, } // Controllers holds the orchestrator's RPC-facing controllers, constructed diff --git a/test/e2e/runway/BUILD.bazel b/test/e2e/runway/BUILD.bazel index 6b98e4ad3..59f3294f6 100644 --- a/test/e2e/runway/BUILD.bazel +++ b/test/e2e/runway/BUILD.bazel @@ -18,7 +18,7 @@ go_test( ], deps = [ "//api/base/change/protopb:go_default_library", - "//api/base/mergestrategy/protopb:go_default_library", + "//api/base/landstrategy/protopb:go_default_library", "//api/runway/messagequeue:go_default_library", "//api/runway/messagequeue/protopb:go_default_library", "//platform/base/messagequeue:go_default_library", diff --git a/test/e2e/runway/harness_test.go b/test/e2e/runway/harness_test.go index 1fdf43e32..5afe031e6 100644 --- a/test/e2e/runway/harness_test.go +++ b/test/e2e/runway/harness_test.go @@ -14,7 +14,7 @@ package e2e_test -// Helpers for the Runway e2e suite: the client side of the merge contract +// Helpers for the Runway e2e suite: the client side of the land contract // (publish a request, await the correlated result) plus the request builders // the tests vary. @@ -26,7 +26,7 @@ import ( "github.com/stretchr/testify/require" changepb "github.com/uber/submitqueue/api/base/change/protopb" - strategypb "github.com/uber/submitqueue/api/base/mergestrategy/protopb" + strategypb "github.com/uber/submitqueue/api/base/landstrategy/protopb" runwaymq "github.com/uber/submitqueue/api/runway/messagequeue" entityqueue "github.com/uber/submitqueue/platform/base/messagequeue" extqueue "github.com/uber/submitqueue/platform/extension/messagequeue" @@ -36,10 +36,10 @@ import ( // other side of that contract, so it addresses topics by the same keys the // service registers (see service/runway/server/main.go newTopicRegistry). var ( - topicMerge = runwaymq.TopicKeyMerge.String() - topicMergeSignal = runwaymq.TopicKeyMergeSignal.String() - topicCheck = runwaymq.TopicKeyMergeConflictCheck.String() - topicCheckSignal = runwaymq.TopicKeyMergeConflictCheckSignal.String() + topicLand = runwaymq.TopicKeyLand.String() + topicLandSignal = runwaymq.TopicKeyLandSignal.String() + topicCheck = runwaymq.TopicKeyLandConflictCheck.String() + topicCheckSignal = runwaymq.TopicKeyLandConflictCheckSignal.String() ) // reconciledPrefix is what the DLQ reconciler prepends to the reason of a @@ -52,30 +52,30 @@ const reconciledPrefix = "dead-lettered: " // reconciled reports whether a result came from the DLQ reconciler rather than // from the primary controller. -func reconciled(result *runwaymq.MergeResult) bool { +func reconciled(result *runwaymq.LandResult) bool { return strings.HasPrefix(result.GetReason(), reconciledPrefix) } // baseURI is a well-formed change URI. Tests append a "sq-fake=" marker -// to steer the fake merger; without one it merges cleanly. +// to steer the fake lander; without one it lands cleanly. const baseURI = "github://github.example.com/uber/runway-e2e/pull/1/abcdef0123456789abcdef0123456789abcdef01" -// Marker tokens the fake merger recognizes (runway/extension/merger/fake). +// Marker tokens the fake lander recognizes (runway/extension/lander/fake). const ( - markerConflict = "merge-conflict" - markerInvalid = "merge-invalid" - markerError = "merge-error" + markerConflict = "land-conflict" + markerInvalid = "land-invalid" + markerError = "land-error" ) -// markedURI returns baseURI carrying the given fake-merger marker token. +// markedURI returns baseURI carrying the given fake-lander marker token. func markedURI(token string) string { return baseURI + "?sq-fake=" + token } -// observedResult is a MergeResult as it arrived on a signal topic, with the +// observedResult is a LandResult as it arrived on a signal topic, with the // envelope fields the tests assert on alongside the payload. type observedResult struct { - result *runwaymq.MergeResult + result *runwaymq.LandResult messageID string partitionKey string } @@ -118,7 +118,7 @@ func (o *observer) await(t *testing.T, ctx context.Context, id string) observedR require.NotNil(t, delivery, "nil delivery on %s", o.topic) msg := delivery.Message() - result := &runwaymq.MergeResult{} + result := &runwaymq.LandResult{} require.NoError(t, runwaymq.Unmarshal(msg.Payload, result), "failed to decode result on %s", o.topic) require.NoError(t, delivery.Ack(ctx), "failed to ack result on %s", o.topic) @@ -152,13 +152,13 @@ func (s *RunwayE2ESuite) observe(topic string) *observer { return &observer{topic: topic, ch: ch} } -// publish sends a merge request to one of Runway's inbound topics, partitioned +// publish sends a land request to one of Runway's inbound topics, partitioned // by queue name exactly as the orchestrator publishes it. -func (s *RunwayE2ESuite) publish(topic string, request *runwaymq.MergeRequest) { +func (s *RunwayE2ESuite) publish(topic string, request *runwaymq.LandRequest) { t := s.T() payload, err := runwaymq.Marshal(request) - require.NoError(t, err, "failed to marshal merge request") + require.NoError(t, err, "failed to marshal land request") s.publishRaw(topic, request.GetId(), request.GetQueueName(), payload) } @@ -173,22 +173,22 @@ func (s *RunwayE2ESuite) publishRaw(topic, id, partitionKey string, payload []by s.log.Logf("published %s to %s (partition %s)", id, topic, partitionKey) } -// mergeRequest builds a request with a unique correlation id. Ids must be +// landRequest builds a request with a unique correlation id. Ids must be // unique across the suite: the queue deduplicates publishes on // (topic, partition, id), and the correlation id is the message id on both the // request and its result, so a reused id silently drops a publish. -func (s *RunwayE2ESuite) mergeRequest(queue string, steps ...*runwaymq.MergeStep) *runwaymq.MergeRequest { +func (s *RunwayE2ESuite) landRequest(queue string, steps ...*runwaymq.LandStep) *runwaymq.LandRequest { s.seq++ - return &runwaymq.MergeRequest{ + return &runwaymq.LandRequest{ Id: fmt.Sprintf("%s/%d", queue, s.seq), QueueName: queue, Steps: steps, } } -// step builds one merge step applying the given URIs with REBASE. -func step(id string, uris ...string) *runwaymq.MergeStep { - return &runwaymq.MergeStep{ +// step builds one land step applying the given URIs with REBASE. +func step(id string, uris ...string) *runwaymq.LandStep { + return &runwaymq.LandStep{ StepId: id, Change: &changepb.Change{Uris: uris}, Strategy: strategypb.Strategy_REBASE, diff --git a/test/e2e/runway/suite_test.go b/test/e2e/runway/suite_test.go index 868bd9922..8239cdd22 100644 --- a/test/e2e/runway/suite_test.go +++ b/test/e2e/runway/suite_test.go @@ -31,17 +31,17 @@ package e2e_test // // The stack runs the Runway service plus a queue MySQL. Runway is consumer-only // — it has no gateway — so the suite drives it the way its real client does: it -// publishes a MergeRequest to an inbound merge topic and listens on the -// corresponding signal topic for the MergeResult carrying its correlation id. +// publishes a LandRequest to an inbound land topic and listens on the +// corresponding signal topic for the LandResult carrying its correlation id. // That is the seam no other test covers: topic wiring, the protojson round-trip // over the wire, partition-key propagation, the ack-and-publish-FAILED path for // expected outcomes, and DLQ reconciliation. // -// The service runs with MERGER=fake, so what a merge *does* is out of scope +// The service runs with LANDER=fake, so what a land *does* is out of scope // here; the fake's marker tokens only pick which outcome Runway has to carry -// back. Merge behavior proper — strategies, real conflicts, staleness, author +// back. Land behavior proper — strategies, real conflicts, staleness, author // attribution — is covered against a real git remote in -// runway/extension/merger/git/git_merger_test.go. +// runway/extension/lander/git/git_lander_test.go. import ( "context" @@ -69,7 +69,7 @@ type RunwayE2ESuite struct { queue extqueue.Queue // Signal-topic observers, standing in for the client awaiting its results. - mergeSignal *observer + landSignal *observer checkSignal *observer // seq mints unique correlation ids across the suite. @@ -88,9 +88,9 @@ func (s *RunwayE2ESuite) SetupSuite() { s.log.Logf("Starting Runway e2e test suite using docker-compose") // Outcomes have to be steerable from the request payload, so the service - // runs the marker-driven fake merger instead of noop or git. Set before the + // runs the marker-driven fake lander instead of noop or git. Set before the // stack is created: ComposeStack snapshots the environment at construction. - t.Setenv("SQ_RUNWAY_MERGER", "fake") + t.Setenv("SQ_RUNWAY_LANDER", "fake") composeFile := testutil.Runfile("service/runway/server/docker-compose.yml") s.stack = testutil.NewComposeStack(t, s.log, s.ctx, composeFile, "e2e-runway", @@ -118,7 +118,7 @@ func (s *RunwayE2ESuite) SetupSuite() { require.NoError(t, err, "failed to create queue client") t.Cleanup(func() { s.queue.Close() }) - s.mergeSignal = s.observe(topicMergeSignal) + s.landSignal = s.observe(topicLandSignal) s.checkSignal = s.observe(topicCheckSignal) s.log.Logf("Runway e2e test suite ready") @@ -130,27 +130,27 @@ func (s *RunwayE2ESuite) TearDownSuite() { s.log.Logf("Tearing down Runway e2e test suite") } -// TestMerge_HappyPath_PublishesMergedResult drives the committing merge: a -// two-step request in, a SUCCEEDED result out on the merge-signal topic. It +// TestLand_HappyPath_PublishesLandedResult drives the committing land: a +// two-step request in, a SUCCEEDED result out on the land-signal topic. It // asserts the whole envelope the client depends on — the correlation id echoed, // every step attributable by its step id in application order, a produced // revision per step, and the partition key carried from request to result so // the client's per-queue ordering survives the round trip. -func (s *RunwayE2ESuite) TestMerge_HappyPath_PublishesMergedResult() { +func (s *RunwayE2ESuite) TestLand_HappyPath_PublishesLandedResult() { t := s.T() - const queue = "e2e-runway/merge" + const queue = "e2e-runway/land" - request := s.mergeRequest(queue, + request := s.landRequest(queue, step("base", baseURI), step("candidate", baseURI), ) - s.publish(topicMerge, request) + s.publish(topicLand, request) - observed := s.mergeSignal.await(t, s.ctx, request.GetId()) + observed := s.landSignal.await(t, s.ctx, request.GetId()) result := observed.result assert.Equal(t, runwaypb.Outcome_SUCCEEDED, result.GetOutcome()) - assert.Empty(t, result.GetReason(), "a successful merge carries no reason") + assert.Empty(t, result.GetReason(), "a successful land carries no reason") assert.Equal(t, queue, observed.partitionKey, "the result must stay on the request's partition") @@ -159,18 +159,18 @@ func (s *RunwayE2ESuite) TestMerge_HappyPath_PublishesMergedResult() { assert.Equal(t, "candidate", result.GetSteps()[1].GetStepId()) for i, stepResult := range result.GetSteps() { assert.NotEmpty(t, stepResult.GetOutputs(), - "step %d of a committing merge must report the revision it produced", i) + "step %d of a committing land must report the revision it produced", i) } } -// TestMergeConflictCheck_HappyPath_PublishesMergeableResult drives the dry-run +// TestLandConflictCheck_HappyPath_PublishesLandableResult drives the dry-run // check on its own topic pair. The distinguishing assertion against the -// committing merge is that a check commits nothing, so it reports no outputs. -func (s *RunwayE2ESuite) TestMergeConflictCheck_HappyPath_PublishesMergeableResult() { +// committing land is that a check commits nothing, so it reports no outputs. +func (s *RunwayE2ESuite) TestLandConflictCheck_HappyPath_PublishesLandableResult() { t := s.T() const queue = "e2e-runway/check" - request := s.mergeRequest(queue, step("candidate", baseURI)) + request := s.landRequest(queue, step("candidate", baseURI)) s.publish(topicCheck, request) observed := s.checkSignal.await(t, s.ctx, request.GetId()) @@ -203,7 +203,7 @@ func (s *RunwayE2ESuite) TestTerminalFailure_AcksAndSignalsFailed() { inbound string observer func() *observer }{ - {"merge", topicMerge, func() *observer { return s.mergeSignal }}, + {"land", topicLand, func() *observer { return s.landSignal }}, {"conflict check", topicCheck, func() *observer { return s.checkSignal }}, } @@ -213,7 +213,7 @@ func (s *RunwayE2ESuite) TestTerminalFailure_AcksAndSignalsFailed() { t := s.T() queue := "e2e-runway/failed" - request := s.mergeRequest(queue, step("candidate", markedURI(tt.marker))) + request := s.landRequest(queue, step("candidate", markedURI(tt.marker))) s.publish(topic.inbound, request) observed := topic.observer().await(t, s.ctx, request.GetId()) @@ -231,7 +231,7 @@ func (s *RunwayE2ESuite) TestTerminalFailure_AcksAndSignalsFailed() { } // TestUnexpectedFailure_ReconcilesFromDLQ covers the backstop. An unexpected -// merger fault is not a terminal outcome, so the controller does not answer it; +// lander fault is not a terminal outcome, so the controller does not answer it; // the consumer rejects the message to the dead-letter topic instead. Runway is // the sole responder on the client's correlation id, so a request that stopped // there would leave the client waiting forever — the DLQ reconciler is what @@ -240,10 +240,10 @@ func (s *RunwayE2ESuite) TestUnexpectedFailure_ReconcilesFromDLQ() { t := s.T() const queue = "e2e-runway/dlq" - request := s.mergeRequest(queue, step("candidate", markedURI(markerError))) - s.publish(topicMerge, request) + request := s.landRequest(queue, step("candidate", markedURI(markerError))) + s.publish(topicLand, request) - observed := s.mergeSignal.await(t, s.ctx, request.GetId()) + observed := s.landSignal.await(t, s.ctx, request.GetId()) assert.Equal(t, runwaypb.Outcome_FAILED, observed.result.GetOutcome()) assert.True(t, reconciled(observed.result), @@ -265,17 +265,17 @@ func (s *RunwayE2ESuite) TestUndecodablePayload_DropsWithoutSignalling() { t := s.T() const queue = "e2e-runway/undecodable" - mark := s.mergeSignal.mark() + mark := s.landSignal.mark() - s.publishRaw(topicMerge, "e2e-runway/garbage", queue, []byte("{not a merge request")) + s.publishRaw(topicLand, "e2e-runway/garbage", queue, []byte("{not a land request")) - sentinel := s.mergeRequest(queue, step("candidate", baseURI)) - s.publish(topicMerge, sentinel) + sentinel := s.landRequest(queue, step("candidate", baseURI)) + s.publish(topicLand, sentinel) - observed := s.mergeSignal.await(t, s.ctx, sentinel.GetId()) + observed := s.landSignal.await(t, s.ctx, sentinel.GetId()) assert.Equal(t, runwaypb.Outcome_SUCCEEDED, observed.result.GetOutcome()) - arrived := s.mergeSignal.newSince(mark) + arrived := s.landSignal.newSince(mark) require.Len(t, arrived, 1, "the undecodable payload must not signal; only the sentinel should have arrived") assert.Equal(t, sentinel.GetId(), arrived[0].result.GetId()) diff --git a/test/e2e/submitqueue/BUILD.bazel b/test/e2e/submitqueue/BUILD.bazel index 680b0f6d3..8055623c3 100644 --- a/test/e2e/submitqueue/BUILD.bazel +++ b/test/e2e/submitqueue/BUILD.bazel @@ -24,7 +24,7 @@ go_test( "@git//:templates", ], # The git suite drives the bare repository with the same pinned git the - # merger uses, rather than whatever the host has installed. + # lander uses, rather than whatever the host has installed. env = { "SUBMITQUEUE_TEST_GIT": "$(location @git//:git)", }, @@ -35,7 +35,7 @@ go_test( ], deps = [ "//api/base/change/protopb:go_default_library", - "//api/base/mergestrategy/protopb:go_default_library", + "//api/base/landstrategy/protopb:go_default_library", "//api/runway/messagequeue:go_default_library", "//api/submitqueue/gateway/protopb:go_default_library", "//api/submitqueue/orchestrator/protopb:go_default_library", diff --git a/test/e2e/submitqueue/git_suite_test.go b/test/e2e/submitqueue/git_suite_test.go index 321051ec0..3a1de0675 100644 --- a/test/e2e/submitqueue/git_suite_test.go +++ b/test/e2e/submitqueue/git_suite_test.go @@ -12,19 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Hermetic end-to-end coverage of a *real* merge. +// Hermetic end-to-end coverage of a *real* land. // -// The tier-1 suite (suite_test.go) runs Runway on the noop merger, so "landed" +// The tier-1 suite (suite_test.go) runs Runway on the noop lander, so "landed" // there proves the pipeline's choreography and nothing about git. This suite // points Runway at a bare repository on a shared volume and asserts against the // repository itself: what reached the target branch, in what order, and in how // many ref updates. // // It needs no credential, no network, and no account anywhere, because none of -// that is what the merge machinery depends on — which is what lets these +// that is what the land machinery depends on — which is what lets these // assertions gate a pull request. What it deliberately cannot cover is the half // that is specific to a change provider: reading change metadata, that -// provider's CI, and a real change being marked merged. Those need a repository +// provider's CI, and a real change being marked landed. Those need a repository // and a credential, so they are exercised by hand — see doc/howto/QUICKSTART.md. package e2e_test @@ -42,7 +42,7 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" changepb "github.com/uber/submitqueue/api/base/change/protopb" - mergestrategypb "github.com/uber/submitqueue/api/base/mergestrategy/protopb" + landstrategypb "github.com/uber/submitqueue/api/base/landstrategy/protopb" gatewaypb "github.com/uber/submitqueue/api/submitqueue/gateway/protopb" gitexectest "github.com/uber/submitqueue/platform/git/exectest" "github.com/uber/submitqueue/submitqueue/entity" @@ -50,16 +50,16 @@ import ( "google.golang.org/grpc" ) -// gitQueue is the queue wired to the git merger in -// service/submitqueue/demo/provider/git/merge.yaml. +// gitQueue is the queue wired to the git lander in +// service/submitqueue/demo/provider/git/land.yaml. const gitQueue = "e2e-git-queue" -// sandboxRemote is the host name used in git:// change URIs. The merger reads +// sandboxRemote is the host name used in git:// change URIs. The lander reads // the commit and ref out of the URI and reaches the repository through its own // configured remote, so this identifies the change rather than routing to it. const sandboxRemote = "git.example.com" -type GitMergeSuite struct { +type GitLandSuite struct { suite.Suite ctx context.Context log *testutil.TestLogger @@ -69,19 +69,19 @@ type GitMergeSuite struct { queueDB *sql.DB // git is the pinned git the test drives the bare repository with — the same - // build the merger uses inside the container. + // build the lander uses inside the container. git string - // bare is the host path of the repository Runway merges into. + // bare is the host path of the repository Runway lands into. bare string // work is a clone the test authors changes in, standing in for a developer. work string } -func TestGitMergeE2E(t *testing.T) { - suite.Run(t, new(GitMergeSuite)) +func TestGitLandE2E(t *testing.T) { + suite.Run(t, new(GitLandSuite)) } -func (s *GitMergeSuite) SetupSuite() { +func (s *GitLandSuite) SetupSuite() { t := s.T() s.ctx = context.Background() s.log = testutil.NewTestLogger(t) @@ -92,7 +92,7 @@ func (s *GitMergeSuite) SetupSuite() { t.Setenv("SQ_CONSUMER_GATE_DIR", t.TempDir()) // The bare repository lives in a host directory bind-mounted into Runway, so - // the test can seed it and read back exactly what the merger pushed. + // the test can seed it and read back exactly what the lander pushed. sandbox := t.TempDir() s.bare = filepath.Join(sandbox, "sandbox.git") s.work = filepath.Join(t.TempDir(), "work") @@ -103,13 +103,13 @@ func (s *GitMergeSuite) SetupSuite() { t.Setenv("SQ_RUNWAY_CHECKOUT_DIR", t.TempDir()) s.seedRepository() - // The committed example configuration is what selects the git merger, so + // The committed example configuration is what selects the git lander, so // the suite runs the real file rather than generating one. Runfiles are // symlinks into the execroot, which a container cannot follow through a // bind mount, so the files are staged as real copies first. t.Setenv("SQ_PROVIDER_CONFIG_DIR", s.stageProviderConfig()) - // The base stack plus an overlay naming only what differs: Runway's merge + // The base stack plus an overlay naming only what differs: Runway's land // target. Sharing the base file means a service added there reaches this // suite too, which a copied compose file would silently miss. composeFile := testutil.Runfile("service/submitqueue/docker-compose.yml") @@ -142,10 +142,10 @@ func (s *GitMergeSuite) SetupSuite() { require.NoError(t, err) s.gatewayClient = gatewaypb.NewSubmitQueueGatewayClient(conn) - s.log.Logf("git merge E2E suite ready (bare repo at %s)", s.bare) + s.log.Logf("git land E2E suite ready (bare repo at %s)", s.bare) } -func (s *GitMergeSuite) TearDownSuite() { +func (s *GitLandSuite) TearDownSuite() { if s.db != nil { s.db.Close() } @@ -156,7 +156,7 @@ func (s *GitMergeSuite) TearDownSuite() { // --- assertions against the repository itself --- -func (s *GitMergeSuite) TestLand_SingleChange_ReachesTheTargetBranch() { +func (s *GitLandSuite) TestLand_SingleChange_ReachesTheTargetBranch() { before := s.mainSHA() head := s.pushChange("feature/single", map[string]string{"single.txt": "single\n"}, "add single") @@ -171,8 +171,8 @@ func (s *GitMergeSuite) TestLand_SingleChange_ReachesTheTargetBranch() { s.Equal("single\n", s.fileOnMain("single.txt")) } -func (s *GitMergeSuite) TestLand_Stack_LandsInOrderInOneRefUpdate() { - // The property that distinguishes a submit queue from merging changes one +func (s *GitLandSuite) TestLand_Stack_LandsInOrderInOneRefUpdate() { + // The property that distinguishes a submit queue from landing changes one // at a time: a stack reaches the target as a single atomic ref update, so // no reader ever observes it half-landed. before := s.mainSHA() @@ -195,8 +195,8 @@ func (s *GitMergeSuite) TestLand_Stack_LandsInOrderInOneRefUpdate() { "the whole stack must reach the target in exactly one ref update") } -func (s *GitMergeSuite) TestLand_MovesEachChangeHeadBranchToItsLandedCommit() { - // What makes a provider mark a rebased change merged: its head branch is moved +func (s *GitLandSuite) TestLand_MovesEachChangeHeadBranchToItsLandedCommit() { + // What makes a provider mark a rebased change landed: its head branch is moved // to the commit the change became, so the head is reachable from the target. before := s.mainSHA() first := s.pushChange("feature/head-1", map[string]string{"h1.txt": "h1\n"}, "add h1") @@ -218,7 +218,7 @@ func (s *GitMergeSuite) TestLand_MovesEachChangeHeadBranchToItsLandedCommit() { s.True(s.isAncestorOfMain(s.branchSHA("feature/head-2"))) } -func (s *GitMergeSuite) TestLand_Conflict_FailsAndLeavesTheTargetUntouched() { +func (s *GitLandSuite) TestLand_Conflict_FailsAndLeavesTheTargetUntouched() { // Two changes editing the same line from the same base: the first lands, // the second cannot be replayed onto it. base := s.mainSHA() @@ -236,7 +236,7 @@ func (s *GitMergeSuite) TestLand_Conflict_FailsAndLeavesTheTargetUntouched() { s.Equal(loser, s.branchSHA("feature/conflict-b")) } -func (s *GitMergeSuite) TestLand_ResubmittedAfterLanding_IsRejectedAsStale() { +func (s *GitLandSuite) TestLand_ResubmittedAfterLanding_IsRejectedAsStale() { // Landing a change moves its head branch to the commit it became, so the // URI that was submitted no longer describes where that branch points. The // staleness check catches exactly that, which is what stops a change from @@ -259,11 +259,11 @@ func (s *GitMergeSuite) TestLand_ResubmittedAfterLanding_IsRejectedAsStale() { // land submits a request and returns its sqid. Repeated URIs are the stack, in // the order they must be applied. -func (s *GitMergeSuite) land(queue string, uris ...string) string { +func (s *GitLandSuite) land(queue string, uris ...string) string { resp, err := s.gatewayClient.Land(s.ctx, &gatewaypb.LandRequest{ Queue: queue, Change: &changepb.Change{Uris: uris}, - Strategy: mergestrategypb.Strategy_REBASE, + Strategy: landstrategypb.Strategy_REBASE, }) s.Require().NoError(err, "Land failed for queue %s", queue) s.Require().NotEmpty(resp.Sqid) @@ -272,7 +272,7 @@ func (s *GitMergeSuite) land(queue string, uris ...string) string { // requireStatus waits for the request to reach a terminal status and asserts // which one. Bazel's test timeout is the only deadline. -func (s *GitMergeSuite) requireStatus(sqid string, want entity.RequestStatus) { +func (s *GitLandSuite) requireStatus(sqid string, want entity.RequestStatus) { var got entity.RequestStatus pollUntil(persistPollInterval, func() bool { resp, err := s.gatewayClient.GetRequestSummaryByID(s.ctx, &gatewaypb.GetRequestSummaryByIDRequest{Sqid: sqid, Queue: gitQueue}) @@ -288,7 +288,7 @@ func (s *GitMergeSuite) requireStatus(sqid string, want entity.RequestStatus) { // uri builds the git:// change URI for a branch pinned at a commit. The ref is // percent-encoded so a branch name containing slashes stays one path segment. -func (s *GitMergeSuite) uri(branch, sha string) string { +func (s *GitLandSuite) uri(branch, sha string) string { ref := "refs/heads/" + branch return fmt.Sprintf("git://%s/sandbox/%s/%s", sandboxRemote, url.PathEscape(ref), sha) } @@ -297,10 +297,10 @@ func (s *GitMergeSuite) uri(branch, sha string) string { // stageProviderConfig copies the committed example configuration into a directory // the containers can bind-mount, and returns its path. -func (s *GitMergeSuite) stageProviderConfig() string { +func (s *GitLandSuite) stageProviderConfig() string { t := s.T() staged := t.TempDir() - for _, name := range []string{"merge.yaml", "profiles.yaml"} { + for _, name := range []string{"land.yaml", "profiles.yaml"} { contents, err := os.ReadFile(testutil.Runfile("service/submitqueue/demo/provider/git/" + name)) require.NoError(t, err, "reading example config %s", name) require.NoError(t, os.WriteFile(filepath.Join(staged, name), contents, 0o644)) @@ -308,9 +308,9 @@ func (s *GitMergeSuite) stageProviderConfig() string { return staged } -// seedRepository creates the bare repository Runway merges into, plus a working +// seedRepository creates the bare repository Runway lands into, plus a working // clone the test authors changes in. -func (s *GitMergeSuite) seedRepository() { +func (s *GitLandSuite) seedRepository() { t := s.T() s.runGit(filepath.Dir(s.bare), "init", "--bare", "-b", "main", s.bare) // Bare repositories do not log ref updates by default, and the reflog is @@ -326,7 +326,7 @@ func (s *GitMergeSuite) seedRepository() { s.runGit(s.work, "push", "origin", "main") } -func (s *GitMergeSuite) configureWorkClone() { +func (s *GitLandSuite) configureWorkClone() { for _, kv := range [][2]string{ {"user.name", "E2E Author"}, {"user.email", "author@example.com"}, @@ -342,13 +342,13 @@ func (s *GitMergeSuite) configureWorkClone() { // pushChange authors a change branched off the current target tip and pushes // it, returning its head SHA — all a change URI ever carries. -func (s *GitMergeSuite) pushChange(branch string, files map[string]string, message string) string { +func (s *GitLandSuite) pushChange(branch string, files map[string]string, message string) string { return s.pushChangeOnto("origin/main", branch, files, message) } // pushChangeOnto is pushChange based at an explicit start point, for building a // change that stacks on another rather than on the target. -func (s *GitMergeSuite) pushChangeOnto(base, branch string, files map[string]string, message string) string { +func (s *GitLandSuite) pushChangeOnto(base, branch string, files map[string]string, message string) string { s.runGit(s.work, "fetch", "origin") s.runGit(s.work, "checkout", "-B", branch, base) for path, contents := range files { @@ -361,18 +361,18 @@ func (s *GitMergeSuite) pushChangeOnto(base, branch string, files map[string]str } // mainSHA is the current tip of the target branch on the bare repository. -func (s *GitMergeSuite) mainSHA() string { +func (s *GitLandSuite) mainSHA() string { return s.runGit(s.bare, "rev-parse", "refs/heads/main") } // branchSHA is the current tip of a change's head branch. -func (s *GitMergeSuite) branchSHA(branch string) string { +func (s *GitLandSuite) branchSHA(branch string) string { return s.runGit(s.bare, "rev-parse", "refs/heads/"+branch) } // shasSince lists the commits added to the target since a known point, oldest // first. -func (s *GitMergeSuite) shasSince(since string) []string { +func (s *GitLandSuite) shasSince(since string) []string { out := s.runGit(s.bare, "rev-list", "--reverse", since+"..refs/heads/main") return strings.Fields(out) } @@ -380,7 +380,7 @@ func (s *GitMergeSuite) shasSince(since string) []string { // subjectsSince lists the messages of the commits added to the target since a // known point, oldest first — the readable form of what landed and in what // order. -func (s *GitMergeSuite) subjectsSince(since string) []string { +func (s *GitLandSuite) subjectsSince(since string) []string { out := s.runGit(s.bare, "log", "--reverse", "--format=%s", since+"..refs/heads/main") var subjects []string for _, line := range strings.Split(out, "\n") { @@ -392,13 +392,13 @@ func (s *GitMergeSuite) subjectsSince(since string) []string { } // fileOnMain reads a file's contents at the target tip. -func (s *GitMergeSuite) fileOnMain(path string) string { +func (s *GitLandSuite) fileOnMain(path string) string { return s.runGit(s.bare, "show", "refs/heads/main:"+path) + "\n" } // isAncestorOfMain reports whether a commit is reachable from the target — the -// property a provider reads to decide a change has merged. -func (s *GitMergeSuite) isAncestorOfMain(sha string) bool { +// property a provider reads to decide a change has landed. +func (s *GitLandSuite) isAncestorOfMain(sha string) bool { cmd := exec.Command(s.git, "merge-base", "--is-ancestor", sha, "refs/heads/main") cmd.Dir = s.bare return cmd.Run() == nil @@ -407,7 +407,7 @@ func (s *GitMergeSuite) isAncestorOfMain(sha string) bool { // mainRefUpdateCount is how many times the target branch has been updated, // read from the bare repository's reflog. One land must cost exactly one, // however many changes it carried. -func (s *GitMergeSuite) mainRefUpdateCount() int { +func (s *GitLandSuite) mainRefUpdateCount() int { out := s.runGit(s.bare, "reflog", "show", "--format=%H", "refs/heads/main") count := 0 for _, line := range strings.Split(out, "\n") { @@ -420,7 +420,7 @@ func (s *GitMergeSuite) mainRefUpdateCount() int { // runGit runs the pinned git and returns its trimmed stdout, failing the test // on a non-zero exit. -func (s *GitMergeSuite) runGit(dir string, args ...string) string { +func (s *GitLandSuite) runGit(dir string, args ...string) string { s.T().Helper() cmd := exec.Command(s.git, args...) cmd.Dir = dir diff --git a/test/e2e/submitqueue/harness_test.go b/test/e2e/submitqueue/harness_test.go index 4ee147f49..5d044007a 100644 --- a/test/e2e/submitqueue/harness_test.go +++ b/test/e2e/submitqueue/harness_test.go @@ -33,7 +33,7 @@ import ( "github.com/stretchr/testify/require" "github.com/uber-go/tally" changepb "github.com/uber/submitqueue/api/base/change/protopb" - mergestrategypb "github.com/uber/submitqueue/api/base/mergestrategy/protopb" + landstrategypb "github.com/uber/submitqueue/api/base/landstrategy/protopb" gatewaypb "github.com/uber/submitqueue/api/submitqueue/gateway/protopb" "github.com/uber/submitqueue/platform/consumer" "github.com/uber/submitqueue/platform/extension/consumergate" @@ -73,7 +73,7 @@ func (s *E2EIntegrationSuite) land(queue string, uris ...string) request { resp, err := s.gatewayClient.Land(s.ctx, &gatewaypb.LandRequest{ Queue: queue, Change: &changepb.Change{Uris: uris}, - Strategy: mergestrategypb.Strategy_REBASE, + Strategy: landstrategypb.Strategy_REBASE, }) require.NoError(t, err, "Land failed for queue %s", queue) require.NotEmpty(t, resp.Sqid, "Land returned an empty sqid for queue %s", queue) diff --git a/test/e2e/submitqueue/suite_test.go b/test/e2e/submitqueue/suite_test.go index 2ecc0973d..c7bdcf1e6 100644 --- a/test/e2e/submitqueue/suite_test.go +++ b/test/e2e/submitqueue/suite_test.go @@ -255,7 +255,7 @@ func (s *E2EIntegrationSuite) TestConflictAnalyzerFailure_ReconcilesFromDLQ() { // TestLand_HappyPath_ReachesLanded drives a single request through the whole // pipeline to terminal success on the fully-hermetic e2e-test-queue (no // conflicts, fake build succeeds, noop runway signals SUCCEEDED for both the -// merge-conflict check and the merge). It asserts three views: the black-box +// land-conflict check and the land). It asserts three views: the black-box // terminal request summary, the public GetRequestHistoryByID timeline, and the internal RequestState // in the operating store. // @@ -315,31 +315,31 @@ func (s *E2EIntegrationSuite) TestLand_HappyPath_ReachesLanded() { "operating store should show request %s in terminal state landed", req.sqid) } -// TestDependentBatch_BypassesMergingDependency proves that a dependency still -// waiting on Runway is unresolved for strict merge but can be bypassed once +// TestDependentBatch_BypassesLandingDependency proves that a dependency still +// waiting on Runway is unresolved for strict land but can be bypassed once // passed paths cover both of its possible outcomes. -func (s *E2EIntegrationSuite) TestDependentBatch_BypassesMergingDependency() { +func (s *E2EIntegrationSuite) TestDependentBatch_BypassesLandingDependency() { t := s.T() const queue = "e2e-chain-queue" - const gateGroup = "runway-merge" - gateTopic := runwaymq.TopicKeyMerge.String() + const gateGroup = "runway-land" + gateTopic := runwaymq.TopicKeyLand.String() - s.closeGate(gateGroup, queue, "e2e: hold the lead merge so the dependent finishes building first") + s.closeGate(gateGroup, queue, "e2e: hold the lead land so the dependent finishes building first") // Reopen even if an assertion below fails, so teardown does not stop the // stack with a delivery still parked. Opening twice is a no-op. defer s.openGate(gateGroup, queue) lead := s.land(queue, "github://github.example.com/uber/e2e-chain/pull/1/abcdef0123456789abcdef0123456789abcdef01") - s.log.Logf("Landed lead request %s; awaiting its merge to park", lead.sqid) + s.log.Logf("Landed lead request %s; awaiting its land to park", lead.sqid) - // The merge request is keyed by batch, so name the batch to prove the - // parked delivery is this request's merge and not some other. + // The land request is keyed by batch, so name the batch to prove the + // parked delivery is this request's land and not some other. leadBatch := s.awaitBatchID(lead) parked := s.awaitParked(gateGroup, gateTopic, leadBatch) - assert.Equal(t, queue, parked.PartitionKey, "merge request should be partitioned by queue") + assert.Equal(t, queue, parked.PartitionKey, "land request should be partitioned by queue") - // The lead is provably stopped mid-merge. A request landed now serializes + // The lead is provably stopped mid-land. A request landed now serializes // behind it. dependent := s.land(queue, "github://github.example.com/uber/e2e-chain/pull/2/1234567890abcdef1234567890abcdef12345678") dependentBatch := s.awaitBatchID(dependent) @@ -352,13 +352,13 @@ func (s *E2EIntegrationSuite) TestDependentBatch_BypassesMergingDependency() { require.Contains(t, got.Dependencies, leadBatch, "batch %s must depend on the in-flight %s for this test to exercise anything", dependentBatch, leadBatch) - // Both paths pass while the lead is still parked. A Merging dependency is - // unresolved because its merge can fail, so the durable Merging state proves - // the dependent advanced through complete coverage rather than strict merge. - s.awaitBatchState(queue, dependentBatch, entity.BatchStateMerging) - s.log.Logf("Dependent %s bypassed merging batch %s", dependent.sqid, leadBatch) + // Both paths pass while the lead is still parked. A Landing dependency is + // unresolved because its land can fail, so the durable Landing state proves + // the dependent advanced through complete coverage rather than strict land. + s.awaitBatchState(queue, dependentBatch, entity.BatchStateLanding) + s.log.Logf("Dependent %s bypassed landing batch %s", dependent.sqid, leadBatch) - // Start: the lead merges, and its fan-out is now the only thing that can + // Start: the lead lands, and its fan-out is now the only thing that can // move the dependent. s.openGate(gateGroup, queue) s.awaitUnparked(gateGroup, gateTopic, leadBatch) @@ -370,7 +370,7 @@ func (s *E2EIntegrationSuite) TestDependentBatch_BypassesMergingDependency() { } // TestDependentBatch_BypassedHeadLandsFirst proves the bypass does not just -// dispatch a merge: the dependent lands while its dependency is still held +// dispatch a land: the dependent lands while its dependency is still held // mid-build, and only then does the dependency proceed. func (s *E2EIntegrationSuite) TestDependentBatch_BypassedHeadLandsFirst() { t := s.T() @@ -389,8 +389,8 @@ func (s *E2EIntegrationSuite) TestDependentBatch_BypassedHeadLandsFirst() { require.NotEqual(t, heldBatch, followerBatch) // The follower builds with and without the held leader while the leader's - // own build is parked; complete coverage hands it to the merge stage. - s.awaitBatchState(queue, followerBatch, entity.BatchStateMerging) + // own build is parked; complete coverage hands it to the land stage. + s.awaitBatchState(queue, followerBatch, entity.BatchStateLanding) s.awaitStatus(follower, entity.RequestStatusLanded) assert.Equal(t, entity.RequestStatusSpeculating, s.mustStatus(lead), "the follower must land while its dependency is still held") @@ -408,7 +408,7 @@ func (s *E2EIntegrationSuite) TestDependentBatch_BypassedHeadLandsFirst() { // TestDependentBatch_NoBypassWhenCoverageIsIncomplete proves the complement: // with only the "dependency succeeds" side passed, a head waits for its -// dependency to resolve and merges strictly, never dispatching ahead of it. +// dependency to resolve and lands strictly, never dispatching ahead of it. // // Partial coverage is seeded directly: the follower's batch is stranded in // Created (build held), its "succeeds" path is written as passed, and a @@ -444,15 +444,15 @@ func (s *E2EIntegrationSuite) TestDependentBatch_NoBypassWhenCoverageIsIncomplet s.awaitBatchID(trigger) // The run that admits the follower also reports the wait: its one passed - // path covers only one of the lead's two outcomes, so it cannot merge + // path covers only one of the lead's two outcomes, so it cannot land // until the lead resolves. s.awaitEvent(follower, entity.RequestEventWaiting) // "No bypass" must not be read off the follower's batch state: waiting is // a recorded event, not a moment, and the lead is ungated — it can land - // before this test looks, at which point the seeded path is mergeable - // *strictly* and the batch rightly moves to Merging. The bypass would - // show up differently: the follower merging while the lead is still + // before this test looks, at which point the seeded path is landable + // *strictly* and the batch rightly moves to Landing. The bypass would + // show up differently: the follower landing while the lead is still // unresolved. Assert the true invariant instead — the follower only lands // after the lead has landed. s.awaitStatus(lead, entity.RequestStatusLanded) @@ -465,6 +465,86 @@ func (s *E2EIntegrationSuite) TestDependentBatch_NoBypassWhenCoverageIsIncomplet s.awaitStatus(trigger, entity.RequestStatusLanded) } +// TestDependentBatch_IsWokenByTheLandAhead proves that a batch waiting on +// another is woken when that one lands — the edge CODEM-303 was silently +// dropping. +// +// A landed batch fans out to speculate so its dependents can re-plan. That +// message used to reuse the bare batch ID, which the batch controller had +// already published to the same topic and partition when the batch was +// created. The queue deduplicates against rows it has not collected yet, +// consumed ones included, so the wake-up was reported as a success, stored +// nothing, and never arrived. +// +// Ordinarily something else re-plans the queue soon enough to hide that. This +// test removes every other source of a wake-up, as stop → observe → start: +// +// 1. Stop: close the gate for runway-land on this queue, before landing, so +// the lead batch cannot complete its land. +// 2. Land the lead. It runs to the land hand-off and parks there. +// 3. Land the dependent. The queue's analyzer serializes conservatively, so +// its batch depends on the lead's, which is in-flight (Landing counts). +// 4. Observe: wait for the dependent to record "waiting" — its speculative +// build has already passed, so its own build signals are finished. From +// here the only thing that can advance it is the lead landing. +// 5. Start: open the gate. The lead lands and fans out. +// +// The dependent reaching "landed" is therefore attributable to the fan-out +// alone. Against the old code it rests at "speculating" and the suite runs to +// Bazel's timeout, which is how the harness reports a pipeline that stalled. +func (s *E2EIntegrationSuite) TestDependentBatch_IsWokenByTheLandAhead() { + t := s.T() + + const queue = "e2e-chain-queue" + const gateGroup = "runway-land" + gateTopic := runwaymq.TopicKeyLand.String() + + s.closeGate(gateGroup, queue, "e2e: hold the lead land so the dependent finishes building first") + // Reopen even if an assertion below fails, so teardown does not stop the + // stack with a delivery still parked. Opening twice is a no-op. + defer s.openGate(gateGroup, queue) + + lead := s.land(queue, "github://github.example.com/uber/e2e-chain/pull/1/abcdef0123456789abcdef0123456789abcdef01") + s.log.Logf("Landed lead request %s; awaiting its land to park", lead.sqid) + + // The land request is keyed by batch, so name the batch to prove the + // parked delivery is this request's land and not some other. + leadBatch := s.awaitBatchID(lead) + parked := s.awaitParked(gateGroup, gateTopic, leadBatch) + assert.Equal(t, queue, parked.PartitionKey, "land request should be partitioned by queue") + + // The lead is provably stopped mid-land. A request landed now serializes + // behind it. + dependent := s.land(queue, "github://github.example.com/uber/e2e-chain/pull/2/1234567890abcdef1234567890abcdef12345678") + dependentBatch := s.awaitBatchID(dependent) + require.NotEqual(t, leadBatch, dependentBatch, "the two requests must be carried by different batches") + + leadState, err := s.appStorage.For(queue) + require.NoError(t, err) + got, err := leadState.GetBatchStore().Get(s.ctx, dependentBatch) + require.NoError(t, err, "failed to read the dependent batch") + require.Contains(t, got.Dependencies, leadBatch, + "batch %s must depend on the in-flight %s for this test to exercise anything", dependentBatch, leadBatch) + + // Its speculative build passes while the lead is still parked, so by the + // time the gate opens the dependent has no build signals left to wake it. + // That rest is an event, not a status: a batch blocked on a dependency has + // not finished speculating, so it stays "speculating" until it can land. + s.awaitEvent(dependent, entity.RequestEventWaiting) + s.log.Logf("Dependent %s has passed its build and waits only on %s", dependent.sqid, leadBatch) + + // Start: the lead lands, and its fan-out is now the only thing that can + // move the dependent. + s.openGate(gateGroup, queue) + s.awaitUnparked(gateGroup, gateTopic, leadBatch) + + s.awaitStatus(lead, entity.RequestStatusLanded) + s.awaitStatus(dependent, entity.RequestStatusLanded) + + assert.Equal(t, entity.RequestStateLanded, s.terminalState(dependent), + "the dependent must land once the batch it waited on landed") +} + // TestReadAPIs validates all five request read endpoints against receipts // created through the public Land API. func (s *E2EIntegrationSuite) TestReadAPIs() { @@ -603,13 +683,13 @@ func (s *E2EIntegrationSuite) TestCancelRequest_InvalidSqid() { // TestCancel_CaughtPreBatch_NeverLands drives the deterministic cancel // scenario from doc/rfc/consumer-gate.md as stop → observe → start: the -// consumer gate stops runway's merge-conflict-check controller before the +// consumer gate stops runway's land-conflict-check controller before the // request's check message can be answered, so the request is provably held // pre-batch while the cancel lands. The change must never reach the repo. // -// 1. Stop: close the gate for runway-mergeconflictcheck, scoped to this +// 1. Stop: close the gate for runway-landconflictcheck, scoped to this // queue's partition, before landing — exact by construction, no timing. -// 2. Land: the orchestrator runs the request to the merge-conflict-check +// 2. Land: the orchestrator runs the request to the land-conflict-check // hand-off; runway's subscriber delivers the check and the gate parks it. // 3. Observe: awaiting the parked record proves the controller is stopped and // holding exactly this request's check (there is otherwise no signal @@ -629,10 +709,10 @@ func (s *E2EIntegrationSuite) TestCancel_CaughtPreBatch_NeverLands() { t := s.T() const queue = "e2e-cancel-queue" - const gateGroup = "runway-mergeconflictcheck" - gateTopic := runwaymq.TopicKeyMergeConflictCheck.String() + const gateGroup = "runway-landconflictcheck" + gateTopic := runwaymq.TopicKeyLandConflictCheck.String() - s.closeGate(gateGroup, queue, "e2e: hold merge-conflict check to catch cancel pre-batch") + s.closeGate(gateGroup, queue, "e2e: hold land-conflict check to catch cancel pre-batch") // Reopen even if an assertion below fails, so teardown does not stop the // stack with a delivery still parked. Opening twice is a no-op. defer s.openGate(gateGroup, queue) @@ -677,7 +757,7 @@ func (s *E2EIntegrationSuite) TestCancel_CaughtPreBatch_NeverLands() { // A batch delivery that is retried after its ack was lost must not enrol the // request into a second batch. Both batches would be analyzed, promoted and -// admitted, and both would merge the same change. +// admitted, and both would land the same change. // // The build gate is what makes the redelivery land in the window that matters: // with the build for the first batch held, the request cannot reach a terminal diff --git a/test/integration/extension/messagequeue/mysql/queue_test.go b/test/integration/extension/messagequeue/mysql/queue_test.go index a87503019..b0fb4a6df 100644 --- a/test/integration/extension/messagequeue/mysql/queue_test.go +++ b/test/integration/extension/messagequeue/mysql/queue_test.go @@ -779,9 +779,9 @@ func (s *SQLQueueIntegrationSuite) TestDedupOutlivesConsumption() { // Naming the cause is what gets it through. require.NoError(t, publisher.Publish(s.ctx, topic, - entityqueue.NewMessage("batch-1/merged", []byte("woken"), "queue-1", nil))) + entityqueue.NewMessage("batch-1/landed", []byte("woken"), "queue-1", nil))) second := receive(t, deliveryChan) - assert.Equal(t, "batch-1/merged", second.Message().ID) + assert.Equal(t, "batch-1/landed", second.Message().ID) assert.Equal(t, []byte("woken"), second.Message().Payload) require.NoError(t, second.Ack(s.ctx)) } diff --git a/test/integration/submitqueue/extension/storage/BUILD.bazel b/test/integration/submitqueue/extension/storage/BUILD.bazel index 7585d20d3..d09e2b4ca 100644 --- a/test/integration/submitqueue/extension/storage/BUILD.bazel +++ b/test/integration/submitqueue/extension/storage/BUILD.bazel @@ -7,7 +7,7 @@ go_library( visibility = ["//visibility:public"], deps = [ "//platform/base/change:go_default_library", - "//platform/base/mergestrategy:go_default_library", + "//platform/base/landstrategy:go_default_library", "//submitqueue/entity:go_default_library", "//submitqueue/extension/storage:go_default_library", "//test/testutil:go_default_library", diff --git a/test/integration/submitqueue/extension/storage/suite.go b/test/integration/submitqueue/extension/storage/suite.go index ff40e3efb..81294a988 100644 --- a/test/integration/submitqueue/extension/storage/suite.go +++ b/test/integration/submitqueue/extension/storage/suite.go @@ -24,7 +24,7 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/uber/submitqueue/platform/base/change" - "github.com/uber/submitqueue/platform/base/mergestrategy" + "github.com/uber/submitqueue/platform/base/landstrategy" "github.com/uber/submitqueue/submitqueue/entity" "github.com/uber/submitqueue/submitqueue/extension/storage" "github.com/uber/submitqueue/test/testutil" @@ -77,7 +77,7 @@ func (s *StorageContractSuite) TestStorage_CreateAndGet() { Change: change.Change{ URIs: []string{"github://github.example.com/uber/storage-test/pull/123/abcdef0123456789abcdef0123456789abcdef01"}, }, - LandStrategy: mergestrategy.MergeStrategyMerge, + LandStrategy: landstrategy.StrategyMerge, Version: 1, } @@ -118,7 +118,7 @@ func (s *StorageContractSuite) TestStorage_CreateAndGet_StackedPRs() { Change: change.Change{ URIs: stackedURIs, }, - LandStrategy: mergestrategy.MergeStrategySquashRebase, + LandStrategy: landstrategy.StrategySquashRebase, Version: 1, } @@ -146,7 +146,7 @@ func (s *StorageContractSuite) TestStorage_Update() { Queue: "test-queue", Change: change.Change{URIs: []string{"github://github.example.com/uber/monorepo/pull/1/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}, State: entity.RequestStateStarted, - LandStrategy: mergestrategy.MergeStrategyMerge, + LandStrategy: landstrategy.StrategyMerge, Version: 1, } @@ -156,7 +156,7 @@ func (s *StorageContractSuite) TestStorage_Update() { updated := request updated.Change.URIs = []string{"github://github.example.com/uber/monorepo/pull/2/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"} - updated.LandStrategy = mergestrategy.MergeStrategySquashRebase + updated.LandStrategy = landstrategy.StrategySquashRebase updated.State = entity.RequestStateProcessing err = s.forQueue("test-queue").GetRequestStore().Update(ctx, updated, request.Version, request.Version+1) require.NoError(t, err, "failed to update request") @@ -177,7 +177,7 @@ func (s *StorageContractSuite) TestStorage_OptimisticLocking() { ID: "test/optimistic-lock", Queue: "test-queue", State: entity.RequestStateStarted, - LandStrategy: mergestrategy.MergeStrategyMerge, + LandStrategy: landstrategy.StrategyMerge, Version: 1, } @@ -188,7 +188,7 @@ func (s *StorageContractSuite) TestStorage_OptimisticLocking() { // Update with correct version. updated := request updated.Change.URIs = []string{"github://github.example.com/uber/monorepo/pull/2/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"} - updated.LandStrategy = mergestrategy.MergeStrategySquashRebase + updated.LandStrategy = landstrategy.StrategySquashRebase updated.State = entity.RequestStateProcessing err = s.forQueue("test-queue").GetRequestStore().Update(ctx, updated, 1, 2) require.NoError(t, err, "update with correct version should succeed") @@ -196,7 +196,7 @@ func (s *StorageContractSuite) TestStorage_OptimisticLocking() { // Try to replace every field with a stale version. stale := request stale.Change.URIs = []string{"github://github.example.com/uber/monorepo/pull/3/cccccccccccccccccccccccccccccccccccccccc"} - stale.LandStrategy = mergestrategy.MergeStrategyRebase + stale.LandStrategy = landstrategy.StrategyRebase stale.State = entity.RequestStateLanded err = s.forQueue("test-queue").GetRequestStore().Update(ctx, stale, 1, 3) assert.Error(t, err, "update with stale version should fail") @@ -229,7 +229,7 @@ func (s *StorageContractSuite) TestStorage_UpdateChangeURIs() { Queue: "test-queue", Change: change.Change{URIs: []string{"github://github.example.com/uber/monorepo/pull/1/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}, State: entity.RequestStateStarted, - LandStrategy: mergestrategy.MergeStrategyMerge, + LandStrategy: landstrategy.StrategyMerge, Version: 1, } require.NoError(t, s.forQueue("test-queue").GetRequestStore().Create(ctx, request)) @@ -319,7 +319,7 @@ func (s *StorageContractSuite) TestStorage_BatchUpdateReplacesAllNonKeyFields() emptyCollections := got emptyCollections.Contains = []string{} emptyCollections.Dependencies = []string{} - emptyCollections.State = entity.BatchStateMerging + emptyCollections.State = entity.BatchStateLanding require.NoError(t, store.Update(ctx, emptyCollections, 2, 3)) got, err = store.Get(ctx, batch.ID) @@ -329,7 +329,7 @@ func (s *StorageContractSuite) TestStorage_BatchUpdateReplacesAllNonKeyFields() assert.Empty(t, got.Contains) assert.NotNil(t, got.Dependencies) assert.Empty(t, got.Dependencies) - assert.Equal(t, entity.BatchStateMerging, got.State) + assert.Equal(t, entity.BatchStateLanding, got.State) assert.Equal(t, int32(3), got.Version) stale := got @@ -375,7 +375,7 @@ func (s *StorageContractSuite) TestStorage_QueueBatchStateRecordLifecycle() { assert.ElementsMatch(t, []entity.QueueBatchState{speculating}, got) // An empty bucket lists empty, not an error. - got, err = storeA.List(ctx, entity.BatchStateMerging) + got, err = storeA.List(ctx, entity.BatchStateLanding) require.NoError(t, err) assert.Empty(t, got) @@ -410,7 +410,7 @@ func (s *StorageContractSuite) TestStorage_QueueIsolation() { storeA := s.forQueue("iso-queue-a") storeB := s.forQueue("iso-queue-b") - request := entity.Request{ID: "iso-a/1", Queue: "iso-queue-a", State: entity.RequestStateStarted, LandStrategy: mergestrategy.MergeStrategyMerge, Version: 1} + request := entity.Request{ID: "iso-a/1", Queue: "iso-queue-a", State: entity.RequestStateStarted, LandStrategy: landstrategy.StrategyMerge, Version: 1} require.NoError(t, storeA.GetRequestStore().Create(ctx, request)) _, err := storeB.GetRequestStore().Get(ctx, request.ID) require.ErrorIs(t, err, storage.ErrNotFound, "a request must be invisible through another queue's binding") @@ -474,7 +474,7 @@ func (s *StorageContractSuite) TestStorage_CreateDuplicate() { ID: "test/duplicate", Queue: "test-queue", State: entity.RequestStateStarted, - LandStrategy: mergestrategy.MergeStrategyMerge, + LandStrategy: landstrategy.StrategyMerge, Version: 1, } diff --git a/test/integration/submitqueue/gateway/BUILD.bazel b/test/integration/submitqueue/gateway/BUILD.bazel index ec8bc96bf..b1f53d970 100644 --- a/test/integration/submitqueue/gateway/BUILD.bazel +++ b/test/integration/submitqueue/gateway/BUILD.bazel @@ -16,7 +16,7 @@ go_test( ], deps = [ "//api/base/change/protopb:go_default_library", - "//api/base/mergestrategy/protopb:go_default_library", + "//api/base/landstrategy/protopb:go_default_library", "//api/submitqueue/gateway/protopb:go_default_library", "//platform/consumer:go_default_library", "//platform/extension/messagequeue/mysql:go_default_library", diff --git a/test/integration/submitqueue/gateway/suite_test.go b/test/integration/submitqueue/gateway/suite_test.go index 6e3da7612..1b7c40668 100644 --- a/test/integration/submitqueue/gateway/suite_test.go +++ b/test/integration/submitqueue/gateway/suite_test.go @@ -36,7 +36,7 @@ import ( "github.com/stretchr/testify/suite" "github.com/uber-go/tally" changepb "github.com/uber/submitqueue/api/base/change/protopb" - mergestrategypb "github.com/uber/submitqueue/api/base/mergestrategy/protopb" + landstrategypb "github.com/uber/submitqueue/api/base/landstrategy/protopb" pb "github.com/uber/submitqueue/api/submitqueue/gateway/protopb" "github.com/uber/submitqueue/platform/consumer" queueMySQL "github.com/uber/submitqueue/platform/extension/messagequeue/mysql" @@ -150,7 +150,7 @@ func (s *GatewayIntegrationSuite) TestLandAPI() { req := &pb.LandRequest{ Queue: "test-queue", Change: &changepb.Change{Uris: []string{"github://github.example.com/uber/integration-test/pull/123/abcdef0123456789abcdef0123456789abcdef01"}}, - Strategy: mergestrategypb.Strategy_REBASE, + Strategy: landstrategypb.Strategy_REBASE, } s.log.Logf("Sending Land request for queue=%s", req.Queue) diff --git a/tool/gitsandbox/BUILD.bazel b/tool/gitsandbox/BUILD.bazel index 511ede4ca..7266858c8 100644 --- a/tool/gitsandbox/BUILD.bazel +++ b/tool/gitsandbox/BUILD.bazel @@ -10,8 +10,8 @@ go_library( go_binary( name = "gitsandbox", - # The pinned git the merger uses, rather than whatever the host has, so a - # sandbox is seeded by the same build that later merges into it. `args` is + # The pinned git the lander uses, rather than whatever the host has, so a + # sandbox is seeded by the same build that later lands into it. `args` is # honored by `bazel run`, and flags the Makefile appends after `--` still # win, since Go's flag package takes the last occurrence. args = ["-git=$(location @git//:git)"], diff --git a/tool/gitsandbox/main.go b/tool/gitsandbox/main.go index e758b15a8..506656868 100644 --- a/tool/gitsandbox/main.go +++ b/tool/gitsandbox/main.go @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Command gitsandbox provisions the bare repository the git provider merges +// Command gitsandbox provisions the bare repository the git provider lands // into (`make local-submitqueue-start PROVIDER=git`). // -// A merge target has to exist before Runway starts: the merger clones it at +// A land target has to exist before Runway starts: the lander clones it at // boot and fails outright if it cannot. So this runs ahead of the stack, // creating a bare repository with one commit on the target branch — the -// smallest thing a merge can be performed against. +// smallest thing a land can be performed against. // // It is idempotent. An already-initialized repository is left exactly as it is, // so restarting the stack keeps whatever previous runs landed rather than @@ -36,7 +36,7 @@ import ( ) // seedFile is committed so the target branch exists with something on it. A -// branch with no commits cannot be cloned or merged into. +// branch with no commits cannot be cloned or landed into. const seedFile = "README.md" const seedContents = `# SubmitQueue sandbox @@ -107,7 +107,7 @@ func provision(ctx context.Context, git, bare, branch string) (bool, error) { } if err := create(ctx, git, bare, branch); err != nil { - // A repository that is initialized but has no commit cannot be merged + // A repository that is initialized but has no commit cannot be landed // into, and the next run would skip it as already provisioned — so the // stack would fail at boot instead of being repaired. Leave nothing // behind rather than something unusable. @@ -148,7 +148,7 @@ func configure(ctx context.Context, git, bare string) error { // repository is written from the host and read from inside a container // through a bind mount, and that boundary does not preserve the // guarantee. A run pushing fifty branches gives fifty chances for the - // merger to read a packed-refs truncated mid-name and fail a land that + // lander to read a packed-refs truncated mid-name and fail a land that // had nothing wrong with it. Left unpacked, there is nothing to rewrite; // loose refs cost a sandbox that gets deleted nothing. {"gc.auto", "0"}, diff --git a/tool/gitsandbox/main_test.go b/tool/gitsandbox/main_test.go index ca1245bbe..f54392eea 100644 --- a/tool/gitsandbox/main_test.go +++ b/tool/gitsandbox/main_test.go @@ -182,7 +182,7 @@ func TestRun_CreatesTheCheckoutDirectory(t *testing.T) { assert.True(t, info.IsDir()) } -// A repository that exists but carries no commit cannot be merged into, and the +// A repository that exists but carries no commit cannot be used as a land target, and the // next run would skip it as already provisioned rather than repair it. func TestProvision_LeavesNothingBehindWhenCreationFails(t *testing.T) { ctx := context.Background() diff --git a/tool/linter/messageid/main_test.go b/tool/linter/messageid/main_test.go index 9fcc6282e..35a315710 100644 --- a/tool/linter/messageid/main_test.go +++ b/tool/linter/messageid/main_test.go @@ -50,7 +50,7 @@ func f() { _ = messagequeue.NewMessage("id", nil, "part", nil) }`, name: "publishing through the helper passes", src: `package p import "github.com/uber/submitqueue/platform/publish" -func f() { _ = publish.IntentID("batch-1", "merged") }`, +func f() { _ = publish.IntentID("batch-1", "landed") }`, }, { name: "NewMessage of an unrelated package passes", @@ -105,7 +105,7 @@ func TestAllowed(t *testing.T) { {"platform/publish/publish.go", true}, {"platform/extension/messagequeue/mysql/subscriber.go", true}, {"submitqueue/orchestrator/controller/batch/batch.go", false}, - {"runway/controller/merge/merge.go", false}, + {"runway/controller/land/land.go", false}, // A path merely prefixed by an allowed root's name is not inside it. {"platform/publishing/thing.go", false}, } diff --git a/tool/proto/BUILD.bazel b/tool/proto/BUILD.bazel index 16bc485cf..874134c22 100644 --- a/tool/proto/BUILD.bazel +++ b/tool/proto/BUILD.bazel @@ -10,10 +10,10 @@ go_proto_generated_files( ) go_proto_generated_files( - name = "api_base_mergestrategy", - srcs = ["//api/base/mergestrategy/proto:mergestrategy.proto"], + name = "api_base_landstrategy", + srcs = ["//api/base/landstrategy/proto:landstrategy.proto"], gen_services = False, - out_dir = "api_base_mergestrategy", + out_dir = "api_base_landstrategy", ) go_proto_generated_files( @@ -35,11 +35,11 @@ go_proto_generated_files( go_proto_generated_files( name = "api_runway_messagequeue", - srcs = ["//api/runway/messagequeue/proto:merge.proto"], + srcs = ["//api/runway/messagequeue/proto:land.proto"], gen_services = False, imports = [ "//api/base/change/proto:change.proto", - "//api/base/mergestrategy/proto:mergestrategy.proto", + "//api/base/landstrategy/proto:landstrategy.proto", "//api/base/messagequeue/proto:messagequeue.proto", ], out_dir = "api_runway_messagequeue", @@ -56,7 +56,7 @@ go_proto_generated_files( srcs = ["//api/submitqueue/gateway/proto:gateway.proto"], imports = [ "//api/base/change/proto:change.proto", - "//api/base/mergestrategy/proto:mergestrategy.proto", + "//api/base/landstrategy/proto:landstrategy.proto", ], out_dir = "api_submitqueue_gateway", ) @@ -94,7 +94,7 @@ filegroup( srcs = [ ":api_base_change", ":api_base_hook", - ":api_base_mergestrategy", + ":api_base_landstrategy", ":api_base_messagequeue", ":api_runway", ":api_runway_messagequeue",