Skip to content
Merged
236 changes: 236 additions & 0 deletions docs/enterprise-configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
# Custom API Endpoint Configuration

This guide explains how to configure GitHub Agentic Workflows to use custom API endpoints for GitHub Enterprise Cloud (GHEC), GitHub Enterprise Server (GHES), or custom AI endpoints.

## Overview

GitHub Agentic Workflows supports custom API endpoints through the `engine.api-target` configuration field. This allows you to specify custom endpoints for:

- **GitHub Enterprise Cloud (GHEC)** - Tenant-specific Copilot API endpoints
- **GitHub Enterprise Server (GHES)** - Enterprise Copilot API endpoints
- **Custom AI Endpoints** - Custom OpenAI-compatible or Anthropic-compatible endpoints

## Configuration

To configure a custom API endpoint, add the `api-target` field to your engine configuration:

**Basic Configuration:**

```yaml
---
engine:
id: copilot
api-target: api.acme.ghe.com
network:
allowed:
- defaults
- acme.ghe.com
- api.acme.ghe.com
---
```

The `api-target` field accepts a hostname (without protocol or path) and works with any agentic engine.

## Examples

### GitHub Enterprise Cloud (GHEC)

For GHEC tenants (domains ending with `.ghe.com`), specify your tenant-specific API endpoint:

**Workflow Configuration:**

```yaml
---
engine:
id: copilot
api-target: api.acme.ghe.com
network:
allowed:
- defaults
- acme.ghe.com
- api.acme.ghe.com
---
```

**Required domains in network allowlist:**
- `acme.ghe.com` - Your GHEC tenant domain (git operations, web UI)
- `api.acme.ghe.com` - Your tenant-specific Copilot API endpoint
- `raw.githubusercontent.com` - Raw content access (if using GitHub MCP server)

### GitHub Enterprise Server (GHES)

For GHES instances (custom domains), specify the enterprise Copilot endpoint:

**Workflow Configuration:**

```yaml
---
engine:
id: copilot
api-target: api.enterprise.githubcopilot.com
network:
allowed:
- defaults
- github.company.com
- api.enterprise.githubcopilot.com
---
```

**Required domains in network allowlist:**
- `github.company.com` - Your GHES instance (git operations, web UI)
- `api.enterprise.githubcopilot.com` - Enterprise Copilot API endpoint (used for all GHES instances)

### Custom AI Endpoints

The `api-target` field works with any agentic engine, allowing you to use custom AI endpoints:

**Workflow Configuration:**

```yaml
---
engine:
id: codex
api-target: api.custom.ai-provider.com
network:
allowed:
- defaults
- api.custom.ai-provider.com
---
```

## Complete Examples

### GHEC with GitHub MCP Server

```yaml
---
description: Workflow for GHEC environment with GitHub API access
on:
workflow_dispatch:
permissions:
contents: read
issues: write
pull-requests: write
engine:
id: copilot
api-target: api.acme.ghe.com
tools:
github:
mode: remote
toolsets: [default]
network:
allowed:
- defaults
- acme.ghe.com
- api.acme.ghe.com
- raw.githubusercontent.com
---

# Your workflow prompt here
```

### GHES with Custom Endpoint

```yaml
---
description: Workflow for GHES environment
on:
issue_comment:
types: [created]
permissions:
contents: read
issues: write
engine:
id: copilot
api-target: api.enterprise.githubcopilot.com
network:
allowed:
- defaults
- github.company.com
- api.enterprise.githubcopilot.com
---

# Your workflow prompt here
```

### Custom AI Provider

```yaml
---
description: Workflow with custom AI endpoint
on:
workflow_dispatch:
permissions:
contents: read
engine:
id: codex
api-target: api.custom.ai-provider.com
network:
allowed:
- defaults
- api.custom.ai-provider.com
---

# Your workflow prompt here
```

## Verification

To verify your configuration is working correctly:

### 1. Check Compiled Workflow

After compiling your workflow, check the generated `.lock.yml` file:

```bash
gh aw compile your-workflow.md
```

