Skip to content
Merged
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
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,8 +389,8 @@ DEBUG_COLORS=0 DEBUG=* ./awmg --config config.toml
- `GITHUB_TOKEN` - Second-priority GitHub auth token fallback after `GITHUB_MCP_SERVER_TOKEN`
- `GITHUB_PERSONAL_ACCESS_TOKEN` - Third-priority GitHub auth fallback
- `GH_TOKEN` - Lowest-priority GitHub auth fallback (set by GitHub CLI)
- `GITHUB_API_URL` - Explicit GitHub API endpoint (e.g., `https://copilot-api.mycompany.ghe.com`); used by proxy to set upstream target
- `GITHUB_SERVER_URL` - GitHub server URL; proxy auto-derives API endpoint: `*.ghe.com` → `copilot-api.*.ghe.com`, GHES → `<host>/api/v3`, `github.com` → `api.github.com`
- `GITHUB_API_URL` - Explicit GitHub API endpoint (e.g., `https://api.mycompany.ghe.com`); used by proxy to set upstream target
- `GITHUB_SERVER_URL` - GitHub server URL; proxy auto-derives API endpoint: `*.ghe.com` → `api.*.ghe.com`, GHES → `<host>/api/v3`, `github.com` → `api.github.com`
- `ACTIONS_ID_TOKEN_REQUEST_URL` - GitHub Actions OIDC token endpoint URL; required for `github-oidc` auth type
- `ACTIONS_ID_TOKEN_REQUEST_TOKEN` - GitHub Actions OIDC request token; required for `github-oidc` auth type
- `MCP_GATEWAY_PORT` - Used by environment validation (`--validate-env`) for container port-mapping checks (validated 1-65535); does not override the gateway listen address
Expand Down
2 changes: 1 addition & 1 deletion docs/AWF_PIPELINE_ENVIRONMENT_VARIABLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ The `deriveCopilotApiTarget()` function in the API proxy sidecar (`server.js`) r
| `https://company.ghe.com` | GHEC | `copilot-api.company.ghe.com` |
| `https://github.company.com` | GHES | `api.enterprise.githubcopilot.com` |

> **Important**: GHEC uses `copilot-api.<slug>.ghe.com`, **not** `api.<slug>.ghe.com` (the REST API endpoint).
> **Important**: The Copilot inference sidecar uses `copilot-api.<slug>.ghe.com`, **not** `api.<slug>.ghe.com`. This is distinct from the **mcpg DIFC proxy**, which forwards REST/GraphQL `gh api` calls and therefore targets the REST API host `api.<slug>.ghe.com` (derived from `GITHUB_SERVER_URL`). Do not point the DIFC proxy at `copilot-api.*` — that endpoint does not serve the REST API (e.g. `/rate_limit`).

---

Expand Down
4 changes: 2 additions & 2 deletions docs/ENVIRONMENT_VARIABLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ When running `awmg proxy`, these variables configure the upstream GitHub API:

| Variable | Description | Default |
|----------|-------------|---------|
| `GITHUB_API_URL` | Explicit GitHub API endpoint (e.g., `https://copilot-api.mycompany.ghe.com`); used by proxy to set upstream target | (auto-derived) |
| `GITHUB_SERVER_URL` | GitHub server URL; proxy auto-derives API endpoint: `*.ghe.com` → `copilot-api.*.ghe.com`, GHES → `<host>/api/v3`, `github.com` → `api.github.com` | (falls back to `api.github.com`) |
| `GITHUB_API_URL` | Explicit GitHub API endpoint (e.g., `https://api.mycompany.ghe.com`); used by proxy to set upstream target | (auto-derived) |
| `GITHUB_SERVER_URL` | GitHub server URL; proxy auto-derives API endpoint: `*.ghe.com` → `api.*.ghe.com`, GHES → `<host>/api/v3`, `github.com` → `api.github.com` | (falls back to `api.github.com`) |

## GitHub Actions OIDC Variables

Expand Down
19 changes: 14 additions & 5 deletions internal/envutil/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,13 @@ func DeriveGitHubAPIURL(defaultURL string) string {
}

