Skip to content

Commit 2e7e15a

Browse files
CopilotlpcoxCopilot
authored
fix: route GHEC Copilot proxy to copilot-api subdomain (#1331)
* Initial plan * fix: route GHEC Copilot proxy to copilot-api subdomain Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: fix awf-runner timeout detection and no-docker test timeouts (#1332) * Initial plan * fix: fix awf-runner timeout detection and no-docker test timeouts --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> Co-authored-by: Landon Cox <landon.cox@microsoft.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 96676b4 commit 2e7e15a

File tree

7 files changed

+79
-30
lines changed

7 files changed

+79
-30
lines changed

containers/api-proxy/server.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ function deriveCopilotApiTarget() {
5757
return process.env.COPILOT_API_TARGET;
5858
}
5959
// Auto-derive from GITHUB_SERVER_URL:
60-
// - GitHub Enterprise Cloud (*.ghe.com) → api.<subdomain>.ghe.com
60+
// - GitHub Enterprise Cloud (*.ghe.com): Copilot inference/models/MCP are served at
61+
// copilot-api.<subdomain>.ghe.com (separate from the GitHub REST API at api.*)
6162
// - GitHub Enterprise Server (non-github.com, non-ghe.com) → api.enterprise.githubcopilot.com
6263
// - github.com → api.githubcopilot.com
6364
const serverUrl = process.env.GITHUB_SERVER_URL;
@@ -69,7 +70,9 @@ function deriveCopilotApiTarget() {
6970
if (hostname.endsWith('.ghe.com')) {
7071
// Extract subdomain: mycompany.ghe.com → mycompany
7172
const subdomain = hostname.slice(0, -8); // Remove '.ghe.com'
72-
return `api.${subdomain}.ghe.com`;
73+
// GHEC routes Copilot inference to copilot-api.<subdomain>.ghe.com,
74+
// not to api.<subdomain>.ghe.com (which is the GitHub REST API)
75+
return `copilot-api.${subdomain}.ghe.com`;
7376
}
7477
// GHES (any other non-github.com hostname)
7578
return 'api.enterprise.githubcopilot.com';

containers/api-proxy/server.test.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,24 +46,24 @@ describe('deriveCopilotApiTarget', () => {
4646
});
4747

4848
describe('GitHub Enterprise Cloud (*.ghe.com)', () => {
49-
it('should derive api.<subdomain>.ghe.com for GHEC tenants', () => {
49+
it('should derive copilot-api.<subdomain>.ghe.com for GHEC tenants', () => {
5050
process.env.GITHUB_SERVER_URL = 'https://mycompany.ghe.com';
51-
expect(deriveCopilotApiTarget()).toBe('api.mycompany.ghe.com');
51+
expect(deriveCopilotApiTarget()).toBe('copilot-api.mycompany.ghe.com');
5252
});
5353

5454
it('should handle GHEC URLs with trailing slash', () => {
5555
process.env.GITHUB_SERVER_URL = 'https://example.ghe.com/';
56-
expect(deriveCopilotApiTarget()).toBe('api.example.ghe.com');
56+
expect(deriveCopilotApiTarget()).toBe('copilot-api.example.ghe.com');
5757
});
5858

5959
it('should handle GHEC URLs with path components', () => {
6060
process.env.GITHUB_SERVER_URL = 'https://acme.ghe.com/some/path';
61-
expect(deriveCopilotApiTarget()).toBe('api.acme.ghe.com');
61+
expect(deriveCopilotApiTarget()).toBe('copilot-api.acme.ghe.com');
6262
});
6363

6464
it('should handle multi-part subdomain for GHEC', () => {
6565
process.env.GITHUB_SERVER_URL = 'https://dev.mycompany.ghe.com';
66-
expect(deriveCopilotApiTarget()).toBe('api.dev.mycompany.ghe.com');
66+
expect(deriveCopilotApiTarget()).toBe('copilot-api.dev.mycompany.ghe.com');
6767
});
6868
});
6969

docs/enterprise-configuration.md

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ When `GITHUB_SERVER_URL` is set to a `*.ghe.com` domain, AWF automatically deriv
1616

1717
```bash
1818
# Example: GITHUB_SERVER_URL=https://acme.ghe.com
19-
# AWF automatically uses: api.acme.ghe.com
19+
# AWF automatically uses: copilot-api.acme.ghe.com
2020
```
2121

2222
**How it works:**
2323
1. AWF reads `GITHUB_SERVER_URL` from your environment
2424
2. Detects that the hostname ends with `.ghe.com`
2525
3. Extracts the subdomain (e.g., `acme` from `acme.ghe.com`)
26-
4. Routes Copilot API traffic to `api.<subdomain>.ghe.com`
26+
4. Routes Copilot API traffic to `copilot-api.<subdomain>.ghe.com`
2727
5. **Auto-injects `GH_HOST` environment variable** in the agent container so the `gh` CLI targets your GHEC instance
2828

2929
**GH_HOST Auto-Injection:**
@@ -45,16 +45,22 @@ export GITHUB_SERVER_URL="https://acme.ghe.com"
4545
export GITHUB_TOKEN="<your-copilot-cli-token>"
4646

4747
sudo -E awf \
48-
--allow-domains acme.ghe.com,api.acme.ghe.com,raw.githubusercontent.com \
48+
--allow-domains acme.ghe.com,copilot-api.acme.ghe.com,copilot-telemetry-service.acme.ghe.com,raw.githubusercontent.com \
4949
--enable-api-proxy \
5050
-- npx @github/copilot@latest --prompt "your prompt here"
5151
```
5252

5353
**Domain breakdown:**
5454
- `acme.ghe.com` - Your GHEC tenant domain (git operations, web UI)
55-
- `api.acme.ghe.com` - Your tenant-specific Copilot API endpoint (automatically routed by AWF)
55+
- `api.acme.ghe.com` - GitHub REST API endpoint (AWF auto-adds this for `*.ghe.com` instances)
56+
- `copilot-api.acme.ghe.com` - Copilot inference, models, and MCP endpoint (AWF auto-adds this for `*.ghe.com` instances)
57+
- `copilot-telemetry-service.acme.ghe.com` - Copilot telemetry endpoint (AWF auto-adds this for `*.ghe.com` instances)
5658
- `raw.githubusercontent.com` - Raw content access (if using GitHub MCP server)
5759

60+
:::note
61+
AWF automatically adds `api.<slug>.ghe.com`, `copilot-api.<slug>.ghe.com`, and `copilot-telemetry-service.<slug>.ghe.com` to the firewall allowlist when `GITHUB_SERVER_URL` points to a `*.ghe.com` domain. They are listed above for transparency; you do not need to include them manually.
62+
:::
63+
5864
### GitHub Actions (GHEC)
5965

6066
In GitHub Actions workflows running on GHEC, the `GITHUB_SERVER_URL` environment variable is automatically set by GitHub Actions. No additional configuration is needed:
@@ -73,7 +79,7 @@ jobs:
7379
# GITHUB_SERVER_URL is automatically set by GitHub Actions
7480
run: |
7581
sudo -E awf \
76-
--allow-domains ${{ github.server_url_hostname }},api.${{ github.server_url_hostname }},raw.githubusercontent.com \
82+
--allow-domains raw.githubusercontent.com \
7783
--enable-api-proxy \
7884
-- npx @github/copilot@latest --prompt "generate tests"
7985
```
@@ -114,7 +120,7 @@ export GITHUB_TOKEN="<your-copilot-cli-token>"
114120
export GITHUB_PERSONAL_ACCESS_TOKEN="<your-github-pat>"
115121
116122
sudo -E awf \
117-
--allow-domains acme.ghe.com,api.acme.ghe.com,raw.githubusercontent.com,registry.npmjs.org \
123+
--allow-domains acme.ghe.com,copilot-api.acme.ghe.com,copilot-telemetry-service.acme.ghe.com,raw.githubusercontent.com,registry.npmjs.org \
118124
--enable-api-proxy \
119125
"npx @github/copilot@latest \
120126
--disable-builtin-mcps \
@@ -211,8 +217,8 @@ If automatic detection doesn't work for your setup, you can manually specify the
211217
```bash
212218
# For GHEC with custom configuration
213219
sudo awf \
214-
--allow-domains acme.ghe.com,api.acme.ghe.com \
215-
--copilot-api-target api.acme.ghe.com \
220+
--allow-domains acme.ghe.com,copilot-api.acme.ghe.com,copilot-telemetry-service.acme.ghe.com \
221+
--copilot-api-target copilot-api.acme.ghe.com \
216222
--enable-api-proxy \
217223
-- your-command
218224
@@ -231,7 +237,7 @@ The `--copilot-api-target` flag takes precedence over automatic detection.
231237
AWF determines the Copilot API endpoint in this order:
232238

233239
1. **`--copilot-api-target` flag** (highest priority) - Manual override
234-
2. **`GITHUB_SERVER_URL` with `*.ghe.com`** - Automatic GHEC detection → `api.<subdomain>.ghe.com`
240+
2. **`GITHUB_SERVER_URL` with `*.ghe.com`** - Automatic GHEC detection → `copilot-api.<subdomain>.ghe.com`
235241
3. **`GITHUB_SERVER_URL` non-github.com** - Automatic GHES detection → `api.enterprise.githubcopilot.com`
236242
4. **Default** - Public GitHub → `api.githubcopilot.com`
237243

@@ -252,7 +258,7 @@ Add `--keep-containers` to inspect the configuration:
252258

253259
```bash
254260
sudo -E awf \
255-
--allow-domains acme.ghe.com,api.acme.ghe.com \
261+
--allow-domains acme.ghe.com,copilot-api.acme.ghe.com \
256262
--enable-api-proxy \
257263
--keep-containers \
258264
-- npx @github/copilot@latest --prompt "test"
@@ -265,7 +271,7 @@ sudo -E awf \
265271
docker logs awf-api-proxy | grep "Copilot proxy"
266272
267273
# Expected for GHEC:
268-
# Copilot proxy listening on port 10002 (target: api.acme.ghe.com)
274+
# Copilot proxy listening on port 10002 (target: copilot-api.acme.ghe.com)
269275
270276
# Expected for GHES:
271277
# Copilot proxy listening on port 10002 (target: api.enterprise.githubcopilot.com)
@@ -304,7 +310,7 @@ sudo cat /tmp/squid-logs-*/access.log | grep TCP_DENIED
304310
305311
# Add the blocked domain to your allowlist
306312
sudo -E awf \
307-
--allow-domains acme.ghe.com,api.acme.ghe.com,<blocked-domain> \
313+
--allow-domains acme.ghe.com,copilot-api.acme.ghe.com,copilot-telemetry-service.acme.ghe.com,<blocked-domain> \
308314
--enable-api-proxy \
309315
-- your-command
310316
```
@@ -366,7 +372,7 @@ docker pull ghcr.io/github/github-mcp-server:latest
366372
367373
# 4. Run Copilot with AWF
368374
sudo -E awf \
369-
--allow-domains acme.ghe.com,api.acme.ghe.com,raw.githubusercontent.com,registry.npmjs.org \
375+
--allow-domains acme.ghe.com,copilot-api.acme.ghe.com,copilot-telemetry-service.acme.ghe.com,raw.githubusercontent.com,registry.npmjs.org \
370376
--enable-api-proxy \
371377
"npx @github/copilot@latest \
372378
--disable-builtin-mcps \

src/cli.test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2232,23 +2232,29 @@ describe('cli', () => {
22322232
expect(domains).toEqual([]);
22332233
});
22342234

2235-
it('should extract GHEC tenant domain and API subdomain from GITHUB_SERVER_URL', () => {
2235+
it('should extract GHEC tenant, API, Copilot API, and telemetry subdomains from GITHUB_SERVER_URL', () => {
22362236
const domains = extractGhecDomainsFromServerUrl({ GITHUB_SERVER_URL: 'https://myorg.ghe.com' });
22372237
expect(domains).toContain('myorg.ghe.com');
22382238
expect(domains).toContain('api.myorg.ghe.com');
2239-
expect(domains).toHaveLength(2);
2239+
expect(domains).toContain('copilot-api.myorg.ghe.com');
2240+
expect(domains).toContain('copilot-telemetry-service.myorg.ghe.com');
2241+
expect(domains).toHaveLength(4);
22402242
});
22412243

22422244
it('should handle GITHUB_SERVER_URL with trailing slash', () => {
22432245
const domains = extractGhecDomainsFromServerUrl({ GITHUB_SERVER_URL: 'https://myorg.ghe.com/' });
22442246
expect(domains).toContain('myorg.ghe.com');
22452247
expect(domains).toContain('api.myorg.ghe.com');
2248+
expect(domains).toContain('copilot-api.myorg.ghe.com');
2249+
expect(domains).toContain('copilot-telemetry-service.myorg.ghe.com');
22462250
});
22472251

22482252
it('should handle GITHUB_SERVER_URL with path components', () => {
22492253
const domains = extractGhecDomainsFromServerUrl({ GITHUB_SERVER_URL: 'https://acme.ghe.com/some/path' });
22502254
expect(domains).toContain('acme.ghe.com');
22512255
expect(domains).toContain('api.acme.ghe.com');
2256+
expect(domains).toContain('copilot-api.acme.ghe.com');
2257+
expect(domains).toContain('copilot-telemetry-service.acme.ghe.com');
22522258
});
22532259

22542260
it('should extract from GITHUB_API_URL for GHEC', () => {
@@ -2296,6 +2302,8 @@ describe('cli', () => {
22962302
resolveApiTargetsToAllowedDomains({}, domains, env);
22972303
expect(domains).toContain('myorg.ghe.com');
22982304
expect(domains).toContain('api.myorg.ghe.com');
2305+
expect(domains).toContain('copilot-api.myorg.ghe.com');
2306+
expect(domains).toContain('copilot-telemetry-service.myorg.ghe.com');
22992307
});
23002308

23012309
it('should not duplicate GHEC domains if already in allowlist', () => {

src/cli.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -380,11 +380,13 @@ export function emitApiProxyTargetWarnings(
380380

381381
/**
382382
* Extracts GHEC domains from GITHUB_SERVER_URL and GITHUB_API_URL environment variables.
383-
* When GITHUB_SERVER_URL points to a GHEC tenant (*.ghe.com), returns the tenant hostname
384-
* and its API subdomain so they can be auto-added to the firewall allowlist.
383+
* When GITHUB_SERVER_URL points to a GHEC tenant (*.ghe.com), returns the tenant hostname,
384+
* its API subdomain, the Copilot API subdomain, and the Copilot telemetry subdomain so they
385+
* can be auto-added to the firewall allowlist.
385386
*
386387
* @param env - Environment variables (defaults to process.env)
387-
* @returns Array of domains to auto-add to allowlist, or empty array if not GHEC
388+
* @returns Array of GHEC-related domains (tenant, api.*, copilot-api.*, copilot-telemetry-service.*)
389+
* to auto-add to the allowlist, or an empty array if not GHEC
388390
*/
389391
export function extractGhecDomainsFromServerUrl(
390392
env: Record<string, string | undefined> = process.env
@@ -397,10 +399,14 @@ export function extractGhecDomainsFromServerUrl(
397399
try {
398400
const hostname = new URL(serverUrl).hostname;
399401
if (hostname !== 'github.com' && hostname.endsWith('.ghe.com')) {
400-
// GHEC tenant: add the tenant domain and its API subdomain
402+
// GHEC tenant with data residency: add the tenant domain, API subdomain,
403+
// Copilot inference subdomain, and Copilot telemetry subdomain.
401404
// e.g., company.ghe.com → company.ghe.com + api.company.ghe.com
405+
// + copilot-api.company.ghe.com + copilot-telemetry-service.company.ghe.com
402406
domains.push(hostname);
403407
domains.push(`api.${hostname}`);
408+
domains.push(`copilot-api.${hostname}`);
409+
domains.push(`copilot-telemetry-service.${hostname}`);
404410
}
405411
} catch {
406412
// Invalid URL — skip

tests/fixtures/awf-runner.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,19 @@ export class AwfRunner {
195195
throw error;
196196
}
197197

198+
// With reject: false, execa returns instead of throwing on timeout.
199+
// Detect this case and return a proper timeout result.
200+
if (result.timedOut) {
201+
return {
202+
exitCode: -1,
203+
stdout: result.stdout || '',
204+
stderr: result.stderr || '',
205+
success: false,
206+
timedOut: true,
207+
workDir: this.extractWorkDir(result.stderr || ''),
208+
};
209+
}
210+
198211
// Extract work directory from stderr logs
199212
const workDir = this.extractWorkDir(result.stderr || '');
200213

@@ -389,6 +402,19 @@ export class AwfRunner {
389402
throw error;
390403
}
391404

405+
// With reject: false, execa returns instead of throwing on timeout.
406+
// Detect this case and return a proper timeout result.
407+
if (result.timedOut) {
408+
return {
409+
exitCode: -1,
410+
stdout: result.stdout || '',
411+
stderr: result.stderr || '',
412+
success: false,
413+
timedOut: true,
414+
workDir: this.extractWorkDir(result.stderr || ''),
415+
};
416+
}
417+
392418
const workDir = this.extractWorkDir(result.stderr || '');
393419

394420
// Normalize exit code to handle undefined (defaults to 0)

tests/integration/no-docker.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ describe('Docker-in-Docker removal (PR #205)', () => {
5050
{
5151
allowDomains: ['github.com'],
5252
logLevel: 'debug',
53-
timeout: 30000,
53+
timeout: 120000,
5454
buildLocal: true,
5555
}
5656
);
@@ -63,7 +63,7 @@ describe('Docker-in-Docker removal (PR #205)', () => {
6363
const result = await runner.runWithSudo('docker run alpine echo hello', {
6464
allowDomains: ['github.com'],
6565
logLevel: 'debug',
66-
timeout: 30000,
66+
timeout: 120000,
6767
buildLocal: true,
6868
});
6969

@@ -78,7 +78,7 @@ describe('Docker-in-Docker removal (PR #205)', () => {
7878
const result = await runner.runWithSudo('which docker-compose', {
7979
allowDomains: ['github.com'],
8080
logLevel: 'debug',
81-
timeout: 30000,
81+
timeout: 120000,
8282
buildLocal: true,
8383
});
8484

@@ -93,7 +93,7 @@ describe('Docker-in-Docker removal (PR #205)', () => {
9393
{
9494
allowDomains: ['github.com'],
9595
logLevel: 'debug',
96-
timeout: 30000,
96+
timeout: 120000,
9797
buildLocal: true,
9898
}
9999
);

0 commit comments

Comments
 (0)