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) }