Repository navigation
feat: GitHub Enterprise Server and GHE.com #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ package main | |
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
| "net/http" | ||
|
|
||
| "github.com/devblac/prw/internal/bitbucket" | ||
|
|
@@ -27,11 +28,15 @@ var newBitbucketClient = func(email, token string) *bitbucket.Client { | |
| } | ||
|
|
||
| // clientFor returns the client for PRs on host. GitHub needs a token; Bitbucket | ||
| // works anonymously for public repositories. | ||
| // works anonymously for public repositories; a GitHub Enterprise host must be the | ||
| // one in github_enterprise_url. | ||
| func clientFor(cfg *config.Config, host string) (prClient, error) { | ||
| if forge.IsBitbucket(host) { | ||
| switch { | ||
| case forge.IsBitbucket(host): | ||
| auth, _ := cfg.ResolveBitbucketAuth() | ||
| return newBitbucketClient(auth.Email, auth.Token), nil | ||
| case forge.IsEnterprise(host): | ||
| return enterpriseClient(cfg, host) | ||
| } | ||
| token := cfg.GetToken() | ||
| if token == "" { | ||
|
|
@@ -40,13 +45,29 @@ func clientFor(cfg *config.Config, host string) (prClient, error) { | |
| return newGitHubClient(token), nil | ||
| } | ||
|
|
||
| // needsGitHub reports whether the watch list or auto-watch involves GitHub. | ||
| // newEnterpriseClient allows tests to point GitHub Enterprise requests at a fake server. | ||
| var newEnterpriseClient = github.NewEnterpriseClient | ||
|
|
||
| // enterpriseClient returns the client of the GitHub Enterprise instance on host. | ||
| func enterpriseClient(cfg *config.Config, host string) (*github.Client, error) { | ||
| configured := cfg.EnterpriseHost() | ||
| if configured == "" || forge.NormalizeHost(host) != forge.NormalizeHost(configured) { | ||
| return nil, fmt.Errorf("%s is not a known host: for GitHub Enterprise, run 'prw config set github_enterprise_url https://%s'", host, host) | ||
| } | ||
| token, _ := cfg.ResolveEnterpriseToken() | ||
| if token == "" { | ||
| return nil, fmt.Errorf("missing GitHub Enterprise token for %s: run 'gh auth login --hostname %s', set GH_ENTERPRISE_TOKEN, or 'prw config set github_enterprise_token <token>'", host, host) | ||
| } | ||
| return newEnterpriseClient(cfg.GitHubEnterpriseURL, token) | ||
| } | ||
|
|
||
| // needsGitHub reports whether the watch list or auto-watch involves github.com. | ||
| func needsGitHub(cfg *config.Config) bool { | ||
| if len(cfg.AutoWatch) > 0 && !bitbucketConfigured(cfg) { | ||
| if len(cfg.AutoWatch) > 0 && !bitbucketConfigured(cfg) && cfg.EnterpriseHost() == "" { | ||
| return true // auto-watch has nothing else to search | ||
| } | ||
| for _, pr := range cfg.WatchedPRs { | ||
| if !forge.IsBitbucket(pr.Host) { | ||
| if forge.NormalizeHost(pr.Host) == forge.GitHub { | ||
| return true | ||
| } | ||
| } | ||
|
|
@@ -67,12 +88,29 @@ func autoWatchFinders(cfg *config.Config, bb *bitbucket.Client) []watcher.Finder | |
| if token := cfg.GetToken(); token != "" { | ||
| finders = append(finders, watcher.GitHubFinder{Searcher: newGitHubClient(token)}) | ||
| } | ||
| if f := enterpriseFinder(cfg); f != nil { | ||
| finders = append(finders, f) | ||
| } | ||
| if f := bitbucketFinder(cfg, bb); f != nil { | ||
| finders = append(finders, f) | ||
| } | ||
| return finders | ||
| } | ||
|
|
||
| // enterpriseFinder returns the GitHub Enterprise auto-watch finder, or nil without | ||
| // a usable github_enterprise_url and token. | ||
| func enterpriseFinder(cfg *config.Config) watcher.Finder { | ||
| host := cfg.EnterpriseHost() | ||
| if host == "" { | ||
| return nil | ||
| } | ||
| c, err := enterpriseClient(cfg, host) | ||
| if err != nil { | ||
| return nil | ||
| } | ||
| return watcher.GitHubFinder{Searcher: c, HostName: host} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For a configured Enterprise instance, this finder calls Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| // bitbucketFinder returns the Bitbucket auto-watch finder, or nil without a Bitbucket | ||
| // account. | ||
| func bitbucketFinder(cfg *config.Config, bb *bitbucket.Client) *watcher.BitbucketFinder { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "net/http" | ||
| "net/http/httptest" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/devblac/prw/internal/config" | ||
| "github.com/devblac/prw/internal/forge" | ||
| "github.com/devblac/prw/internal/github" | ||
| ) | ||
|
|
||
| func TestClientFor_Enterprise(t *testing.T) { | ||
| var gotPath, gotAuth string | ||
| srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| gotPath, gotAuth = r.URL.Path, r.Header.Get("Authorization") | ||
| fmt.Fprint(w, `{"number": 9, "title": "Add SSO", "state": "open", "head": {"sha": "abc"}}`) | ||
| })) | ||
| defer srv.Close() | ||
| old := newEnterpriseClient | ||
| newEnterpriseClient = func(webURL, token string) (*github.Client, error) { | ||
| c, err := github.NewEnterpriseClient(webURL, token) | ||
| if err == nil { | ||
| c.BaseURL = srv.URL + "/api/v3" | ||
| } | ||
| return c, err | ||
| } | ||
| t.Cleanup(func() { newEnterpriseClient = old }) | ||
| t.Setenv("GH_ENTERPRISE_TOKEN", "ghe-token") | ||
| t.Setenv("GITHUB_ENTERPRISE_TOKEN", "") | ||
|
|
||
| ref, err := forge.ParsePRURL("https://github.acme.com/team/api/pull/9") | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if _, err := clientFor(&config.Config{}, ref.Host); err == nil || !strings.Contains(err.Error(), "github_enterprise_url") { | ||
| t.Errorf("unconfigured host: err = %v", err) | ||
| } | ||
| if _, err := clientFor(&config.Config{GitHubEnterpriseURL: "https://other.example.com"}, ref.Host); err == nil { | ||
| t.Error("a host other than the configured one must be refused") | ||
| } | ||
|
|
||
| cfg := &config.Config{GitHubEnterpriseURL: "https://github.acme.com"} | ||
| client, err := clientFor(cfg, ref.Host) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| pr, err := client.GetPullRequest(context.Background(), ref.Owner, ref.Repo, ref.Number) | ||
| if err != nil || pr.Title != "Add SSO" { | ||
| t.Fatalf("pr = %+v, %v", pr, err) | ||
| } | ||
| if gotPath != "/api/v3/repos/team/api/pulls/9" || gotAuth != "Bearer ghe-token" { | ||
| t.Errorf("request to %s with %q", gotPath, gotAuth) | ||
| } | ||
|
|
||
| t.Setenv("GH_ENTERPRISE_TOKEN", "") | ||
| old2 := config.GHAuthTokenForHost | ||
| config.GHAuthTokenForHost = func(string) (string, error) { return "", fmt.Errorf("not logged in") } | ||
| t.Cleanup(func() { config.GHAuthTokenForHost = old2 }) | ||
| if _, err := clientFor(cfg, ref.Host); err == nil || !strings.Contains(err.Error(), "gh auth login --hostname github.acme.com") { | ||
| t.Errorf("missing token: err = %v", err) | ||
| } | ||
| } | ||
|
|
||
| func TestConfigSet_EnterpriseURL(t *testing.T) { | ||
| cfg := &config.Config{} | ||
| if err := applyConfigSet(cfg, "github_enterprise_url", "https://github.acme.com/"); err != nil || cfg.GitHubEnterpriseURL != "https://github.acme.com" { | ||
| t.Errorf("set: %v, %q", err, cfg.GitHubEnterpriseURL) | ||
| } | ||
| for _, bad := range []string{"https://github.com", "bitbucket.org", "ftp://x.example.com"} { | ||
| if err := applyConfigSet(cfg, "github_enterprise_url", bad); err == nil { | ||
| t.Errorf("github_enterprise_url %q should be refused", bad) | ||
| } | ||
| } | ||
| if err := applyConfigUnset(cfg, "github_enterprise_url"); err != nil || cfg.GitHubEnterpriseURL != "" { | ||
| t.Errorf("unset: %v", err) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When only Enterprise credentials are configured and auto-watch is enabled, an empty watch list still makes
needsGitHubreturn true through its finalreturn len(cfg.WatchedPRs) == 0. Thus, after all watched Enterprise PRs close—or when the initial search finds none—prw runexits witherrMissingTokenincmd/prw/main.go:435–436, before registering the Enterprise finder. Account for the configured Enterprise auto-watch host in the empty-list fallback so monitoring can start without a github.com token.Useful? React with 👍 / 👎.