Skip to content
Closed
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
1,894 changes: 1,894 additions & 0 deletions .github/workflows/smoke-copilot-no-firewall.lock.yml

Large diffs are not rendered by default.

107 changes: 107 additions & 0 deletions .github/workflows/smoke-copilot-no-firewall.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
description: Smoke Copilot (No Firewall)
on:
schedule: every 12h
workflow_dispatch:
pull_request:
types: [labeled]
names: ["smoke-no-firewall"]
reaction: "eyes"
permissions:
contents: read
pull-requests: read
issues: read
discussions: read
actions: read
name: Smoke Copilot (No Firewall)
engine: copilot
sandbox: false
strict: false
imports:
- shared/gh.md
- shared/reporting.md
- shared/github-queries-safe-input.md
network:
allowed:
- defaults
- node
- github
- playwright
tools:
agentic-workflows:
cache-memory: true
edit:
bash:
- "*"
github:
playwright:
allowed_domains:
- github.com
serena:
languages:
go: {}
web-fetch:
runtimes:
go:
version: "1.25"
safe-outputs:
add-comment:
hide-older-comments: true
max: 2
create-issue:
expires: 2h
group: true
close-older-issues: true
add-labels:
allowed: [smoke-copilot-no-firewall]
remove-labels:
allowed: [smoke-no-firewall]
messages:
append-only-comments: true
footer: "> 📰 *BREAKING: Report filed by [{workflow_name}]({run_url})*"
run-started: "📰 BREAKING: [{workflow_name}]({run_url}) is now investigating this {event_type}. Sources say the story is developing..."
run-success: "📰 VERDICT: [{workflow_name}]({run_url}) has concluded. All systems operational. This is a developing story. 🎤"
run-failure: "📰 DEVELOPING STORY: [{workflow_name}]({run_url}) reports {status}. Our correspondents are investigating the incident..."
timeout-minutes: 15
---

# Smoke Test: Copilot Engine Validation (No Firewall)

**IMPORTANT: Keep all outputs extremely short and concise. Use single-line responses where possible. No verbose explanations.**

## Test Requirements

