From d4c28d4685c7b9b79ce2692f7f9bb07d057b3d07 Mon Sep 17 00:00:00 2001 From: Preetam Dwivedi Date: Thu, 9 Jul 2026 14:54:07 -0700 Subject: [PATCH] docs(rfc): add change URI design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary ### Why? Change URIs are the system-wide identity of a code change — stored as a primary-key column, correlated by strict string equality, and shared across SubmitQueue, Stovepipe, and Runway via the `api/base/change` contract. Today only `git://` URIs name the provider instance; GitHub and Phabricator URIs carry no host, so the same URI can name two different changes on two provider instances, and the host lives out-of-band in queue config — coupling change identity to queue topology. ### What? Adds `doc/rfc/change-uri.md`, a spec for change URIs: every URI is `scheme://{host[:port]}/{path}` where the scheme names the provider model, the mandatory authority names the instance, and the path pins the change to an exact code state. Defines the per-provider formats (GitHub PR, Phabricator Diff, git ref/commit), canonical-form rules (validated, never normalized: lowercase host, verbatim optional port, full 40-hex SHAs, byte-for-byte round-trip), and rejected alternatives (out-of-band host, per-flavor `ghe`/`ghes` schemes, web URLs as identity). Also links the RFC from `doc/rfc/index.md` under Shared, and drops a pre-existing duplicated "Build Runner" entry from the index. --- doc/rfc/change-uri.md | 43 +++++++++++++++++++++++++++++++++++++++++++ doc/rfc/index.md | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 doc/rfc/change-uri.md diff --git a/doc/rfc/change-uri.md b/doc/rfc/change-uri.md new file mode 100644 index 00000000..05fd0c95 --- /dev/null +++ b/doc/rfc/change-uri.md @@ -0,0 +1,43 @@ +# Change URIs + +A change URI is the system-wide identity of a code change — a Pull Request, a Phabricator Diff, or a git ref/commit. It is minted by the client at submission, validated at the gateway, and flows as an opaque string through the shared `Change` wire contract (`api/base/change`), cross-domain queue payloads, and storage, where it is a primary-key column and correlation key. A change URI must therefore be globally unambiguous on its own: interpretable without knowing which queue carried it or how any backend is wired. + +## Shape + +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. +- **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. + +## Formats + +| Provider | Format | Example | +|---|---|---| +| GitHub PR | `github://{host[:port]}/{org}/{repo}/pull/{pr}/{head_sha}` | `github://github.uberinternal.com/uber/submitqueue/pull/123/c3a4…89ab` | +| Phabricator Diff | `phab://{host[:port]}/D{revision}/{diff}` | `phab://phabricator.example.com/D12345/67890` | +| git ref/commit | `git://{host[:port]}/{repo}/{ref}/{sha}` | `git://git.example.com:9418/uber/mono/refs%2Fheads%2Fmain/c3a4…89ab` | + +Path rules per provider: + +- **GitHub** — `{org}` may be a nested path (`uber/frontend`); the literal `pull` segment separates it from the PR number, mirroring the real PR URL layout so URIs are built by substitution, not reshaping. `{head_sha}` is the PR's head commit at submission time. +- **Phabricator** — `D{revision}` is the logical review (stable across updates); `{diff}` is the uploaded patch version that pins the exact code state, analogous to GitHub's head SHA. Both are positive integers without leading zeros. +- **git** — `{repo}` is the repository path on the remote and may contain slashes; `{ref}` is a fully-qualified git ref (`refs/heads/main`, `refs/tags/v1.0`), percent-encoded so it occupies a single path segment; `{sha}` is a commit that ref has pointed to. + +## Canonical form + +URIs are compared as opaque strings everywhere (primary keys, claim lookups, staleness checks), so exactly one spelling per change is valid. Parsers **validate the canonical form and reject everything else — they never normalize**, because normalization applied at one entry point and skipped at another lets two spellings of one change into the system. + +- **Host** — required, non-empty, lowercase (DNS is case-insensitive, so case variants would alias one instance into many identities). Uppercase is rejected, not folded. +- **Port** — optional, digits only, verbatim when present. Custom schemes have no registered default port, so there is nothing to strip; omit it unless the backend listens on a non-standard one. +- **Commit SHAs** — the full 40-character lowercase hex form. Abbreviated or uppercase SHAs are rejected, not expanded or folded. +- **All other path segments** — verbatim. Org, repo, and ref segments live in namespaces that are case-sensitive (git refs, repository paths on a git remote) or provider-canonical (GitHub resolves org/repo case-insensitively, but each repo has one canonical casing and uppercase is legal — the parser cannot know which). Folding their case would silently point the identity at a different resource; canonical casing here is the provider's to enforce, at the point where the provider is consulted. +- **Round-trip** — parsing a valid URI and re-serializing the parsed form yields the input byte-for-byte. + +Parsing is delegated to `net/url`, which handles `host:port` splitting, bracketed IPv6 hosts, and percent-encoding correctly. + +## Rejected alternatives + +- **Host out-of-band in queue config.** Conflates identity with routing: the meaning of a stored primary-key value must not depend on deployment wiring, and the shared contract must be interpretable by every domain that imports it. +- **Per-flavor schemes** (`ghe://`, `ghes://`). Redundant with the authority, and an open-ended enum baked into parsers and routing — a new instance should be configuration, not code. The flavors share one PR model and one API surface; what does differ per instance (API base path, version skew) is wiring config on the client for that host, never identifier grammar. +- **The provider's web URL as identity** (`https://github.com/uber/repo/pull/123`). Human-facing URLs don't uniformly pin the code state, vary with provider UI cosmetics, and hand our identity grammar to a third party. Custom schemes keep the grammar strict and ours. diff --git a/doc/rfc/index.md b/doc/rfc/index.md index 5baf9016..d67f28c8 100644 --- a/doc/rfc/index.md +++ b/doc/rfc/index.md @@ -6,12 +6,12 @@ Design documents and technical proposals, grouped by scope. Shared/cross-cutting - [SQL-Based Distributed Queue](sql-queue-rfc.md) - MySQL-based distributed message queue with partition leasing and at-least-once delivery (used by SubmitQueue, Stovepipe, and other repo-local services) - [Message Queue Contract](messagequeue-contract.md) - How queue payloads are defined (Protobuf, serialized as protobuf JSON), located by audience (external in `api/{domain}/messagequeue/`, internal in `{domain}/core/messagequeue/`), bound to topics (the `topics` proto option), and enforced by Bazel visibility +- [Change URIs](change-uri.md) - Identity of a code change: `scheme://{host[:port]}/{path}` per provider (GitHub PR, Phabricator Diff, git ref/commit) and canonical-form rules ## SubmitQueue - [Orchestrator Workflow](submitqueue/workflow.md) - Queue-driven controller pipeline from gateway entry through batching, scoring, build, merge, and conclude - [Build Runner](submitqueue/build-runner.md) - Vendor-agnostic BuildRunner interface, provider-neutral BuildStatus lifecycle, and how the orchestrator wires it into the build stage -- [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 - [Speculation](submitqueue/speculation.md) - Why SubmitQueue speculates, the path/tree model, and the two pluggable seams: speculation-tree enumeration and path selection