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
1,362 changes: 1,362 additions & 0 deletions .github/workflows/smoke-gemini.lock.yml

Large diffs are not rendered by default.

67 changes: 67 additions & 0 deletions .github/workflows/smoke-gemini.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
description: Smoke test workflow that validates Gemini engine functionality twice daily
on:
schedule: every 12h
workflow_dispatch:
pull_request:
types: [labeled]
names: ["smoke"]
permissions:
contents: read
issues: read
pull-requests: read
name: Smoke Gemini
engine: gemini
strict: true
imports:
- shared/gh.md
- shared/reporting.md
network:
allowed:
- defaults
- github
tools:
cache-memory: true
github:
toolsets: [repos, pull_requests]
edit:
bash:
- "*"
safe-outputs:
add-comment:
hide-older-comments: true
max: 2
create-issue:
expires: 2h
close-older-issues: true
add-labels:
allowed: [smoke-gemini]
messages:
footer: "> ✨ *[{workflow_name}]({run_url}) — Powered by Gemini*"
run-started: "✨ Gemini awakens... [{workflow_name}]({run_url}) begins its journey on this {event_type}..."
run-success: "🚀 [{workflow_name}]({run_url}) **MISSION COMPLETE!** Gemini has spoken. ✨"
run-failure: "⚠️ [{workflow_name}]({run_url}) {status}. Gemini encountered unexpected challenges..."
timeout-minutes: 10
---

# Smoke Test: Gemini Engine Validation

**CRITICAL EFFICIENCY REQUIREMENTS:**
- Keep ALL outputs extremely short and concise. Use single-line responses.
- NO verbose explanations or unnecessary context.
- Minimize file reading - only read what is absolutely necessary for the task.

## Test Requirements