1. **GitHub MCP Testing**: Review the last 2 merged pull requests in ${{ github.repository }}
2. **Safe Inputs GH CLI Testing**: Use the `safeinputs-gh` tool to query 2 pull requests from ${{ github.repository }} (use args: "pr list --repo ${{ github.repository }} --limit 2 --json number,title,author")
3. **Serena MCP Testing**: Use the Serena MCP server tool `activate_project` to initialize the workspace at `${{ github.workspace }}` and verify it succeeds (do NOT use bash to run go commands - use Serena's MCP tools)
4. **Playwright Testing**: Use playwright to navigate to <https://github.com> and verify the page title contains "GitHub"
5. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-copilot-${{ github.run_id }}.txt` with content "Smoke test passed for Copilot at $(date)" (create the directory if it doesn't exist)
6. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back)
7. **Discussion Interaction Testing**:
- Use the `github-discussion-query` safe-input tool with params: `limit=1, jq=".[0]"` to get the latest discussion from ${{ github.repository }}
- Extract the discussion number from the result (e.g., if the result is `{"number": 123, "title": "...", ...}`, extract 123)
- Use the `add_comment` tool with `discussion_number: <extracted_number>` to add a fun, playful comment stating that the smoke test agent was here
8. **Build gh-aw**: Run `GOCACHE=/tmp/go-cache GOMODCACHE=/tmp/go-mod make build` to verify the agent can successfully build the gh-aw project (both caches must be set to /tmp because the default cache locations are not writable). If the command fails, mark this test as ❌ and report the failure.

## Output

1. **Create an issue** with a summary of the smoke test run:
- Title: "Smoke Test: Copilot (No Firewall) - ${{ github.run_id }}"
- Body should include:
- Test results (✅ or ❌ for each test)
- Overall status: PASS or FAIL
- Run URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
- Timestamp
- Pull request author and assignees

2. Add a **very brief** comment (max 5-10 lines) to the current pull request with:
- PR titles only (no descriptions)
- ✅ or ❌ for each test result
- Overall status: PASS or FAIL
- Mention the pull request author and any assignees

3. Use the `add_comment` tool to add a **fun and creative comment** to the latest discussion (using the `discussion_number` you extracted in step 7) - be playful and entertaining in your comment

If all tests pass:
- Use the `add_labels` safe-output tool to add the label `smoke-copilot-no-firewall` to the pull request
- Use the `remove_labels` safe-output tool to remove the label `smoke-no-firewall` from the pull request
51 changes: 46 additions & 5 deletions pkg/workflow/copilot_mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,52 @@ func (e *CopilotEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]
gatewayConfig := buildMCPGatewayConfig(workflowData)

// Use shared JSON MCP config renderer with unified renderer methods
options := JSONMCPConfigOptions{
ConfigPath: "/home/runner/.copilot/mcp-config.json",
GatewayConfig: gatewayConfig,
options := e.buildCopilotMCPConfigOptions(createRenderer, gatewayConfig, workflowData, false)

RenderJSONMCPConfig(yaml, tools, mcpTools, workflowData, options)
}

// RenderMCPConfigWithoutGateway generates MCP server configuration for Copilot CLI
// without the MCP gateway proxy. This is used when sandbox is disabled and
// MCP servers run directly via Docker (for container-based servers) or HTTP.
// The config uses "command": "docker" format that Copilot CLI can execute directly.
func (e *CopilotEngine) RenderMCPConfigWithoutGateway(yaml *strings.Builder, tools map[string]any, mcpTools []string, workflowData *WorkflowData) {
copilotMCPLog.Printf("Rendering MCP config without gateway for Copilot engine: mcpTools=%d", len(mcpTools))

// Create the directory first
yaml.WriteString(" mkdir -p /home/runner/.copilot\n")

// Create unified renderer with Copilot-specific options
// UseDirectDockerCommand=true: Use "command": "docker" format instead of "container" field
// This allows Copilot CLI to spawn Docker containers directly without MCP Gateway
createRenderer := func(isLast bool) *MCPConfigRendererUnified {
return NewMCPConfigRenderer(MCPRendererOptions{
IncludeCopilotFields: true,
InlineArgs: true,
Format: "json",
IsLast: isLast,
UseDirectDockerCommand: true,
})
}

// Build base options without gateway
options := e.buildCopilotMCPConfigOptions(createRenderer, nil, workflowData, true)

RenderJSONMCPConfig(yaml, tools, mcpTools, workflowData, options)
}

// buildCopilotMCPConfigOptions creates the JSONMCPConfigOptions for Copilot engine
// This shared helper avoids code duplication between RenderMCPConfig and RenderMCPConfigWithoutGateway
func (e *CopilotEngine) buildCopilotMCPConfigOptions(
createRenderer func(isLast bool) *MCPConfigRendererUnified,
gatewayConfig *MCPGatewayRuntimeConfig,
workflowData *WorkflowData,
skipGatewayStartup bool,
) JSONMCPConfigOptions {
return JSONMCPConfigOptions{
ConfigPath: "/home/runner/.copilot/mcp-config.json",
GatewayConfig: gatewayConfig,
SkipGatewayStartup: skipGatewayStartup,
Renderers: MCPToolRenderers{
RenderGitHub: func(yaml *strings.Builder, githubTool any, isLast bool, workflowData *WorkflowData) {
renderer := createRenderer(isLast)
Expand Down Expand Up @@ -75,8 +118,6 @@ func (e *CopilotEngine) RenderMCPConfig(yaml *strings.Builder, tools map[string]
return toolName != "cache-memory"
},
}

RenderJSONMCPConfig(yaml, tools, mcpTools, workflowData, options)
}

// renderCopilotMCPConfigWithContext generates custom MCP server configuration for Copilot CLI
Expand Down
60 changes: 60 additions & 0 deletions pkg/workflow/mcp_config_playwright_renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,63 @@ func renderPlaywrightMCPConfigWithOptions(yaml *strings.Builder, playwrightConfi
yaml.WriteString(" },\n")
}
}

// renderPlaywrightMCPDirectDocker generates Playwright MCP configuration using direct Docker command format.
// This format uses "command": "docker" with all args inline, allowing CLIs to spawn containers directly
// without the MCP Gateway.
func renderPlaywrightMCPDirectDocker(yaml *strings.Builder, playwrightConfig *PlaywrightToolConfig, isLast bool, includeCopilotFields bool) {
mcpPlaywrightLog.Print("Rendering Playwright MCP with direct docker command")

args := generatePlaywrightDockerArgs(playwrightConfig)
customArgs := getPlaywrightCustomArgs(playwrightConfig)

// Use official Playwright MCP Docker image
playwrightImage := "mcr.microsoft.com/playwright/mcp"

yaml.WriteString(" \"playwright\": {\n")

// Add type field for Copilot
if includeCopilotFields {
yaml.WriteString(" \"type\": \"stdio\",\n")
}

// Use direct docker command format
yaml.WriteString(" \"command\": \"docker\",\n")
yaml.WriteString(" \"args\": [\n")
yaml.WriteString(" \"run\",\n")
yaml.WriteString(" \"-i\",\n")
yaml.WriteString(" \"--rm\",\n")
yaml.WriteString(" \"--init\",\n")
yaml.WriteString(" \"--network\", \"host\",\n")
yaml.WriteString(" \"-v\", \"/tmp/gh-aw/mcp-logs:/tmp/gh-aw/mcp-logs:rw\",\n")

// Docker image
yaml.WriteString(" \"" + playwrightImage + "\",\n")

// Entrypoint args for Playwright MCP server
yaml.WriteString(" \"--output-dir\", \"/tmp/gh-aw/mcp-logs/playwright\"")

if len(args.AllowedDomains) > 0 {
domainsStr := strings.Join(args.AllowedDomains, ";")
yaml.WriteString(",\n")
yaml.WriteString(" \"--allowed-hosts\", \"" + domainsStr + "\",\n")
yaml.WriteString(" \"--allowed-origins\", \"" + domainsStr + "\"")
}

// Append custom args if present
if len(customArgs) > 0 {
for _, arg := range customArgs {
yaml.WriteString(",\n")
yaml.WriteString(" \"" + arg + "\"")
}
}

yaml.WriteString("\n")
yaml.WriteString(" ]\n")

if isLast {
yaml.WriteString(" }\n")
} else {
yaml.WriteString(" },\n")
}
}
65 changes: 65 additions & 0 deletions pkg/workflow/mcp_config_serena_renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,68 @@ func renderSerenaMCPConfigWithOptions(yaml *strings.Builder, serenaTool any, isL
yaml.WriteString(" },\n")
}
}

// renderSerenaMCPDirectDocker generates Serena MCP configuration using direct Docker command format.
// This format uses "command": "docker" with all args inline, allowing CLIs to spawn containers directly
// without the MCP Gateway.
func renderSerenaMCPDirectDocker(yaml *strings.Builder, serenaTool any, isLast bool, includeCopilotFields bool) {
mcpSerenaLog.Print("Rendering Serena MCP with direct docker command")

customArgs := getSerenaCustomArgs(serenaTool)

// Determine the mode
mode := "docker" // default
if toolMap, ok := serenaTool.(map[string]any); ok {
if modeStr, ok := toolMap["mode"].(string); ok {
mode = modeStr
}
}

yaml.WriteString(" \"serena\": {\n")

if mode == "local" {
// Local mode: use HTTP transport (same as regular config)
if includeCopilotFields {
yaml.WriteString(" \"type\": \"http\",\n")
}
yaml.WriteString(" \"url\": \"http://localhost:$GH_AW_SERENA_PORT\"\n")
} else {
// Docker mode: use direct docker command format
if includeCopilotFields {
yaml.WriteString(" \"type\": \"stdio\",\n")
}

// Select the appropriate Serena container
containerImage := selectSerenaContainer(serenaTool)

yaml.WriteString(" \"command\": \"docker\",\n")
yaml.WriteString(" \"args\": [\n")
yaml.WriteString(" \"run\",\n")
yaml.WriteString(" \"-i\",\n")
yaml.WriteString(" \"--rm\",\n")
yaml.WriteString(" \"--network\", \"host\",\n")
yaml.WriteString(" \"-v\", \"${{ github.workspace }}:${{ github.workspace }}:rw\",\n")
yaml.WriteString(" \"--entrypoint\", \"serena\",\n")
yaml.WriteString(" \"" + containerImage + ":latest\",\n")
yaml.WriteString(" \"start-mcp-server\",\n")
yaml.WriteString(" \"--context\", \"codex\",\n")
yaml.WriteString(" \"--project\", \"${{ github.workspace }}\"")

// Append custom args if present
if len(customArgs) > 0 {
for _, arg := range customArgs {
yaml.WriteString(",\n")
yaml.WriteString(" \"" + arg + "\"")
}
}

yaml.WriteString("\n")
yaml.WriteString(" ]\n")
}

if isLast {
yaml.WriteString(" }\n")
} else {
yaml.WriteString(" },\n")
}
}
Loading