Skip to content

Commit 4b3552c

Browse files
authored
👔 Enhance hostname normalization (#125)
1 parent 40e6020 commit 4b3552c

File tree

3 files changed

+88
-18
lines changed

3 files changed

+88
-18
lines changed

‎readme.md‎

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,10 @@ $ action-reporting-cli --<scope> <name> --<report-options> --<output-options>
7777

7878
### Authentication and Connection
7979

80-
| Option | Description | Default |
81-
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
82-
| `--token`,<br/>`-t` | Your GitHub Personal Access Token | Environment variable `GITHUB_TOKEN` |
83-
| `--hostname` | GitHub Enterprise Server hostname or GitHub Enterprise Cloud with Data Residency endpoint:<br/>- For GitHub Enterprise Server: `github.example.com`<br/>- For GitHub Enterprise Cloud with Data Residency: `api.example.ghe.com` | `api.github.com` |
80+
| Option | Description | Default |
81+
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
82+
| `--token`,<br/>`-t` | Your GitHub Personal Access Token | Environment variable `GITHUB_TOKEN` |
83+
| `--hostname` | GitHub Enterprise Server hostname **or** GitHub Enterprise Cloud with Data Residency regional hostname.<br/>Examples:<br/>- GHES: `github.example.com` (resolved -> `https://github.example.com/api/v3`)<br/>- GHEC+DR: `example.ghe.com` (auto-resolved -> `https://api.example.ghe.com`)<br/>- GHEC+DR (explicit API host also accepted): `api.example.ghe.com` (left unchanged)<br/>If omitted, defaults to public: `https://api.github.com` | `api.github.com` |
8484

8585
### Report Content Options
8686

@@ -130,6 +130,35 @@ The tool generates reports in your specified format(s):
130130

131131
When you use `--unique both` with `--uses`, you'll get an additional file with the `.unique` suffix containing only unique third-party actions.
132132

133+
### Hostname Handling
134+
135+
The CLI automatically normalizes the GitHub API base URL based on the value you pass to `--hostname`:
136+
137+
| Input Provided | Resolved API Base URL | Classification |
138+
| ----------------------------- | ----------------------------------- | ---------------------------------- |
139+
| (none) | `https://api.github.com` | Public GitHub |
140+
| `github.example.com` | `https://github.example.com/api/v3` | GitHub Enterprise Server (GHES) |
141+
| `https://github.example.com/` | `https://github.example.com/api/v3` | GHES (normalized) |
142+
| `example.ghe.com` | `https://api.example.ghe.com` | GHEC+DR regional (prefixed) |
143+
| `api.example.ghe.com` | `https://api.example.ghe.com` | GHEC+DR regional (already API) |
144+
| `HTTPS://Api.Example.GHE.COM` | `https://api.example.ghe.com` | GHEC+DR (case + protocol stripped) |
145+
| `example.ghe.com/anything` | `https://api.example.ghe.com` | GHEC+DR (path stripped) |
146+
147+
Rules applied in order:
148+
149+
1. If no hostname is provided, use the public API.
150+
2. If the hostname ends with `.ghe.com` (GitHub Enterprise Cloud with Data Residency):
151+
152+
- If it already starts with `api.`, it's used as-is.
153+
- Otherwise `api.` is prefixed (no `/api/v3` suffix is added).
154+
155+
3. All other custom hostnames are treated as GitHub Enterprise Server and `/api/v3` is appended.
156+
4. Protocol, trailing slashes, mixed case, and extraneous path segments are stripped/normalized.
157+
158+
You can safely pass either the regional base (`example.ghe.com`) or the explicit API host (`api.example.ghe.com`); both resolve to the correct endpoint.
159+
160+
> Tip: If your environment migrates between public GitHub and GHES/GHEC+DR, you can centralize logic by always providing `--hostname` and letting the tool normalize.
161+
133162
## Examples
134163

135164
Here are some common usage scenarios to help you get started:

‎src/github/octokit.js‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,25 @@ export default function getOctokit(token, hostname = null, debug = false) {
2727
// Create separate logger instance for Octokit
2828
const logger = log('octokit', token, debug, true)
2929

30-
// Normalize hostname for GitHub Enterprise servers
30+
// Normalize hostname for GitHub Enterprise (Server or Cloud with Data Residency)
31+
// Rules:
32+
// - If hostname ends with `.ghe.com` (GHEC+DR regional hostname like `example.ghe.com`),
33+
// the API hostname is `https://api.<hostname>` (no `/api/v3` suffix).
34+
// - Otherwise (enterprise server host provided), append `/api/v3`.
35+
// - If no hostname provided, use public github.com API.
3136
if (hostname) {
3237
const normalizedHost = normalizeUrl(hostname, {
3338
removeTrailingSlash: true,
3439
stripProtocol: true,
3540
}).split('/')[0]
3641

37-
hostname = `https://${normalizedHost}/api/v3`
42+
if (/\.ghe\.com$/i.test(normalizedHost)) {
43+
// GHEC+DR regional host. If already api-prefixed, keep as-is; else prefix with api.
44+
const gheCloudHost = normalizedHost.startsWith('api.') ? normalizedHost : `api.${normalizedHost}`
45+
hostname = `https://${gheCloudHost}`
46+
} else {
47+
hostname = `https://${normalizedHost}/api/v3`
48+
}
3849
} else {
3950
hostname = 'https://api.github.com'
4051
}
@@ -70,3 +81,12 @@ export default function getOctokit(token, hostname = null, debug = false) {
7081

7182
return instance
7283
}
84+
85+
/**
86+
* Helper to retrieve the resolved baseUrl from an Octokit instance.
87+
* @param {import('@octokit/core').Octokit} octokit
88+
* @returns {string|undefined}
89+
*/
90+
export function getBaseUrl(octokit) {
91+
return octokit?.request?.endpoint?.DEFAULTS?.baseUrl
92+
}

‎test/github/octokit.test.js‎

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
*/
44
import {jest} from '@jest/globals'
55

6+
// Reset singleton between tests by clearing module cache
7+
beforeEach(() => {
8+
jest.resetModules()
9+
})
10+
611
describe('octokit', () => {
712
beforeEach(() => {})
813

@@ -11,33 +16,49 @@ describe('octokit', () => {
1116
/**
1217
* Test that Octokit client is created with correct token and options.
1318
*/
14-
test('should create Octokit client with valid token and options', () => {
15-
// TODO: Implement test logic
16-
expect(true).toBe(true)
19+
test('should create Octokit client with valid token and default api.github.com', async () => {
20+
const {default: getOctokit, getBaseUrl} = await import('../../src/github/octokit.js')
21+
const client = getOctokit('token')
22+
expect(getBaseUrl(client)).toBe('https://api.github.com')
1723
})
1824

1925
/**
2026
* Test constructor options
2127
*/
2228
describe('constructor options', () => {
23-
test('should handle all constructor parameters', () => {
24-
// TODO: Implement test logic
25-
expect(true).toBe(true)
29+
test('should normalize classic GHE hostname to /api/v3', async () => {
30+
const {default: getOctokit, getBaseUrl} = await import('../../src/github/octokit.js')
31+
const client = getOctokit('token', 'ghe.internal.example.com')
32+
expect(getBaseUrl(client)).toBe('https://ghe.internal.example.com/api/v3')
33+
})
34+
35+
test('should transform GHEC+DR hostname *.ghe.com to api.<host>', async () => {
36+
const {default: getOctokit, getBaseUrl} = await import('../../src/github/octokit.js')
37+
const client = getOctokit('token', 'region1.ghe.com')
38+
expect(getBaseUrl(client)).toBe('https://api.region1.ghe.com')
39+
})
40+
41+
test('should preserve already api-prefixed GHEC+DR hostname without /api/v3', async () => {
42+
const {default: getOctokit, getBaseUrl} = await import('../../src/github/octokit.js')
43+
const client = getOctokit('token', 'api.region1.ghe.com')
44+
expect(getBaseUrl(client)).toBe('https://api.region1.ghe.com')
2645
})
2746

28-
test('should handle default values', () => {
29-
// TODO: Implement test logic
30-
expect(true).toBe(true)
47+
test('should handle default values', async () => {
48+
const {default: getOctokit, getBaseUrl} = await import('../../src/github/octokit.js')
49+
const client = getOctokit('token')
50+
expect(getBaseUrl(client)).toBe('https://api.github.com')
3151
})
3252
})
3353

3454
/**
3555
* Test client operations
3656
*/
3757
describe('client operations', () => {
38-
test('should perform API requests correctly', () => {
39-
// TODO: Implement test logic
40-
expect(true).toBe(true)
58+
test('should perform API requests correctly', async () => {
59+
const {default: getOctokit} = await import('../../src/github/octokit.js')
60+
const client = getOctokit('token')
61+
expect(typeof client.request).toBe('function')
4162
})
4263
})
4364
})

0 commit comments

Comments
 (0)