From adcf5e997829174e8c0072ecdb7fcb354d522d45 Mon Sep 17 00:00:00 2001 From: sergeyb Date: Thu, 17 Sep 2026 17:58:24 +0000 Subject: [PATCH] docs(runway): state the merge target atomicity guarantee in the contract A committing merge has always been all-or-nothing against its merge target by construction of the Git merger, but the property was only written down in the Git merger's own README. This documents it as part of the Merger interface contract, the published wire contract README, and the Runway workflow RFC, so callers and future backends can rely on it without reading the Git implementation. Co-Authored-By: Claude Fable 5.1 --- api/runway/messagequeue/README.md | 2 ++ doc/rfc/runway/workflow.md | 2 ++ runway/extension/merger/merger.go | 3 +++ 3 files changed, 7 insertions(+) diff --git a/api/runway/messagequeue/README.md b/api/runway/messagequeue/README.md index d9aaf161b..bc66739cd 100644 --- a/api/runway/messagequeue/README.md +++ b/api/runway/messagequeue/README.md @@ -25,6 +25,8 @@ One message serves a queue pair because a merge-conflict check is a dry run of a `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, …). +A committing merge is all-or-nothing against the merge target: Runway updates the target at most once per request, and a `SUCCEEDED` result means every step in `steps` is reachable from the target, while a `FAILED` result means the target is unchanged. + ## Evolution Contract changes are additive-only: add new fields; never remove, rename, repurpose, or retype an existing field, and never reuse a field number. protojson ignores unknown fields on read and omits zero-valued fields on write, so a new optional field is backward-compatible in both directions. diff --git a/doc/rfc/runway/workflow.md b/doc/rfc/runway/workflow.md index b6d1cf464..23e3fb084 100644 --- a/doc/rfc/runway/workflow.md +++ b/doc/rfc/runway/workflow.md @@ -79,6 +79,8 @@ Together these guarantee the client's correlation id always resolves: the primar 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. +A committing merge is also atomic against the merge target: it updates the target at most once per request, and afterwards either every step of the request is reachable from the target or the target is unchanged. A retried redelivery therefore either replays cleanly against the same unchanged target or finds its work already landed. + ## Ownership by service ### Runway diff --git a/runway/extension/merger/merger.go b/runway/extension/merger/merger.go index 8371e0f74..bf0f7b1ec 100644 --- a/runway/extension/merger/merger.go +++ b/runway/extension/merger/merger.go @@ -55,6 +55,9 @@ type Merger interface { CheckMergeability(ctx context.Context, req *runwaymq.MergeRequest) (*runwaymq.MergeResult, error) // Merge applies the ordered steps, commits the result to the remote, and // reports per-step Outputs (the VCS-neutral revision identifiers produced). + // Merge is all-or-nothing against the target: it updates the target at + // most once per request, and afterwards either every step is reachable + // from the target or the target is unchanged. Merge(ctx context.Context, req *runwaymq.MergeRequest) (*runwaymq.MergeResult, error) }