1. **GitHub MCP Testing**: Use GitHub MCP tools to fetch details of exactly 2 merged pull requests from ${{ github.repository }} (title and number only)
2. **File Writing Testing**: Create a test file `/tmp/gh-aw/agent/smoke-test-gemini-${{ github.run_id }}.txt` with content "Smoke test passed for Gemini at $(date)" (create the directory if it doesn't exist)
3. **Bash Tool Testing**: Execute bash commands to verify file creation was successful (use `cat` to read the file back)
4. **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. If the command fails, mark this test as ❌ and report the failure.

## Output

Add a **very brief** comment (max 5-10 lines) to the current pull request with:
- ✅ or ❌ for each test result
- Overall status: PASS or FAIL

If all tests pass, use the `add_labels` safe-output tool to add the label `smoke-gemini` to the pull request.
15 changes: 10 additions & 5 deletions cmd/gh-aw/main_entry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ func TestValidateEngine(t *testing.T) {
engine: "copilot",
expectErr: false,
},
{
name: "valid gemini engine",
engine: "gemini",
expectErr: false,
},
{
name: "invalid engine",
engine: "gpt4",
Expand Down Expand Up @@ -82,11 +87,11 @@ func TestValidateEngine(t *testing.T) {
return
}

// Check that error message contains the expected format
// Error may include "Did you mean" suggestions, so we check if it starts with the base message
expectedMsg := fmt.Sprintf("invalid engine value '%s'. Must be 'claude', 'codex', or 'copilot'", tt.engine)
if tt.errMessage != "" && !strings.HasPrefix(err.Error(), expectedMsg) {
t.Errorf("validateEngine(%q) error message = %v, want to start with %v", tt.engine, err.Error(), expectedMsg)
// Check that error message contains the expected format.
// The engine list is dynamic, so only check the prefix.
expectedPrefix := fmt.Sprintf("invalid engine value '%s'. Must be", tt.engine)
if tt.errMessage != "" && !strings.HasPrefix(err.Error(), expectedPrefix) {
t.Errorf("validateEngine(%q) error message = %v, want to start with %v", tt.engine, err.Error(), expectedPrefix)
}
} else {
if err != nil {
Expand Down
6 changes: 3 additions & 3 deletions pkg/cli/completions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ func TestValidEngineNames(t *testing.T) {
assert.NotEmpty(t, engines, "Engine names list should not be empty")

// Verify expected engines are present
expectedEngines := []string{"copilot", "claude", "codex"}
expectedEngines := []string{"copilot", "claude", "codex", "gemini"}
for _, expected := range expectedEngines {
assert.Contains(t, engines, expected, "Expected engine '%s' to be in the list", expected)
}
Expand All @@ -245,7 +245,7 @@ func TestCompleteEngineNames(t *testing.T) {
{
name: "empty prefix returns all engines",
toComplete: "",
wantLen: 3, // copilot, claude, codex
wantLen: 4, // copilot, claude, codex, gemini
},
{
name: "c prefix returns claude, codex, copilot",
Expand Down Expand Up @@ -753,7 +753,7 @@ func TestValidEngineNamesConsistency(t *testing.T) {
assert.Len(t, thirdCall, len(secondCall), "Engine names list length should be consistent")

// Verify all expected engines are present in all calls
expectedEngines := []string{"copilot", "claude", "codex"}
expectedEngines := []string{"copilot", "claude", "codex", "gemini"}
for _, engine := range expectedEngines {
assert.Contains(t, firstCall, engine, "Expected engine '%s' in first call", engine)
assert.Contains(t, secondCall, engine, "Expected engine '%s' in second call", engine)
Expand Down
10 changes: 10 additions & 0 deletions pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,9 @@ const (

// CopilotLLMGatewayPort is the port for the Copilot LLM gateway
CopilotLLMGatewayPort = 10002

// GeminiLLMGatewayPort is the port for the Gemini LLM gateway
GeminiLLMGatewayPort = 10003
)

// DefaultMCPRegistryURL is the default MCP registry URL.
Expand Down Expand Up @@ -339,12 +342,16 @@ const (
EnvVarModelAgentCodex = "GH_AW_MODEL_AGENT_CODEX"
// EnvVarModelAgentCustom configures the default Custom model for agent execution
EnvVarModelAgentCustom = "GH_AW_MODEL_AGENT_CUSTOM"
// EnvVarModelAgentGemini configures the default Gemini model for agent execution
EnvVarModelAgentGemini = "GH_AW_MODEL_AGENT_GEMINI"
// EnvVarModelDetectionCopilot configures the default Copilot model for detection
EnvVarModelDetectionCopilot = "GH_AW_MODEL_DETECTION_COPILOT"
// EnvVarModelDetectionClaude configures the default Claude model for detection
EnvVarModelDetectionClaude = "GH_AW_MODEL_DETECTION_CLAUDE"
// EnvVarModelDetectionCodex configures the default Codex model for detection
EnvVarModelDetectionCodex = "GH_AW_MODEL_DETECTION_CODEX"
// EnvVarModelDetectionGemini configures the default Gemini model for detection
EnvVarModelDetectionGemini = "GH_AW_MODEL_DETECTION_GEMINI"

// Common environment variable names used across all engines

Expand Down Expand Up @@ -373,6 +380,9 @@ const (
// DefaultCodexVersion is the default version of the OpenAI Codex CLI
const DefaultCodexVersion Version = "0.104.0"

// DefaultGeminiVersion is the default version of the Google Gemini CLI
const DefaultGeminiVersion Version = "0.29.0"

// DefaultGitHubMCPServerVersion is the default version of the GitHub MCP server Docker image
const DefaultGitHubMCPServerVersion Version = "v0.30.3"

Expand Down
8 changes: 4 additions & 4 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -6848,17 +6848,17 @@
"oneOf": [
{
"type": "string",
"enum": ["claude", "codex", "copilot"],
"description": "Simple engine name: 'claude' (default, Claude Code), 'copilot' (GitHub Copilot CLI), or 'codex' (OpenAI Codex CLI)"
"enum": ["claude", "codex", "copilot", "gemini"],
"description": "Simple engine name: 'claude' (default, Claude Code), 'copilot' (GitHub Copilot CLI), 'codex' (OpenAI Codex CLI), or 'gemini' (Google Gemini CLI - experimental)"
},
{
"type": "object",
"description": "Extended engine configuration object with advanced options for model selection, turn limiting, environment variables, and custom steps",
"properties": {
"id": {
"type": "string",
"enum": ["claude", "codex", "copilot"],
"description": "AI engine identifier: 'claude' (Claude Code), 'codex' (OpenAI Codex CLI), or 'copilot' (GitHub Copilot CLI)"
"enum": ["claude", "codex", "copilot", "gemini"],
"description": "AI engine identifier: 'claude' (Claude Code), 'codex' (OpenAI Codex CLI), 'copilot' (GitHub Copilot CLI), or 'gemini' (Google Gemini CLI - experimental)"
},
"version": {
"type": ["string", "number"],
Expand Down
1 change: 1 addition & 0 deletions pkg/workflow/agentic_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ func NewEngineRegistry() *EngineRegistry {
registry.Register(NewClaudeEngine())
registry.Register(NewCodexEngine())
registry.Register(NewCopilotEngine())
registry.Register(NewGeminiEngine())

agenticEngineLog.Printf("Registered %d engines", len(registry.engines))
return registry
Expand Down
17 changes: 14 additions & 3 deletions pkg/workflow/agentic_engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,21 @@ import (
func TestEngineRegistry(t *testing.T) {
registry := NewEngineRegistry()

// Test that built-in engines are registered
// Test that built-in engines are registered - check for specific IDs rather than exact count
// to avoid brittleness when new engines are added
supportedEngines := registry.GetSupportedEngines()
if len(supportedEngines) != 3 {
t.Errorf("Expected 3 supported engines, got %d", len(supportedEngines))
expectedEngineIDs := []string{"claude", "codex", "copilot", "gemini"}
for _, engineID := range expectedEngineIDs {
found := false
for _, id := range supportedEngines {
if id == engineID {
found = true
break
}
}
if !found {
t.Errorf("Expected engine '%s' to be registered", engineID)
}
}

// Test getting engines by ID
Expand Down
16 changes: 16 additions & 0 deletions pkg/workflow/domains.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ var ClaudeDefaultDomains = []string{
"ts-ocsp.ws.symantec.com",
}

// GeminiDefaultDomains are the default domains required for Google Gemini CLI authentication and operation
var GeminiDefaultDomains = []string{
"*.googleapis.com",
"generativelanguage.googleapis.com",
"github.com",
"host.docker.internal",
"raw.githubusercontent.com",
"registry.npmjs.org",
}

// PlaywrightDomains are the domains required for Playwright browser downloads
// These domains are needed when Playwright MCP server initializes in the Docker container
var PlaywrightDomains = []string{
Expand Down Expand Up @@ -510,6 +520,12 @@ func GetClaudeAllowedDomainsWithToolsAndRuntimes(network *NetworkPermissions, to
return mergeDomainsWithNetworkToolsAndRuntimes(ClaudeDefaultDomains, network, tools, runtimes)
}

// GetGeminiAllowedDomainsWithToolsAndRuntimes merges Gemini default domains with NetworkPermissions, HTTP MCP server domains, and runtime ecosystem domains
// Returns a deduplicated, sorted, comma-separated string suitable for AWF's --allow-domains flag
func GetGeminiAllowedDomainsWithToolsAndRuntimes(network *NetworkPermissions, tools map[string]any, runtimes map[string]any) string {
return mergeDomainsWithNetworkToolsAndRuntimes(GeminiDefaultDomains, network, tools, runtimes)
}

// GetBlockedDomains returns the blocked domains from network permissions
// Returns empty slice if no network permissions configured or no domains blocked
// The returned list is sorted and deduplicated
Expand Down
Loading
Loading