Look for:
- `--copilot-api-target` flag in AWF command (if using Copilot engine)
- Correct API endpoint hostname in the flag value

### 2. Check Workflow Runs

In GitHub Actions workflow runs:
1. Go to the agent job
2. Check the "Run Copilot Agent" (or equivalent) step
3. Verify the AWF command includes the correct API target
4. Check AWF logs for API connection messages

## Troubleshooting

### Wrong API Endpoint

**Problem:** Traffic is going to the wrong API endpoint

**Solutions:**
1. Verify `engine.api-target` is set correctly in your workflow frontmatter
2. Check that the domain is in your `network.allowed` list
3. Review AWF logs in the workflow run for endpoint configuration messages
4. Ensure you're not using a full URL (use hostname only: `api.acme.ghe.com` not `https://api.acme.ghe.com`)

### Domain Not Whitelisted

**Problem:** Requests are blocked with network errors

**Solution:** Add the missing domain to your `network.allowed` list:
- For GHEC: `[acme.ghe.com, api.acme.ghe.com]`
- For GHES: `[github.company.com, api.enterprise.githubcopilot.com]`
- For custom AI: `[api.custom.ai-provider.com]`

### GitHub MCP Server Issues

**Problem:** GitHub MCP server fails to connect to your enterprise instance

**Solutions:**
1. Ensure your GHEC/GHES domain is in `network.allowed`
2. Verify the GitHub token has appropriate scopes for your enterprise tenant
3. Use `mode: remote` for the GitHub MCP server when on GHEC/GHES

## Related Documentation

- [AWF Firewall Configuration](https://github.com/github/gh-aw-firewall) - Detailed AWF documentation
- [GitHub Actions Environment Variables](https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables) - Default GitHub Actions variables
- [Network Permissions](network.md) - Network access configuration
- [Tools Configuration](tools.md) - MCP server and tool setup
5 changes: 5 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -7971,6 +7971,11 @@
"type": "string",
"description": "Agent identifier to pass to copilot --agent flag (copilot engine only). Specifies which custom agent to use for the workflow."
},
"api-target": {
"type": "string",
"description": "Custom API endpoint hostname for the agentic engine. Used for GitHub Enterprise Cloud (GHEC), GitHub Enterprise Server (GHES), or custom AI endpoints. Example: 'api.acme.ghe.com' for GHEC, 'api.enterprise.githubcopilot.com' for GHES, or custom endpoint hostnames.",
"examples": ["api.acme.ghe.com", "api.enterprise.githubcopilot.com", "api.custom.endpoint.com"]
},
"args": {
"type": "array",
"items": {
Expand Down
7 changes: 7 additions & 0 deletions pkg/workflow/awf_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,13 @@ func BuildAWFArgs(config AWFCommandConfig) []string {
awfHelpersLog.Printf("Added --anthropic-api-target=%s", anthropicTarget)
}

// Add Copilot API target for custom Copilot endpoints (GHEC, GHES, or custom)
// This uses the engine.api-target field if configured
if config.WorkflowData.EngineConfig != nil && config.WorkflowData.EngineConfig.APITarget != "" {
awfArgs = append(awfArgs, "--copilot-api-target", config.WorkflowData.EngineConfig.APITarget)
awfHelpersLog.Printf("Added --copilot-api-target=%s", config.WorkflowData.EngineConfig.APITarget)
}

// Add SSL Bump support for HTTPS content inspection (v0.9.0+)
sslBumpArgs := getSSLBumpArgs(firewallConfig)
awfArgs = append(awfArgs, sslBumpArgs...)
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/copilot_engine_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ COPILOT_CLI_INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"

// Always add GH_AW_PROMPT for agentic workflows
env["GH_AW_PROMPT"] = "/tmp/gh-aw/aw-prompts/prompt.txt"

// Tag the step as a GitHub AW agentic execution for discoverability by agents
env["GITHUB_AW"] = "true"
// Indicate the phase: "agent" for the main run, "detection" for threat detection
Expand Down
9 changes: 9 additions & 0 deletions pkg/workflow/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type EngineConfig struct {
Args []string
Firewall *FirewallConfig // AWF firewall configuration
Agent string // Agent identifier for copilot --agent flag (copilot engine only)
APITarget string // Custom API endpoint hostname (e.g., "api.acme.ghe.com" or "api.enterprise.githubcopilot.com")

// Inline definition fields (populated when engine.runtime is specified in frontmatter)
IsInlineDefinition bool // true when the engine is defined inline via engine.runtime + optional engine.provider
Expand Down Expand Up @@ -314,6 +315,14 @@ func (c *Compiler) ExtractEngineConfig(frontmatter map[string]any) (string, *Eng
}
}

// Extract optional 'api-target' field (custom API endpoint for any engine)
if apiTarget, hasAPITarget := engineObj["api-target"]; hasAPITarget {
if apiTargetStr, ok := apiTarget.(string); ok && apiTargetStr != "" {
config.APITarget = apiTargetStr
engineLog.Printf("Extracted api-target: %s", apiTargetStr)
}
}

// Return the ID as the engineSetting for backwards compatibility
engineLog.Printf("Extracted engine configuration: ID=%s", config.ID)
return config.ID, config
Expand Down
76 changes: 76 additions & 0 deletions pkg/workflow/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,79 @@ func TestEngineCommandField(t *testing.T) {
})
}
}

