From 444c9c13349056aac1279ed7817458c2a120b98d Mon Sep 17 00:00:00 2001 From: manjari Date: Mon, 2 Nov 2026 20:10:53 +0000 Subject: [PATCH] feat(baserevision): Resolve the base revision from the GitHub API --- .../orchestrator/server/BUILD.bazel | 1 + .../submitqueue/orchestrator/server/config.go | 10 ++ .../orchestrator/server/config_test.go | 46 ++++++- .../orchestrator/server/profiles.go | 85 ++++++++---- submitqueue/extension/baserevision/README.md | 18 +++ .../extension/baserevision/baserevision.go | 14 +- submitqueue/extension/baserevision/git/git.go | 7 +- .../extension/baserevision/github/BUILD.bazel | 23 ++++ .../extension/baserevision/github/github.go | 100 +++++++++++++++ .../baserevision/github/github_test.go | 121 ++++++++++++++++++ .../extension/conflict/tango/README.md | 2 +- 11 files changed, 395 insertions(+), 32 deletions(-) create mode 100644 submitqueue/extension/baserevision/README.md create mode 100644 submitqueue/extension/baserevision/github/BUILD.bazel create mode 100644 submitqueue/extension/baserevision/github/github.go create mode 100644 submitqueue/extension/baserevision/github/github_test.go diff --git a/service/submitqueue/orchestrator/server/BUILD.bazel b/service/submitqueue/orchestrator/server/BUILD.bazel index 051ea967e..ae4a1fb08 100644 --- a/service/submitqueue/orchestrator/server/BUILD.bazel +++ b/service/submitqueue/orchestrator/server/BUILD.bazel @@ -45,6 +45,7 @@ go_library( "//submitqueue/entity:go_default_library", "//submitqueue/extension/baserevision:go_default_library", "//submitqueue/extension/baserevision/git:go_default_library", + "//submitqueue/extension/baserevision/github:go_default_library", "//submitqueue/extension/buildrunner:go_default_library", "//submitqueue/extension/buildrunner/buildkite:go_default_library", "//submitqueue/extension/buildrunner/fake:go_default_library", diff --git a/service/submitqueue/orchestrator/server/config.go b/service/submitqueue/orchestrator/server/config.go index 268423c49..bf512ee69 100644 --- a/service/submitqueue/orchestrator/server/config.go +++ b/service/submitqueue/orchestrator/server/config.go @@ -273,6 +273,16 @@ type tangoAnalyzerConfig struct { // Strategy is how Tango computes the base target graph: "unset", "shell" // or "native". Required. Strategy string `yaml:"strategy"` + // GitHub names the branch whose tip is the base revision. Required with + // the github change provider, which keeps no clone to read it from. + GitHub *tangoGitHubConfig `yaml:"github"` +} + +// tangoGitHubConfig locates a queue's target branch on GitHub. +type tangoGitHubConfig struct { + Owner string `yaml:"owner"` + Repo string `yaml:"repo"` + Branch string `yaml:"branch"` } // scorerConfig selects how a queue ranks candidate speculation paths. The diff --git a/service/submitqueue/orchestrator/server/config_test.go b/service/submitqueue/orchestrator/server/config_test.go index 7c9a07bb2..b18d25b3d 100644 --- a/service/submitqueue/orchestrator/server/config_test.go +++ b/service/submitqueue/orchestrator/server/config_test.go @@ -930,7 +930,7 @@ func TestLoadProfilesConfig_QueueScorerBaseReplacesDefaultBase(t *testing.T) { func TestNewTangoAnalyzerFactory(t *testing.T) { key := tangoAnalyzerKey{ Analyzer: analyzerConfig{Type: analyzerTypeTango, Tango: &tangoAnalyzerConfig{Remote: "origin", Strategy: "unset"}}, - Git: gitProviderConfig{RepoPath: "/var/repos/x", Remote: "origin", Target: "main"}, + Git: &gitProviderConfig{RepoPath: "/var/repos/x", Remote: "origin", Target: "main"}, } connection, err := tangoclient.Dial("127.0.0.1:0") require.NoError(t, err) @@ -968,7 +968,49 @@ func TestNewTangoAnalyzerFactory(t *testing.T) { } } -func TestNewProfiles_TangoRequiresTheGitChangeProvider(t *testing.T) { +func TestNewTangoAnalyzerFactory_GitHub(t *testing.T) { + connection, err := tangoclient.Dial("127.0.0.1:0") + require.NoError(t, err) + t.Cleanup(func() { _ = connection.Close() }) + db, _, err := sqlmock.New() + require.NoError(t, err) + t.Cleanup(func() { _ = db.Close() }) + + tests := []struct { + name string + branch *tangoGitHubConfig + token string + wantErr bool + }{ + {name: "with a branch and a token", branch: &tangoGitHubConfig{Owner: "org", Repo: "repo", Branch: "main"}, token: "secret"}, + {name: "without the branch block", token: "secret", wantErr: true}, + {name: "with an incomplete branch block", branch: &tangoGitHubConfig{Owner: "org", Repo: "repo"}, token: "secret", wantErr: true}, + {name: "without a token", branch: &tangoGitHubConfig{Owner: "org", Repo: "repo", Branch: "main"}, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("TEST_TANGO_GITHUB_TOKEN", tt.token) + key := tangoAnalyzerKey{ + Analyzer: analyzerConfig{Type: analyzerTypeTango, Tango: &tangoAnalyzerConfig{Remote: "origin", Strategy: "unset", GitHub: tt.branch}}, + GitHub: &githubProviderConfig{TokenEnv: "TEST_TANGO_GITHUB_TOKEN", BaseURL: defaultGitHubBaseURL}, + } + b := &profileBuilder{scope: tally.NoopScope, tango: tangoDeps{DB: db, Connection: connection, Cache: tango.NewSignatureCache(1 << 20)}} + + factory, err := b.newTangoAnalyzerFactory(key, "defaults") + + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + analyzer, err := factory.For(conflict.Config{QueueName: "q"}) + require.NoError(t, err) + assert.NotNil(t, analyzer) + }) + } +} + +func TestNewProfiles_TangoRequiresTheGitOrGitHubChangeProvider(t *testing.T) { cfg, err := loadProfilesConfig(writeProfiles(t, ` tango: {address: "127.0.0.1:0", signatureCacheBytes: 1048576} defaults: diff --git a/service/submitqueue/orchestrator/server/profiles.go b/service/submitqueue/orchestrator/server/profiles.go index d1414b125..ec635a6eb 100644 --- a/service/submitqueue/orchestrator/server/profiles.go +++ b/service/submitqueue/orchestrator/server/profiles.go @@ -39,6 +39,7 @@ import ( "github.com/uber/submitqueue/submitqueue/entity" "github.com/uber/submitqueue/submitqueue/extension/baserevision" baserevisiongit "github.com/uber/submitqueue/submitqueue/extension/baserevision/git" + baserevisiongithub "github.com/uber/submitqueue/submitqueue/extension/baserevision/github" "github.com/uber/submitqueue/submitqueue/extension/buildrunner" buildkiterunner "github.com/uber/submitqueue/submitqueue/extension/buildrunner/buildkite" buildfake "github.com/uber/submitqueue/submitqueue/extension/buildrunner/fake" @@ -310,10 +311,14 @@ func (b *profileBuilder) build(cfg queueProfileConfig, where string) (Profile, e } var analyzer conflict.Factory if cfg.Analyzer.Type == analyzerTypeTango { - if cfg.ChangeProvider.Git == nil { - return Profile{}, fmt.Errorf("%s: the tango analyzer needs the git change provider, got %q", where, cfg.ChangeProvider.Type) + key := tangoAnalyzerKey{Analyzer: cfg.Analyzer, Git: cfg.ChangeProvider.Git} + if cfg.ChangeProvider.Type == changeProviderTypeGitHub { + key.GitHub = cfg.ChangeProvider.GitHub } - analyzer, err = reuse(b, tangoAnalyzerKey{Analyzer: cfg.Analyzer, Git: *cfg.ChangeProvider.Git}, where, b.newTangoAnalyzerFactory) + if key.Git == nil && key.GitHub == nil { + return Profile{}, fmt.Errorf("%s: the tango analyzer needs the git or github change provider, got %q", where, cfg.ChangeProvider.Type) + } + analyzer, err = reuse(b, key, where, b.newTangoAnalyzerFactory) } else { analyzer, err = reuse(b, cfg.Analyzer, where, b.newAnalyzerFactory) } @@ -739,16 +744,18 @@ func (b *profileBuilder) newAnalyzerFactory(cfg analyzerConfig, where string) (c }), nil } -// tangoAnalyzerKey keys the tango analyzer on both the analyzer block and the -// git block of its queue, because the base revision comes from that clone. +// tangoAnalyzerKey keys the tango analyzer on the analyzer block and on the +// change provider block its base revision comes from. Exactly one of Git and +// GitHub is set. type tangoAnalyzerKey struct { Analyzer analyzerConfig - Git gitProviderConfig + Git *gitProviderConfig + GitHub *githubProviderConfig } // newTangoAnalyzerFactory builds a factory for tango analyzers that read the -// base revision from the queue's own clone. Validation guarantees the -// process-level pieces are set. +// base revision from the queue's own clone or from the GitHub API. Validation +// guarantees the process-level pieces are set. func (b *profileBuilder) newTangoAnalyzerFactory(key tangoAnalyzerKey, where string) (conflict.Factory, error) { if b.tango.Connection == nil || b.tango.Cache == nil || b.tango.DB == nil { return nil, fmt.Errorf("%s: the tango analyzer needs a tango connection, signature cache and database", where) @@ -757,28 +764,62 @@ func (b *profileBuilder) newTangoAnalyzerFactory(key tangoAnalyzerKey, where str if err != nil { return nil, fmt.Errorf("%s: %w", where, err) } - - repo, ok := b.repos[key.Git.RepoPath] - if !ok { - return nil, fmt.Errorf("%s: no change repository provisioned at %q", where, key.Git.RepoPath) + baseRevisions, err := b.newBaseRevisions(key, where) + if err != nil { + return nil, err } - workspaces := cloneWorkspaces{} - target := baserevisiongit.Target{Repo: key.Git.RepoPath, Remote: key.Git.Remote, Branch: key.Git.Target} return analyzerFunc(func(c conflict.Config) (conflict.Analyzer, error) { return tango.New(tango.Params{ - Signatures: tangomysql.NewSignatureStore(b.tango.DB, b.scope.SubScope("tango"), c.QueueName), - Cache: b.tango.Cache, - Client: client, - BaseRevisions: lockedBaseRevisions{ - Resolver: baserevisiongit.New(workspaces, map[string]baserevisiongit.Target{c.QueueName: target}), - lock: repo, - }, - Changes: b.resolver, + Signatures: tangomysql.NewSignatureStore(b.tango.DB, b.scope.SubScope("tango"), c.QueueName), + Cache: b.tango.Cache, + Client: client, + BaseRevisions: baseRevisions(c.QueueName), + Changes: b.resolver, }), nil }), nil } +// newBaseRevisions returns a function building the base revision resolver of a +// queue, reading from the queue's clone when the git change provider is used and +// from the GitHub API otherwise. The clone and the HTTP client are set up once +// here. +func (b *profileBuilder) newBaseRevisions(key tangoAnalyzerKey, where string) (func(queue string) baserevision.Resolver, error) { + if key.Git != nil { + repo, ok := b.repos[key.Git.RepoPath] + if !ok { + return nil, fmt.Errorf("%s: no change repository provisioned at %q", where, key.Git.RepoPath) + } + target := baserevisiongit.Target{Repo: key.Git.RepoPath, Remote: key.Git.Remote, Branch: key.Git.Target} + return func(queue string) baserevision.Resolver { + return lockedBaseRevisions{ + Resolver: baserevisiongit.New(cloneWorkspaces{}, map[string]baserevisiongit.Target{queue: target}), + lock: repo, + } + }, nil + } + + branch := key.Analyzer.Tango.GitHub + if branch == nil || branch.Owner == "" || branch.Repo == "" || branch.Branch == "" { + return nil, fmt.Errorf("%s: the tango analyzer on the github change provider needs tango.github with an owner, repo and branch", where) + } + token, ok := tokenFrom(key.GitHub.TokenEnv) + if !ok { + return nil, fmt.Errorf("%s: the tango analyzer on the github change provider needs %s to be set", where, key.GitHub.TokenEnv) + } + client, err := b.githubClient(key.GitHub.BaseURL, key.GitHub.Timeout, token, where) + if err != nil { + return nil, err + } + target := baserevisiongithub.Target{Owner: branch.Owner, Repo: branch.Repo, Branch: branch.Branch} + return func(queue string) baserevision.Resolver { + return baserevisiongithub.New(baserevisiongithub.Params{ + HTTPClient: client, + Targets: map[string]baserevisiongithub.Target{queue: target}, + }) + }, nil +} + // lockedBaseRevisions holds the clone's lock around each lookup, because the // change provider works in the same clone. type lockedBaseRevisions struct { diff --git a/submitqueue/extension/baserevision/README.md b/submitqueue/extension/baserevision/README.md new file mode 100644 index 000000000..23f581010 --- /dev/null +++ b/submitqueue/extension/baserevision/README.md @@ -0,0 +1,18 @@ +# Base Revision Extension + +Vendor-agnostic source of the commit a queue's new batches are analyzed against. + +## Interfaces + +### Resolver + +Returns the full commit SHA of the tip of a queue's target branch, as of the call. A queue without a target is an error, and so is any failure to read the tip. A caller must not fall back to an earlier value, because the revision names the code a batch is measured against. Implementations must be safe for concurrent use. + +## Implementations + +- `git` fetches the target branch into a clone through any `gitworkspace.Factory`, so where git runs is the deployment's choice. Calls that share a repository are serialized, because concurrent fetches into one clone can fail on a ref lock. +- `github` reads the branch from the GitHub REST API, so it needs no clone. It takes an HTTP client that already carries the API root and credentials, as the GitHub change provider does. + +A deployer can supply its own implementation, for example one backed by an internal source control service. + +The Tango conflict analyzer is the only consumer today. The orchestrator wiring picks the implementation from the queue's change provider. diff --git a/submitqueue/extension/baserevision/baserevision.go b/submitqueue/extension/baserevision/baserevision.go index a739c6d78..74341caf4 100644 --- a/submitqueue/extension/baserevision/baserevision.go +++ b/submitqueue/extension/baserevision/baserevision.go @@ -16,7 +16,19 @@ package baserevision //go:generate mockgen -source=baserevision.go -destination=mock/baserevision_mock.go -package=mock -import "context" +import ( + "context" + "regexp" +) + +// fullCommitSHA matches the SHA-1 and SHA-256 object formats. +var fullCommitSHA = regexp.MustCompile(`^([0-9a-f]{40}|[0-9a-f]{64})$`) + +// IsFullCommitSHA reports whether s is a complete commit SHA. A base revision +// must be one, because the consumers of it reject an abbreviated SHA. +func IsFullCommitSHA(s string) bool { + return fullCommitSHA.MatchString(s) +} // Resolver returns the commit a queue's new batches are analyzed against. type Resolver interface { diff --git a/submitqueue/extension/baserevision/git/git.go b/submitqueue/extension/baserevision/git/git.go index 3c21bd070..1c249ee08 100644 --- a/submitqueue/extension/baserevision/git/git.go +++ b/submitqueue/extension/baserevision/git/git.go @@ -20,7 +20,6 @@ package git import ( "context" "fmt" - "regexp" "strings" "sync" @@ -30,10 +29,6 @@ import ( const defaultRemote = "origin" -// fullCommitSHA matches the SHA-1 and SHA-256 object formats. Tango needs the -// full SHA, so an abbreviated one is rejected. -var fullCommitSHA = regexp.MustCompile(`^([0-9a-f]{40}|[0-9a-f]{64})$`) - // Target locates the branch whose tip is a queue's base revision. type Target struct { // Repo identifies the repository to the workspace factory. @@ -103,7 +98,7 @@ func (r resolver) Resolve(ctx context.Context, queue string) (string, error) { } sha := strings.TrimSpace(outputs[1].Stdout) - if !fullCommitSHA.MatchString(sha) { + if !baserevision.IsFullCommitSHA(sha) { return "", fmt.Errorf("git returned %q, not a full commit SHA", sha) } return sha, nil diff --git a/submitqueue/extension/baserevision/github/BUILD.bazel b/submitqueue/extension/baserevision/github/BUILD.bazel new file mode 100644 index 000000000..3bfabf47d --- /dev/null +++ b/submitqueue/extension/baserevision/github/BUILD.bazel @@ -0,0 +1,23 @@ +load("@rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["github.go"], + importpath = "github.com/uber/submitqueue/submitqueue/extension/baserevision/github", + visibility = ["//visibility:public"], + deps = [ + "//platform/http:go_default_library", + "//submitqueue/extension/baserevision:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["github_test.go"], + embed = [":go_default_library"], + deps = [ + "//platform/http:go_default_library", + "@com_github_stretchr_testify//assert:go_default_library", + "@com_github_stretchr_testify//require:go_default_library", + ], +) diff --git a/submitqueue/extension/baserevision/github/github.go b/submitqueue/extension/baserevision/github/github.go new file mode 100644 index 000000000..5b4324c6d --- /dev/null +++ b/submitqueue/extension/baserevision/github/github.go @@ -0,0 +1,100 @@ +// Copyright (c) 2026 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 github resolves the base revision of a queue by asking the GitHub +// REST API for the tip of the queue's target branch, so it needs no clone. +package github + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + + phttp "github.com/uber/submitqueue/platform/http" + "github.com/uber/submitqueue/submitqueue/extension/baserevision" +) + +// Target locates the branch whose tip is a queue's base revision. +type Target struct { + // Owner is the organization or user that owns the repository. + Owner string + + // Repo is the repository name. + Repo string + + // Branch is the target branch. + Branch string +} + +// Params are the dependencies of a Resolver. +type Params struct { + // HTTPClient resolves a relative path against the GitHub API root and + // carries the credentials, as with the GitHub change provider. + HTTPClient *http.Client + + // Targets maps each queue to its target branch. + Targets map[string]Target +} + +type resolver struct { + Params +} + +// New returns a Resolver that reads the target branch tip of each queue from +// GitHub as of the call. A queue without a target, or any API failure, is an +// error. +func New(params Params) baserevision.Resolver { + return resolver{params} +} + +func (r resolver) Resolve(ctx context.Context, queue string) (string, error) { + target, ok := r.Targets[queue] + if !ok { + return "", fmt.Errorf("no target branch configured for queue %s", queue) + } + + path := (&url.URL{Path: fmt.Sprintf("/repos/%s/%s/git/ref/heads/%s", target.Owner, target.Repo, target.Branch)}).EscapedPath() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, path, nil) + if err != nil { + return "", fmt.Errorf("failed to create HTTP request: %w", err) + } + req.Header.Set("Accept", "application/vnd.github+json") + + resp, err := r.HTTPClient.Do(req) + if err != nil { + return "", fmt.Errorf("failed to read branch %s of %s/%s: %w", target.Branch, target.Owner, target.Repo, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return "", fmt.Errorf("GitHub API request failed: %w", phttp.NewStatusError(resp.StatusCode, body)) + } + + var ref struct { + Object struct { + SHA string `json:"sha"` + } `json:"object"` + } + if err := json.NewDecoder(resp.Body).Decode(&ref); err != nil { + return "", fmt.Errorf("failed to decode the branch response: %w", err) + } + if !baserevision.IsFullCommitSHA(ref.Object.SHA) { + return "", fmt.Errorf("GitHub returned %q, not a full commit SHA", ref.Object.SHA) + } + return ref.Object.SHA, nil +} diff --git a/submitqueue/extension/baserevision/github/github_test.go b/submitqueue/extension/baserevision/github/github_test.go new file mode 100644 index 000000000..529cb6e25 --- /dev/null +++ b/submitqueue/extension/baserevision/github/github_test.go @@ -0,0 +1,121 @@ +// Copyright (c) 2026 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 github + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + phttp "github.com/uber/submitqueue/platform/http" +) + +const ( + testQueue = "monorepo" + testSHA = "9f2c1d0e3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d" +) + +func TestResolver_Resolve(t *testing.T) { + tests := []struct { + name string + queue string + target Target + status int + body string + wantPath string + want string + wantErr bool + }{ + { + name: "returns the branch tip", + queue: testQueue, + target: Target{Owner: "org", Repo: "repo", Branch: "main"}, + status: http.StatusOK, + body: fmt.Sprintf(`{"ref":"refs/heads/main","object":{"sha":%q,"type":"commit"}}`, testSHA), + wantPath: "/repos/org/repo/git/ref/heads/main", + want: testSHA, + }, + { + name: "reads a branch whose name has a slash", + queue: testQueue, + target: Target{Owner: "org", Repo: "repo", Branch: "release/1.0"}, + status: http.StatusOK, + body: fmt.Sprintf(`{"object":{"sha":%q}}`, testSHA), + wantPath: "/repos/org/repo/git/ref/heads/release/1.0", + want: testSHA, + }, + { + name: "queue without a target fails", + queue: "other", + target: Target{Owner: "org", Repo: "repo", Branch: "main"}, + wantErr: true, + }, + { + name: "missing branch fails", + queue: testQueue, + target: Target{Owner: "org", Repo: "repo", Branch: "gone"}, + status: http.StatusNotFound, + body: `{"message":"Not Found"}`, + wantPath: "/repos/org/repo/git/ref/heads/gone", + wantErr: true, + }, + { + name: "malformed response fails", + queue: testQueue, + target: Target{Owner: "org", Repo: "repo", Branch: "main"}, + status: http.StatusOK, + body: `not json`, + wantPath: "/repos/org/repo/git/ref/heads/main", + wantErr: true, + }, + { + name: "abbreviated SHA fails", + queue: testQueue, + target: Target{Owner: "org", Repo: "repo", Branch: "main"}, + status: http.StatusOK, + body: `{"object":{"sha":"9f2c1d0"}}`, + wantPath: "/repos/org/repo/git/ref/heads/main", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var gotPath string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotPath = r.URL.EscapedPath() + w.WriteHeader(tt.status) + _, _ = w.Write([]byte(tt.body)) + })) + t.Cleanup(server.Close) + client, err := phttp.NewClient(server.URL) + require.NoError(t, err) + + got, err := New(Params{HTTPClient: client, Targets: map[string]Target{testQueue: tt.target}}).Resolve(context.Background(), tt.queue) + + assert.Equal(t, tt.wantPath, gotPath) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/submitqueue/extension/conflict/tango/README.md b/submitqueue/extension/conflict/tango/README.md index 00ccac76c..81c612c8a 100644 --- a/submitqueue/extension/conflict/tango/README.md +++ b/submitqueue/extension/conflict/tango/README.md @@ -20,7 +20,7 @@ Each signature is computed against trunk at its own analysis time, without the o ## Injected pieces -The analyzer owns no connection. A deployment supplies the Tango client adapter, which collects the response and resolves each target's label, and a `baserevision.Resolver` that returns the commit SHA of the target branch tip a new batch is analyzed against. The `baserevision` extension holds the contract and a git implementation over any `gitworkspace.Factory`. The changes of a batch come from the changeset resolver, the signature store is bound to one queue, and one signature cache can be shared by every queue's analyzer. The MySQL implementations of the signature and dictionary stores are in `mysql/`. +The analyzer owns no connection. A deployment supplies the Tango client adapter, which collects the response and resolves each target's label, and a `baserevision.Resolver` that returns the commit SHA of the target branch tip a new batch is analyzed against. The `baserevision` extension holds the contract and implementations that read the branch tip from a git workspace or from the GitHub API. The changes of a batch come from the changeset resolver, the signature store is bound to one queue, and one signature cache can be shared by every queue's analyzer. The MySQL implementations of the signature and dictionary stores are in `mysql/`. ## Reference orchestrator