diff --git a/internal/collector/collector.go b/internal/collector/collector.go index ce626f8..d7e678b 100644 --- a/internal/collector/collector.go +++ b/internal/collector/collector.go @@ -54,6 +54,7 @@ type Collector struct { repos map[string]model.Repo now func() time.Time log *slog.Logger + webBase string kick chan struct{} names []string th config.Thresholds @@ -78,6 +79,7 @@ func New(f Fetcher, cfg *config.Config, log *slog.Logger) *Collector { interval: cfg.RefreshInterval, concurrency: cfg.Concurrency, now: time.Now, + webBase: config.WebBase(cfg.GitHub.APIURL), log: log, kick: make(chan struct{}, 1), repos: make(map[string]model.Repo, len(cfg.Repos)), @@ -174,7 +176,7 @@ func (c *Collector) refreshOne(ctx context.Context, name string, at time.Time) m prev, ok := c.repos[name] c.mu.RUnlock() if !ok { - prev = model.Repo{FullName: name, URL: "https://github.com/" + name} + prev = model.Repo{FullName: name, URL: c.webBase + "/" + name} } prev.Up, prev.Err = false, err.Error() return prev diff --git a/internal/collector/collector_test.go b/internal/collector/collector_test.go index 0ede1dc..0c2d98b 100644 --- a/internal/collector/collector_test.go +++ b/internal/collector/collector_test.go @@ -337,3 +337,17 @@ func TestScanProgressAdvancesAsReposFinish(t *testing.T) { t.Fatalf("after scan: done=%d total=%d", v.ScanDone, v.ScanTotal) } } + +func TestFailedFirstScanLinksToTheConfiguredHost(t *testing.T) { + fail := &fakeFetcher{fn: func(string) (model.Repo, error) { return model.Repo{}, errors.New("nope") }} + cfg := &config.Config{ + Repos: []string{"o/a"}, RefreshInterval: time.Hour, Concurrency: 1, Thresholds: th(), + GitHub: config.GitHub{APIURL: "https://ghe.example.com/api/v3"}, + } + c := New(fail, cfg, slog.New(slog.NewTextHandler(io.Discard, nil))) + c.now = func() time.Time { return t0 } + c.Refresh(context.Background()) + if got := c.View().Repos[0].URL; got != "https://ghe.example.com/o/a" { + t.Fatalf("repo link = %q", got) + } +} diff --git a/internal/config/config.go b/internal/config/config.go index b709dc5..323de96 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,6 +4,7 @@ package config import ( "errors" "fmt" + "net/url" "os" "strings" "time" @@ -161,3 +162,24 @@ func normalizeRepos(in []string) ([]string, error) { } return out, nil } + +const ( + defaultWebURL = "https://github.com" + apiV3Suffix = "/api/v3" + apiHostPrefix = "api." +) + +// WebBase returns the address of the web interface that belongs to a GitHub API address: +// github.com for the public API, the host without /api/v3 for Enterprise Server, and the host +// without its "api." prefix for Enterprise Cloud with data residency. +func WebBase(apiURL string) string { + u, err := url.Parse(strings.TrimRight(apiURL, "/")) + if err != nil || u.Host == "" || strings.EqualFold(u.Host, "api.github.com") { + return defaultWebURL + } + u.Path = strings.TrimSuffix(u.Path, apiV3Suffix) + if u.Path == "" { + u.Host = strings.TrimPrefix(u.Host, apiHostPrefix) + } + return strings.TrimRight(u.String(), "/") +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index b5e493f..06a8b75 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -126,3 +126,22 @@ func TestManualRefreshCanBeDisabled(t *testing.T) { t.Fatal("explicit false must stay off") } } + +func TestWebBase(t *testing.T) { + tests := map[string]string{ + "": "https://github.com", + "https://api.github.com": "https://github.com", + "https://api.github.com/": "https://github.com", + "https://ghe.example.com/api/v3": "https://ghe.example.com", + "https://ghe.example.com/api/v3/": "https://ghe.example.com", + "http://ghe.internal:8080/api/v3": "http://ghe.internal:8080", + "https://api.acme.ghe.com": "https://acme.ghe.com", + "https://github.example.com/sub/api/v3": "https://github.example.com/sub", + "not a url": "https://github.com", + } + for in, want := range tests { + if got := WebBase(in); got != want { + t.Errorf("WebBase(%q) = %q, want %q", in, got, want) + } + } +}