// TestAPITargetExtraction tests that the api-target configuration is correctly
// extracted from frontmatter for custom API endpoints (GHEC, GHES, or custom AI endpoints).
func TestAPITargetExtraction(t *testing.T) {
tests := []struct {
name string
frontmatter map[string]any
expectedAPITarget string
}{
{
name: "GHEC api-target",
frontmatter: map[string]any{
"engine": map[string]any{
"id": "copilot",
"api-target": "api.acme.ghe.com",
},
},
expectedAPITarget: "api.acme.ghe.com",
},
{
name: "GHES api-target",
frontmatter: map[string]any{
"engine": map[string]any{
"id": "copilot",
"api-target": "api.enterprise.githubcopilot.com",
},
},
expectedAPITarget: "api.enterprise.githubcopilot.com",
},
{
name: "custom api-target",
frontmatter: map[string]any{
"engine": map[string]any{
"id": "codex",
"api-target": "api.custom.endpoint.com",
},
},
expectedAPITarget: "api.custom.endpoint.com",
},
{
name: "no api-target",
frontmatter: map[string]any{
"engine": map[string]any{
"id": "copilot",
},
},
expectedAPITarget: "",
},
{
name: "empty api-target",
frontmatter: map[string]any{
"engine": map[string]any{
"id": "copilot",
"api-target": "",
},
},
expectedAPITarget: "",
},
}

compiler := NewCompiler()

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, config := compiler.ExtractEngineConfig(tt.frontmatter)

if config == nil {
t.Fatal("Expected config to be non-nil")
}

if config.APITarget != tt.expectedAPITarget {
t.Errorf("Expected api-target %q, got %q", tt.expectedAPITarget, config.APITarget)
}
})
}
}
3 changes: 0 additions & 3 deletions pkg/workflow/secrets_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ var secretsValidationLog = newValidationLogger("secrets")
// This is the same pattern used in the github_token schema definition ($defs/github_token).
var secretsExpressionPattern = regexp.MustCompile(`^\$\{\{\s*secrets\.[A-Za-z_][A-Za-z0-9_]*(\s*\|\|\s*secrets\.[A-Za-z_][A-Za-z0-9_]*)*\s*\}\}$`)

// secretNamePattern validates that a secret name follows environment variable naming conventions
var secretNamePattern = regexp.MustCompile(`^[A-Z][A-Z0-9_]*$`)

// validateSecretsExpression validates that a value is a proper GitHub Actions secrets expression.
// Returns an error if the value is not in the format: ${{ secrets.NAME }} or ${{ secrets.NAME || secrets.NAME2 }}
// Note: This function intentionally does not accept the secret key name as a parameter to prevent
Expand Down
Loading