// deriveAPIFromServerURL converts a GITHUB_SERVER_URL to the corresponding API endpoint.
// GHEC tenants (*.ghe.com): https://tenant.ghe.com → https://copilot-api.tenant.ghe.com
// GHEC data-residency 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
//
// Note: GHEC data residency exposes its REST/GraphQL API at api.<tenant>.ghe.com.
// This is distinct from copilot-api.<tenant>.ghe.com, which is the Copilot inference
// endpoint and does not serve the REST API (e.g. /rate_limit) that the DIFC proxy forwards.
func deriveAPIFromServerURL(serverURL string) string {
logGitHub.Printf("Deriving API URL from server URL: %s", sanitize.RedactURL(serverURL))

Expand All @@ -100,13 +104,18 @@ func deriveAPIFromServerURL(serverURL string) string {
logGitHub.Printf("GitHub.com detected, using default API URL: %s", DefaultGitHubAPIBaseURL)
return DefaultGitHubAPIBaseURL
case strings.HasSuffix(hostname, ".ghe.com"):
// If the hostname already starts with "api." (e.g. GITHUB_SERVER_URL was
// set to the API base directly), return it as-is to avoid producing
// "api.api.<tenant>.ghe.com".
var apiURL string
if port := parsed.Port(); port != "" {
apiURL = fmt.Sprintf("%s://copilot-api.%s:%s", parsed.Scheme, hostname, port)
if strings.HasPrefix(hostname, "api.") {
apiURL = fmt.Sprintf("%s://%s", parsed.Scheme, parsed.Host)
} else if port := parsed.Port(); port != "" {
apiURL = fmt.Sprintf("%s://api.%s:%s", parsed.Scheme, hostname, port)
} else {
apiURL = fmt.Sprintf("%s://copilot-api.%s", parsed.Scheme, hostname)
apiURL = fmt.Sprintf("%s://api.%s", parsed.Scheme, hostname)
Comment on lines 106 to +116
}
logGitHub.Printf("GHEC tenant detected, using copilot-api subdomain: hostname=%s, apiURL=%s", hostname, apiURL)
logGitHub.Printf("GHEC data-residency tenant detected, using api subdomain: hostname=%s, apiURL=%s", hostname, apiURL)
return apiURL
default:
apiURL := fmt.Sprintf("%s://%s/api/v3", parsed.Scheme, parsed.Host)
Expand Down
8 changes: 4 additions & 4 deletions internal/envutil/github_coverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@ func TestDeriveAPIFromServerURL_HTTPSchemeAndEdgeCases(t *testing.T) {
{
// http scheme is explicitly allowed; schemes other than "http" and
// "https" are rejected.
// GHEC tenant: prepend "copilot-api." subdomain.
name: "http scheme GHEC tenant derives copilot-api subdomain",
// GHEC data-residency tenant: prepend "api." subdomain.
name: "http scheme GHEC tenant derives api subdomain",
serverURL: "http://mycompany.ghe.com",
expected: "http://copilot-api.mycompany.ghe.com",
expected: "http://api.mycompany.ghe.com",
},
{
// GHEC tenant with both http scheme and a port number.
name: "http scheme GHEC tenant with port",
serverURL: "http://mycompany.ghe.com:8080",
expected: "http://copilot-api.mycompany.ghe.com:8080",
expected: "http://api.mycompany.ghe.com:8080",
},
{
// http scheme is allowed for GHES hosts as well.
Expand Down
18 changes: 14 additions & 4 deletions internal/envutil/github_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ func TestDeriveAPIFromServerURL(t *testing.T) {
expected: DefaultGitHubAPIBaseURL,
},
{
name: "GHEC tenant derives copilot-api subdomain",
name: "GHEC data-residency tenant derives api subdomain",
serverURL: "https://mycompany.ghe.com",
expected: "https://copilot-api.mycompany.ghe.com",
expected: "https://api.mycompany.ghe.com",
},
{
name: "GHES uses /api/v3 path",
Expand All @@ -125,7 +125,17 @@ func TestDeriveAPIFromServerURL(t *testing.T) {
{
name: "GHEC tenant with port",
serverURL: "https://mycompany.ghe.com:8443",
expected: "https://copilot-api.mycompany.ghe.com:8443",
expected: "https://api.mycompany.ghe.com:8443",
},
{
name: "GHEC already-API hostname is returned as-is",
serverURL: "https://api.mycompany.ghe.com",
expected: "https://api.mycompany.ghe.com",
},
{
name: "GHEC already-API hostname with port is returned as-is",
serverURL: "https://api.mycompany.ghe.com:8443",
expected: "https://api.mycompany.ghe.com:8443",
},
{
name: "GHES with port",
Expand Down Expand Up @@ -190,7 +200,7 @@ func TestDeriveGitHubAPIURL(t *testing.T) {
envAPIURL: "",
envSrvURL: "https://mycompany.ghe.com",
defaultURL: DefaultGitHubAPIBaseURL,
expected: "https://copilot-api.mycompany.ghe.com",
expected: "https://api.mycompany.ghe.com",
},
{
name: "invalid GITHUB_SERVER_URL falls back to default",
Expand Down
17 changes: 9 additions & 8 deletions internal/proxy/forward_to_github_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,21 +189,22 @@ func TestForwardToGitHub_GraphQLPathRouting(t *testing.T) {
wantServerPath: "/api/graphql?ref=main&query=foo",
},
{
// GHE Cloud data residency (e.g. copilot-api.sj.ghe.com) has no /api/v3
// suffix but the gh CLI sends GraphQL requests to /api/graphql.
// forwardToGitHub must forward to base/api/graphql, not base/graphql.
name: "GHE Cloud data residency api/graphql path routes to base/api/graphql",
githubAPIURL: "http://copilot-api.sj.ghe.com",
// GHE Cloud data residency exposes its REST/GraphQL API at
// api.<tenant>.ghe.com (not copilot-api), so GraphQL lives at /graphql
// just like github.com. The gh CLI sends GraphQL requests to /api/graphql
// (rewritten GITHUB_GRAPHQL_URL); forwardToGitHub must normalise to base/graphql.
name: "GHE Cloud data residency api/graphql path routes to base/graphql",
githubAPIURL: "http://api.sj.ghe.com",
routeToUpstreamHost: true,
requestPath: "/api/graphql",
wantServerPath: "/api/graphql",
wantServerPath: "/graphql",
},
{
name: "GHE Cloud data residency api/graphql with query string preserves query",
githubAPIURL: "http://copilot-api.sj.ghe.com",
githubAPIURL: "http://api.sj.ghe.com",
routeToUpstreamHost: true,
requestPath: "/api/graphql?foo=bar",
wantServerPath: "/api/graphql?foo=bar",
wantServerPath: "/graphql?foo=bar",
},
{
name: "dotcom-style api graphql path normalises to graphql",
Expand Down
13 changes: 2 additions & 11 deletions internal/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,29 +342,20 @@ func (s *Server) upstreamHost() string {
return host
}

func (s *Server) isGHECDataResidencyHost() bool {
host := strings.ToLower(s.upstreamHost())
return strings.HasPrefix(host, "copilot-api.") && strings.HasSuffix(host, ".ghe.com")
}

// forwardToGitHub sends a request to the upstream GitHub API.
// clientAuth is the Authorization header from the inbound client request;
// if non-empty it is forwarded as-is, otherwise the configured fallback token is used.
func (s *Server) forwardToGitHub(ctx context.Context, method, path string, body io.Reader, contentType string, clientAuth string) (*http.Response, error) {
url := s.githubAPIURL + path
pathOnly, query, hasQuery := strings.Cut(path, "?")
if IsGraphQLPath(pathOnly) {
normalizedPath := strings.TrimSuffix(pathOnly, "/")
var graphqlURL string
if strings.HasSuffix(s.githubAPIURL, "/api/v3") {
// GHES: strip /api/v3, GraphQL lives at /api/graphql
graphqlURL = strings.TrimSuffix(s.githubAPIURL, "/api/v3") + "/api/graphql"
} else if s.isGHECDataResidencyHost() && strings.HasSuffix(normalizedPath, "/api/graphql") {
// GHE Cloud data residency (e.g. copilot-api.sj.ghe.com):
// the client already sent /api/graphql — use the same path on upstream
graphqlURL = s.githubAPIURL + "/api/graphql"
} else {
// github.com: GraphQL lives at /graphql
// github.com and GHEC data residency (api.<tenant>.ghe.com):
// GraphQL lives at /graphql
graphqlURL = s.githubAPIURL + "/graphql"
}
url = graphqlURL
Expand Down
Loading