Skip to content

feat: GHEC tenant support for proxy and guard URL parsing#2481

Merged
lpcox merged 6 commits intomainfrom
fix/ghec-tenant-support
Mar 24, 2026
Merged

feat: GHEC tenant support for proxy and guard URL parsing#2481
lpcox merged 6 commits intomainfrom
fix/ghec-tenant-support

Conversation

@lpcox
Copy link
Copy Markdown
Collaborator

@lpcox lpcox commented Mar 24, 2026

Summary

Adds auto-derivation of GitHub API URL from environment variables for GHEC/GHES tenants, and fixes the Rust guard URL parser to handle non-github.com URLs.

Changes

Proxy: Environment-based API URL resolution

The proxy --github-api-url flag now auto-resolves from environment when not explicitly set:

  1. GITHUB_API_URL — explicit API endpoint
  2. GITHUB_SERVER_URL — auto-derived:
    • *.ghe.comcopilot-api.*.ghe.com (GHEC)
    • other hosts → <host>/api/v3 (GHES)
    • github.comapi.github.com
  3. Default: https://api.github.com

This matches the derivation pattern used by GitHub Agentic Workflows' deriveCopilotApiTarget().

Rust Guard: Generic URL parsing

extract_repo_from_github_url() now handles GHEC/GHES URLs by looking for /repos/<owner>/<repo> generically in the URL path, instead of only matching api.github.com prefixes.

Tests

  • 10 Go unit tests for DeriveGitHubAPIURL() and deriveAPIFromServerURL()
  • 3 Rust unit tests for GHEC/GHES/standard URL extraction

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Add auto-derivation of GitHub API URL from GITHUB_API_URL and
GITHUB_SERVER_URL environment variables, matching the pattern used
by GitHub Agentic Workflows.

Priority order:
1. --github-api-url flag (explicit)
2. GITHUB_API_URL env var
3. GITHUB_SERVER_URL auto-derivation:
   - *.ghe.com → api.*.ghe.com (GHEC)
   - other hosts → <host>/api/v3 (GHES)
   - github.com → api.github.com
4. Default: https://api.github.com

Also fix Rust guard extract_repo_from_github_url() to handle
GHEC/GHES URLs by looking for /repos/<owner>/<repo> pattern
generically instead of only matching api.github.com prefixes.

Closes #2480

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 24, 2026 22:57
@lpcox
Copy link
Copy Markdown
Collaborator Author

lpcox commented Mar 24, 2026

@copilot api..ghe.com is incorrect. it should be copilot-api..ghe.com

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds tenant-aware GitHub API URL derivation for the proxy (GHEC/GHES) and expands the Rust guard’s repo URL parsing to work with non-github.com hosts, aligning behavior with environment variables commonly present in GitHub Actions.

Changes:

  • Proxy: derive upstream GitHub API base from GITHUB_API_URL / GITHUB_SERVER_URL when --github-api-url is unset.
  • Proxy CLI: update --github-api-url flag defaulting behavior to enable env-based auto-derivation with fallback to https://api.github.com.
  • Rust guard: make repo extraction work generically by locating /repos/<owner>/<repo> in the URL path and add tests; update docs to describe env vars.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
internal/proxy/proxy.go Adds env-based API URL derivation helpers for GHEC/GHES/github.com.
internal/cmd/proxy.go Switches --github-api-url to default empty and resolves flag→env→default at runtime.
internal/proxy/proxy_test.go Adds Go unit tests for API URL derivation functions.
guards/github-guard/rust-guard/src/labels/helpers.rs Updates repo extraction to support non-api.github.com URL shapes.
guards/github-guard/rust-guard/src/labels/mod.rs Adds Rust unit tests covering GHEC/GHES/standard URL extraction.
AGENTS.md Documents GITHUB_API_URL / GITHUB_SERVER_URL behavior for the proxy.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +59 to +80
// deriveAPIFromServerURL converts a GITHUB_SERVER_URL to the corresponding API endpoint.
// GHEC tenants (*.ghe.com): https://tenant.ghe.com → https://api.tenant.ghe.com
// GitHub.com: https://github.com → https://api.github.com
// GHES (all others): https://github.example.com → https://github.example.com/api/v3
func deriveAPIFromServerURL(serverURL string) string {
parsed, err := url.Parse(strings.TrimRight(serverURL, "/"))
if err != nil || parsed.Host == "" {
return ""
}

host := strings.ToLower(parsed.Host)

switch {
case host == "github.com" || host == "www.github.com":
return DefaultGitHubAPIBase
case strings.HasSuffix(host, ".ghe.com"):
// GHEC tenant: api.<subdomain>.ghe.com
return fmt.Sprintf("%s://api.%s", parsed.Scheme, parsed.Host)
default:
// GHES: <host>/api/v3
return fmt.Sprintf("%s://%s/api/v3", parsed.Scheme, parsed.Host)
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the GHES case this derives the upstream base as /api/v3. Elsewhere the proxy forwards GraphQL requests to a hard-coded "/graphql" path; with an /api/v3 base this becomes /api/v3/graphql, but GHES GraphQL is typically served under /api/graphql (and the proxy already recognizes "/api/graphql" as a GraphQL path). To avoid GHES GraphQL requests breaking when using the auto-derived URL, consider either (a) keeping the base at the host root and adding /api/v3 only for REST requests, or (b) making the forwarded GraphQL path configurable/derived separately (e.g., /graphql for github.com/GHEC vs /api/graphql for GHES).

Copilot uses AI. Check for mistakes.
Comment on lines +69 to +79
host := strings.ToLower(parsed.Host)

switch {
case host == "github.com" || host == "www.github.com":
return DefaultGitHubAPIBase
case strings.HasSuffix(host, ".ghe.com"):
// GHEC tenant: api.<subdomain>.ghe.com
return fmt.Sprintf("%s://api.%s", parsed.Scheme, parsed.Host)
default:
// GHES: <host>/api/v3
return fmt.Sprintf("%s://%s/api/v3", parsed.Scheme, parsed.Host)
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

url.Parse stores the host including any port (e.g. "mycompany.ghe.com:8443") in parsed.Host. The GHEC detection uses strings.HasSuffix(host, ".ghe.com"), which will fail when a port is present and will incorrectly fall through to the GHES /api/v3 derivation. Consider using parsed.Hostname() for the suffix/equals checks (and then re-adding parsed.Port() when formatting the returned URL).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 24, 2026

@copilot api..ghe.com is incorrect. it should be copilot-api..ghe.com

Fixed in 880c1d9. Changed GHEC tenant URL derivation from api.<subdomain>.ghe.com to copilot-api.<subdomain>.ghe.com in deriveAPIFromServerURL(), and updated the corresponding comments, docs, and tests.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • example.com
    • Triggering command: /tmp/go-build2137962035/b333/launcher.test /tmp/go-build2137962035/b333/launcher.test -test.testlogfile=/tmp/go-build2137962035/b333/testlog.txt -test.paniconexit0 -test.timeout=10m0s ortc�� 9946333/b160/_pkg_.a 64/src/hash/fnv/fnv.go x_amd64/vet (dns block)
  • invalid-host-that-does-not-exist-12345.com
    • Triggering command: /tmp/go-build2137962035/b318/config.test /tmp/go-build2137962035/b318/config.test -test.testlogfile=/tmp/go-build2137962035/b318/testlog.txt -test.paniconexit0 -test.timeout=10m0s --no�� s3GP/wMm-iz4PwKrmbSbus3GP 64/src/runtime/cgo x_amd64/vet (dns block)
  • nonexistent.local
    • Triggering command: /tmp/go-build2137962035/b333/launcher.test /tmp/go-build2137962035/b333/launcher.test -test.testlogfile=/tmp/go-build2137962035/b333/testlog.txt -test.paniconexit0 -test.timeout=10m0s ortc�� 9946333/b160/_pkg_.a 64/src/hash/fnv/fnv.go x_amd64/vet (dns block)
  • slow.example.com
    • Triggering command: /tmp/go-build2137962035/b333/launcher.test /tmp/go-build2137962035/b333/launcher.test -test.testlogfile=/tmp/go-build2137962035/b333/testlog.txt -test.paniconexit0 -test.timeout=10m0s ortc�� 9946333/b160/_pkg_.a 64/src/hash/fnv/fnv.go x_amd64/vet (dns block)
  • this-host-does-not-exist-12345.com
    • Triggering command: /tmp/go-build2137962035/b342/mcp.test /tmp/go-build2137962035/b342/mcp.test -test.testlogfile=/tmp/go-build2137962035/b342/testlog.txt -test.paniconexit0 -test.timeout=10m0s 9946�� azero@v1.11.0/ingo1.25.8 azero@v1.11.0/in-c=4 x_amd64/vet (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@lpcox
Copy link
Copy Markdown
Collaborator Author

lpcox commented Mar 24, 2026

@copilot open a new pull request to apply changes based on the comments in this thread

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 24, 2026

@lpcox I've opened a new pull request, #2484, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI and others added 2 commits March 24, 2026 23:10
`url.Parse` stores host+port together in `parsed.Host` (e.g.
`mycompany.ghe.com:8443`), so `strings.HasSuffix(host, ".ghe.com")`
silently fails when a non-standard port is present, causing GHEC URLs to
fall through to GHES `/api/v3` derivation.

## Changes

- **`deriveAPIFromServerURL`**: switch from `parsed.Host` to
`parsed.Hostname()` for `.ghe.com`/`github.com` checks; re-append port
via `parsed.Port()` when formatting the GHEC `copilot-api.*` URL
- **Tests**: add `GHEC tenant with port` case
(`https://mycompany.ghe.com:8443` →
`https://copilot-api.mycompany.ghe.com:8443`)

```go
// Before — port breaks suffix match, falls through to GHES
host := strings.ToLower(parsed.Host)          // "mycompany.ghe.com:8443"
strings.HasSuffix(host, ".ghe.com")           // false ❌

// After — hostname-only check, port re-added when formatting
hostname := strings.ToLower(parsed.Hostname()) // "mycompany.ghe.com"
strings.HasSuffix(hostname, ".ghe.com")        // true ✅
```

<!-- START COPILOT CODING AGENT TIPS -->
---

📍 Connect Copilot coding agent with [Jira](https://gh.io/cca-jira-docs),
[Azure Boards](https://gh.io/cca-azure-boards-docs) or
[Linear](https://gh.io/cca-linear-docs) to delegate work to Copilot in
one click without leaving your project management tool.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants