feat: Add support for Github Enterprise - #199
Conversation
… github enterprise api calls.
There was a problem hiding this comment.
Code Review
This pull request introduces support for GitHub Enterprise instances by adding a new github-api-url configuration option. This option allows users to specify a custom GitHub API base URL, which is then correctly parsed and applied to the GitHub client, with comprehensive unit tests added to verify various URL formats. There are no review comments, and I have no additional feedback to provide.
Confidence Score: 4/5This PR should not merge until host-only GitHub Enterprise Server endpoints beginning with The new endpoint flow is internally consistent, but a realistic Enterprise hostname shape causes every REST request to target the host root, preventing pull-request initialization and all subsequent processing. Files Needing Attention: internal/github/gh.go and internal/github/gh_test.go
|
| Filename | Overview |
|---|---|
| action.yml | Adds the optional Enterprise API input and consistently exports it to the executable. |
| main.go | Adds the CLI/environment endpoint flag and propagates it into application configuration. |
| internal/app/app.go | Carries the configured endpoint into GitHub client construction. |
| internal/github/gh.go | Configures Enterprise URLs, but host-only GHES names beginning with api. can resolve to the wrong REST root. |
| internal/github/gh_test.go | Covers common GHES and data-residency URL forms but omits the failing api.-prefixed GHES hostname case. |
| README.md | Documents host-only and suffixed Enterprise endpoints, including the input form affected by the normalization edge case. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
A[github-api-url action input] --> B[INPUT_GITHUB-API-URL]
B --> C[api-url flag]
C --> D[app.Config.ApiUrl]
D --> E[gh.NewClient]
E --> F[go-github BaseURL]
F --> G[GitHub Enterprise API requests]
Reviews (1): Last reviewed commit: "Add an option for customizing base githu..." | Re-trigger Greptile
| // WithEnterpriseURLs appends api/uploads/ to the upload URL, so strip | ||
| // any api/v3 suffix to avoid ending up with api/v3/api/uploads/. | ||
| uploadUrl := strings.TrimSuffix(strings.TrimSuffix(apiUrl, "/"), "/api/v3") | ||
| opts = append(opts, github.WithEnterpriseURLs(apiUrl, uploadUrl)) |
There was a problem hiding this comment.
When a host-only GitHub Enterprise Server URL begins with api., passing it directly to WithEnterpriseURLs prevents /api/v3/ from being appended, causing InitPR and subsequent REST operations to target the host root and fail.
Knowledge Base Used: GitHub API integration
BakerNet
left a comment
There was a problem hiding this comment.
Thanks so much for the contribution, @darshanparajuli — Enterprise support is a great addition, and the test table is much appreciated! 🎉
The implementation works well for the common cases; just a few changes requested, mostly around making the URL handling foolproof (details inline):
- Default
github-api-urlto${{ github.api_url }}so GHES works out of the box — and a forgotten input can't silently send an enterprise token to public GitHub. - With that default,
NewClientcan use the URL verbatim instead of relying on go-github's hostname heuristics. - Align the README /
action.ymlguidance on the/api/v3suffix (it's only optional for some hostnames).
One note that can't be inline since the file isn't in the diff: scripts/install-action.sh still downloads release binaries from public github.com, so tagged releases won't bootstrap on egress-restricted GHES. A sentence in the README's Enterprise section about that prerequisite would be great (or a follow-up to honor GITHUB_SERVER_URL).
Thanks again — happy to clarify any of these!
| description: 'GitHub API base URL. Set for GitHub Enterprise, e.g. https://ghe.example.com/api/v3. Defaults to the public GitHub API.' | ||
| required: false | ||
| default: '' |
There was a problem hiding this comment.
The runner always exports the instance's API URL, so defaulting to it makes GHES work with zero config — and avoids the footgun where someone forgets this input and the action silently talks to public api.github.com with their enterprise token. (A getEnv fallback in main.go wouldn't catch it, since the run step always exports INPUT_GITHUB-API-URL, even when empty.)
| description: 'GitHub API base URL. Set for GitHub Enterprise, e.g. https://ghe.example.com/api/v3. Defaults to the public GitHub API.' | |
| required: false | |
| default: '' | |
| description: 'GitHub API base URL. Defaults to the API URL of the instance running the workflow. If overriding, use the exact value of the `github.api_url` context (GitHub Enterprise Server includes the /api/v3 suffix; GHE Cloud with data residency does not).' | |
| required: false | |
| default: '${{ github.api_url }}' |
| if apiUrl != "" { | ||
| // WithEnterpriseURLs appends api/uploads/ to the upload URL, so strip | ||
| // any api/v3 suffix to avoid ending up with api/v3/api/uploads/. | ||
| uploadUrl := strings.TrimSuffix(strings.TrimSuffix(apiUrl, "/"), "/api/v3") | ||
| opts = append(opts, github.WithEnterpriseURLs(apiUrl, uploadUrl)) | ||
| } |
There was a problem hiding this comment.
This works for the common cases, but WithEnterpriseURLs guesses the layout from the hostname (e.g. it skips appending /api/v3 for hosts starting with api.), and a doubled trailing slash slips past the single TrimSuffix and yields .../api/v3//api/v3/. If the input defaults to github.api_url (see action.yml comment), the value is already the exact API base URL on every platform, so we can skip the guessing entirely:
apiUrl = strings.TrimSpace(apiUrl)
if apiUrl != "" {
base, err := url.Parse(strings.TrimRight(apiUrl, "/") + "/")
if err != nil {
return nil, fmt.Errorf("invalid api url %q: %w", apiUrl, err)
}
if (base.Scheme != "http" && base.Scheme != "https") || base.Host == "" {
return nil, fmt.Errorf("api url %q must be an absolute http(s) URL", apiUrl)
}
client.BaseURL = base
}(after the plain github.NewClient(github.WithAuthToken(token)) call)
Bonus: this trims stray whitespace and catches schemeless typos like ghe.example.com at startup instead of failing later with an opaque transport error. I'd drop the upload URL handling entirely — nothing in the repo calls upload endpoints.
| github-api-url: 'https://ghe.example.com' | ||
| ``` | ||
|
|
||
| The `/api/v3` suffix is optional. For GitHub Enterprise Cloud with data residency, use `https://api.<tenant>.ghe.com`. If the input is not set, the public GitHub API is used. |
There was a problem hiding this comment.
Small correction: the suffix is only optional for some hostnames — go-github skips appending /api/v3 when the host starts with api., so e.g. a GHES instance at api.corp.example.com would break without it. Simplest guidance that's always right:
| The `/api/v3` suffix is optional. For GitHub Enterprise Cloud with data residency, use `https://api.<tenant>.ghe.com`. If the input is not set, the public GitHub API is used. | |
| The value should be the instance's exact API URL — the same value as the `github.api_url` context (`GITHUB_API_URL`). For GitHub Enterprise Server this includes the `/api/v3` suffix; for GitHub Enterprise Cloud with data residency use `https://api.<tenant>.ghe.com`. If the input is not set, it defaults to the API URL of the instance running the workflow. |
| name: "enterprise cloud with data residency", | ||
| apiUrl: "https://api.acme.ghe.com", | ||
| expectedBase: "https://api.acme.ghe.com/", | ||
| expectedUpload: "https://api.acme.ghe.com/", |
There was a problem hiding this comment.
Tiny note: GitHub's actual upload host for data residency is uploads.<tenant>.ghe.com, so this asserts a value that wouldn't work if we ever add uploads. Since nothing in the repo calls upload endpoints, I'd just drop the expectedUpload column (it goes away naturally if NewClient sets BaseURL directly).
| if got := client.client.BaseURL(); got != "https://api.github.com/" { | ||
| t.Errorf("Expected base URL to be https://api.github.com/, got %s", got) | ||
| } |
There was a problem hiding this comment.
nit (optional): this duplicates the empty uses public GitHub row in the new table below — fine to let the table own default-URL coverage.
Summary / Background
My team is looking into using codeowners-plus in our github enterprise repo. I noticed that codeowners-plus internally uses public GH apis, and this change adds an option to set a different base api url for Github so that it can be used in github enterprise environment as well.