Skip to content

feat!: adopt canonical change URIs for change requests - #264

Merged
xytan0056 merged 5 commits into
mainfrom
change-uri-rfc
Jul 30, 2026
Merged

feat!: adopt canonical change URIs for change requests#264
xytan0056 merged 5 commits into
mainfrom
change-uri-rfc

Conversation

@xytan0056

@xytan0056 xytan0056 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Adopt the change-URI RFC format for change requests. The URI is the self-contained identity of a change: the host names the provider instance, the embedded head SHA pins the code state, and the URI alone determines the cache key. The separate proto commit field is removed.

The format is enforced by the bundled native orchestrator, not the gateway — the mapper passes URLs through opaquely, so custom Orchestrator implementations can accept formats of their own.

Accepted URI format

Provider Format Example
GitHub PR github://{host[:port]}/{org}/{repo}/pull/{pr}/{head_sha} github://github.com/uber/tango/pull/123/c3a4b5d6e7f80912a3b4c5d6e7f80912a3b4c5d6

phab:// and git:// are not supported yet. Violations are rejected, never normalized:

Component Rule
host required; lowercase only
port optional; digits only
org, repo verbatim, case-sensitive; org may span segments (uber/frontend)
pr positive integer, no leading zeros
head_sha full 40-char lowercase hex

No userinfo, query, fragment, or empty segments; parse → serialize round-trips byte-for-byte.

Changes

  • New core/changeuri package: canonical parser/validator
  • Proto: Request.commit removed; tangopb/ regenerated
  • Cache key hashes only the URI; native orchestrator rejects invalid URIs as ErrorUser
  • workspace.NewRequest parses the URI and threads PR number + head SHA to gitRequest

Breaking change

The native orchestrator rejects old-format URLs (github://org/repo/pull/N + commit field); callers must migrate. Old treehash-cache entries recompute; treehash-keyed graph entries are unaffected.

Test plan

CI
integration test

@xytan0056
xytan0056 requested review from a team as code owners July 30, 2026 16:39
@xytan0056
xytan0056 force-pushed the change-uri-rfc branch 3 times, most recently from e4599bd to c6deb47 Compare July 30, 2026 16:54
Change requests were identified by a loosely-formatted URL plus a
separate commit field. Adopt the change-URI RFC format instead:

    github://{host[:port]}/{org}/{repo}/pull/{pr}/{head_sha}

The URI is the self-contained identity of a change request: the host
names the provider instance and the embedded head SHA pins the exact
code state, so the URI alone determines the cache key.

The URL format is orchestrator-specific: the gateway (mapper) passes it
through opaquely, and the bundled native orchestrator validates and
rejects non-canonical URIs as user errors. Custom Orchestrator
implementations may accept formats of their own.

- Add core/changeuri: canonical parser/validator (github:// scheme).
  Rejects non-canonical spellings (uppercase host, abbreviated or
  uppercase SHA, leading-zero PR number, query/fragment/userinfo) and
  enforces byte-for-byte round-tripping; never normalizes.
- Remove the proto Request.commit field (reserved); regenerate tangopb.
- Hash only the URI in GetReqURLsHash; the pinned SHA is embedded in it.
- workspace.NewRequest parses the URI and threads the PR number and head
  SHA to gitRequest; the ancestor sanity check is now unconditional.
- Update example client help text and README with the URI format.

BREAKING CHANGE: for the bundled native orchestrator, request URLs in
the old format (github://org/repo/pull/N with a separate commit field)
are rejected; callers must send canonical change URIs.
Comment on lines +71 to +89
u, err := url.Parse(raw)
if err != nil {
return PullRequest{}, fmt.Errorf("parse change URI: %w", err)
}
if u.Scheme != Scheme {
return PullRequest{}, fmt.Errorf("unsupported scheme %q: only %q is supported", u.Scheme, Scheme)
}
if u.Opaque != "" {
return PullRequest{}, fmt.Errorf("change URI must have an authority: %q", raw)
}
if u.User != nil {
return PullRequest{}, fmt.Errorf("change URI must not contain userinfo: %q", raw)
}
if u.RawQuery != "" || u.ForceQuery || u.Fragment != "" {
return PullRequest{}, fmt.Errorf("change URI must not contain a query or fragment: %q", raw)
}
if err := validateHost(u); err != nil {
return PullRequest{}, err
}

@yushan8 yushan8 Jul 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should these lines be in a helper function validateURL. I think it's a little bit more readable that way so this function focuses on parsing the pull request string into the struct.

@xytan0056 xytan0056 Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

xytan0056 and others added 2 commits July 30, 2026 15:49
Comment thread core/changeuri/changeuri.go Outdated
@@ -0,0 +1,181 @@
// Copyright (c) 2025 Uber Technologies, Inc.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Copyright (c) 2025 Uber Technologies, Inc.
// Copyright (c) 2026 Uber Technologies, Inc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in f4264d9.

Comment thread proto/tango.proto Outdated
string commit = 2;
// commit was the SHA to apply; it is now embedded in the URI.
reserved 2;
reserved "commit";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we just remove it?

@xytan0056 xytan0056 Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

once remove and deploy the package, existing calls can fail

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should just remove it for now to keep the proto clean. This isn't used by anyone but us, I think we can update it until it's actually finalized and productionized. Once it's actually in production, I agree we should use that to deprecated the fields.

@xytan0056
xytan0056 merged commit 075e784 into main Jul 30, 2026
10 checks passed
xytan0056 added a commit that referenced this pull request Jul 30, 2026
Stacked on #264.

## Summary

`Apply` diffed against the floating `pull/{id}/head` ref, so two
requests carrying the same change URI (pull/123) could materialize
different trees under the same cache key if the PR has different HEADs.
This diffs against the pinned head SHA from the URI instead, making the
materialized tree deterministic for a given change URI. The ancestor
check still rejects stale or foreign SHAs.

** This is a breaking change

## Tests

- Mock test asserting the diff target is the pinned SHA, not the PR head
- Real-git tests (local bare repo with `refs/pull/1/head`, no env
gating): pinned content applies end-to-end; the tree is byte-identical
across PR advancement; a SHA that exists but is not an ancestor of the
PR head is rejected

## Test plan
CI
integration test
Comment thread proto/tango.proto Outdated
// The commit SHA associated with the change request
string commit = 2;
// commit was the SHA to apply; it is now embedded in the URI.
reserved 2;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as long as we can afford to make breaking changes anyways, we may refrain from reserving a field numeric

Comment thread proto/tango.proto
// e.g. github://github.com/uber/tango/pull/123/c3a4b5d6e7f80912a3b4c5d6e7f80912a3b4c5d6
// whose embedded head SHA pins the exact code state; custom
// orchestrators may accept formats of their own. The URL participates
// in cache keys as an opaque string.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

important to mention that URI must be immutable to the content it represents, for any possible implementation of the orchestrator

// (https://github.com/uber/submitqueue/blob/main/doc/rfc/change-uri.md).
//
// A change URI is an RFC 3986 URI of the form scheme://{host[:port]}/{path}
// whose path pins the change to an exact code state. Only the GitHub pull

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to make future implementations more structured, should we place github implementation under changeuri/github folder? Or even adopt the package from submitqueue?

// Scheme is the URI scheme for GitHub pull requests.
const Scheme = "github"

// PullRequest is the parsed form of a canonical GitHub pull request URI.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the "canonical" Github only has PR number, not a hash I think?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants