Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions githubapp/caching_client_creator.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,13 @@ const (
// NewDefaultCachingClientCreator returns a ClientCreator using values from the
// configuration or other defaults.
func NewDefaultCachingClientCreator(c Config, opts ...ClientOption) (ClientCreator, error) {
urls, err := c.GetURLs()
if err != nil {
return nil, err
}
delegate := NewClientCreator(
c.V3APIURL,
c.V4APIURL,
urls.APIv3.String(),
urls.APIv4.String(),
c.App.IntegrationID,
[]byte(c.App.PrivateKey),
opts...,
Expand Down
63 changes: 60 additions & 3 deletions githubapp/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@
package githubapp

import (
"net/url"
"os"
"strconv"
)

type Config struct {
// BaseURL is a convenience field that sets Web, V3APIURL, and V4APIURL
// automatically via ParseURLs. Explicit URL fields take precedence.
BaseURL string `yaml:"base_url" json:"baseUrl"`
WebURL string `yaml:"web_url" json:"webUrl"`
V3APIURL string `yaml:"v3_api_url" json:"v3ApiUrl"`
V4APIURL string `yaml:"v4_api_url" json:"v4ApiUrl"`
Expand All @@ -36,10 +40,10 @@ type Config struct {
} `yaml:"oauth" json:"oauth"`
}

// SetValuesFromEnv sets values in the configuration from coresponding
// environment variables, if they exist. The optional prefix is added to the
// start of the environment variable names.
// SetValuesFromEnv sets configuration values from environment variables.
// The optional prefix is prepended to each variable name.
func (c *Config) SetValuesFromEnv(prefix string) {
setStringFromEnv("GITHUB_BASE_URL", prefix, &c.BaseURL)
setStringFromEnv("GITHUB_WEB_URL", prefix, &c.WebURL)
setStringFromEnv("GITHUB_V3_API_URL", prefix, &c.V3APIURL)
setStringFromEnv("GITHUB_V4_API_URL", prefix, &c.V4APIURL)
Expand All @@ -52,6 +56,59 @@ func (c *Config) SetValuesFromEnv(prefix string) {
setStringFromEnv("GITHUB_OAUTH_CLIENT_SECRET", prefix, &c.OAuth.ClientSecret)
}

// GetURLs resolves the GitHub endpoint URLs for this configuration.
// Explicit URL fields (WebURL, V3APIURL, V4APIURL) take precedence over
// BaseURL. If none are set, it falls back to ParseURLs("github.com").
func (c *Config) GetURLs() (*URLs, error) {
// all three explicit URLs are set — use them directly
if c.WebURL != "" && c.V3APIURL != "" && c.V4APIURL != "" {
return parseExplicitURLs(c.WebURL, c.V3APIURL, c.V4APIURL)
}

// derive from BaseURL, then overlay any explicit overrides
base := c.BaseURL
if base == "" {
base = githubPublicHost
}
urls, err := ParseURLs(base)
if err != nil {
return nil, err
}

if c.WebURL != "" {
if urls.Web, err = url.Parse(c.WebURL); err != nil {
return nil, err
}
}
if c.V3APIURL != "" {
if urls.APIv3, err = url.Parse(c.V3APIURL); err != nil {
return nil, err
}
}
if c.V4APIURL != "" {
if urls.APIv4, err = url.Parse(c.V4APIURL); err != nil {
return nil, err
}
}
return urls, nil
}

func parseExplicitURLs(web, v3, v4 string) (*URLs, error) {
wu, err := url.Parse(web)
if err != nil {
return nil, err
}
v3u, err := url.Parse(v3)
if err != nil {
return nil, err
}
v4u, err := url.Parse(v4)
if err != nil {
return nil, err
}
return &URLs{Web: wu, APIv3: v3u, APIv4: v4u}, nil
}

func setStringFromEnv(key, prefix string, value *string) {
if v, ok := os.LookupEnv(prefix + key); ok {
*value = v
Expand Down
87 changes: 87 additions & 0 deletions githubapp/urls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright 2024 Palantir 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 githubapp

import (
"fmt"
"net/url"
"strings"
)

const (
githubPublicHost = "github.com"

githubPublicV3URL = "https://api.github.com/"
githubPublicV4URL = "https://api.github.com/graphql"
githubPublicWebURL = "https://github.com"
)

// URLs holds the resolved endpoint URLs for a GitHub deployment.
type URLs struct {
Web *url.URL
APIv3 *url.URL
APIv4 *url.URL
}

// ParseURLs resolves all GitHub endpoint URLs from a single base URL.
// For github.com it returns the known public API endpoints; for any other
// host it constructs the standard GitHub Enterprise Server paths.
func ParseURLs(baseURL string) (*URLs, error) {
if baseURL == "" {
return nil, fmt.Errorf("githubapp: base URL must not be empty")
}

// normalize: add scheme if missing so url.Parse works reliably
if !strings.Contains(baseURL, "://") {
baseURL = "https://" + baseURL
}

u, err := url.Parse(baseURL)
if err != nil {
return nil, fmt.Errorf("githubapp: invalid base URL %q: %w", baseURL, err)
}

host := strings.ToLower(u.Hostname())
if host == githubPublicHost {
return publicURLs()
}
return enterpriseURLs(u)
}

func publicURLs() (*URLs, error) {
web, _ := url.Parse(githubPublicWebURL)
v3, _ := url.Parse(githubPublicV3URL)
v4, _ := url.Parse(githubPublicV4URL)
return &URLs{Web: web, APIv3: v3, APIv4: v4}, nil
}

func enterpriseURLs(base *url.URL) (*URLs, error) {
// strip any path so we build from the root of the host
root := &url.URL{Scheme: base.Scheme, Host: base.Host}

v3, err := root.Parse("api/v3/")
if err != nil {
return nil, fmt.Errorf("githubapp: could not construct v3 API URL: %w", err)
}

v4, err := root.Parse("api/graphql")
if err != nil {
return nil, fmt.Errorf("githubapp: could not construct v4 API URL: %w", err)
}

web := &url.URL{Scheme: root.Scheme, Host: root.Host, Path: "/"}

return &URLs{Web: web, APIv3: v3, APIv4: v4}, nil
}
152 changes: 152 additions & 0 deletions githubapp/urls_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Copyright 2024 Palantir 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 githubapp

import (
"testing"
)

func TestParseURLs(t *testing.T) {
tests := map[string]struct {
input string
wantWeb string
wantV3 string
wantV4 string
wantErr bool
}{
"publicDomain": {
input: "github.com",
wantWeb: "https://github.com",
wantV3: "https://api.github.com/",
wantV4: "https://api.github.com/graphql",
},
"publicHTTPS": {
input: "https://github.com",
wantWeb: "https://github.com",
wantV3: "https://api.github.com/",
wantV4: "https://api.github.com/graphql",
},
"enterprise": {
input: "https://github.example.com",
wantWeb: "https://github.example.com/",
wantV3: "https://github.example.com/api/v3/",
wantV4: "https://github.example.com/api/graphql",
},
"enterpriseNoScheme": {
input: "github.example.com",
wantWeb: "https://github.example.com/",
wantV3: "https://github.example.com/api/v3/",
wantV4: "https://github.example.com/api/graphql",
},
"enterpriseWithPath": {
input: "https://github.example.com/some/path",
wantWeb: "https://github.example.com/",
wantV3: "https://github.example.com/api/v3/",
wantV4: "https://github.example.com/api/graphql",
},
"empty": {
input: "",
wantErr: true,
},
}

for name, tt := range tests {
t.Run(name, func(t *testing.T) {
got, err := ParseURLs(tt.input)
if tt.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got.Web.String() != tt.wantWeb {
t.Errorf("Web: got %q, want %q", got.Web, tt.wantWeb)
}
if got.APIv3.String() != tt.wantV3 {
t.Errorf("APIv3: got %q, want %q", got.APIv3, tt.wantV3)
}
if got.APIv4.String() != tt.wantV4 {
t.Errorf("APIv4: got %q, want %q", got.APIv4, tt.wantV4)
}
})
}
}

func TestConfigGetURLs(t *testing.T) {
tests := map[string]struct {
config Config
wantV3 string
wantV4 string
wantErr bool
}{
"baseURLPublic": {
config: Config{BaseURL: "github.com"},
wantV3: "https://api.github.com/",
wantV4: "https://api.github.com/graphql",
},
"baseURLEnterprise": {
config: Config{BaseURL: "https://github.example.com"},
wantV3: "https://github.example.com/api/v3/",
wantV4: "https://github.example.com/api/graphql",
},
"explicitURLsOverrideBase": {
config: Config{
BaseURL: "https://github.example.com",
V3APIURL: "https://github.example.com/api/v3/",
V4APIURL: "https://github.example.com/api/graphql",
WebURL: "https://github.example.com",
},
wantV3: "https://github.example.com/api/v3/",
wantV4: "https://github.example.com/api/graphql",
},
"noFieldsDefaultsToPublic": {
config: Config{},
wantV3: "https://api.github.com/",
wantV4: "https://api.github.com/graphql",
},
"partialOverride": {
config: Config{
BaseURL: "https://github.example.com",
V3APIURL: "https://custom.example.com/api/v3/",
},
wantV3: "https://custom.example.com/api/v3/",
wantV4: "https://github.example.com/api/graphql",
},
}

for name, tt := range tests {
t.Run(name, func(t *testing.T) {
got, err := tt.config.GetURLs()
if tt.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got.APIv3.String() != tt.wantV3 {
t.Errorf("APIv3: got %q, want %q", got.APIv3, tt.wantV3)
}
if got.APIv4.String() != tt.wantV4 {
t.Errorf("APIv4: got %q, want %q", got.APIv4, tt.wantV4)
}
})
}
}