diff --git a/.changeset/patch-add-importable-tools.md b/.changeset/patch-add-importable-tools.md new file mode 100644 index 00000000000..cf1336e5aa4 --- /dev/null +++ b/.changeset/patch-add-importable-tools.md @@ -0,0 +1,9 @@ +--- +"gh-aw": patch +--- + +Add importable tools: `agentic-workflows`, `serena`, and `playwright`. + +These tool definitions were added to the parser schema so they can be configured +in shared workflow files and merged into consuming workflows during compilation. +Includes tests and necessary schema updates. diff --git a/pkg/parser/schemas/included_file_schema.json b/pkg/parser/schemas/included_file_schema.json index 465f9288b06..682b6d891b7 100644 --- a/pkg/parser/schemas/included_file_schema.json +++ b/pkg/parser/schemas/included_file_schema.json @@ -424,6 +424,223 @@ } ] ] + }, + "playwright": { + "description": "Playwright browser automation tool for web scraping, testing, and UI interactions in containerized browsers", + "oneOf": [ + { + "type": "null", + "description": "Enable Playwright tool with default settings (localhost access only for security)" + }, + { + "type": "object", + "description": "Playwright tool configuration with custom version and domain restrictions", + "properties": { + "version": { + "type": ["string", "number"], + "description": "Optional Playwright container version (e.g., 'v1.41.0', 1.41, 20). Numeric values are automatically converted to strings at runtime.", + "examples": ["v1.41.0", 1.41, 20] + }, + "allowed_domains": { + "description": "Domains allowed for Playwright browser network access. Defaults to localhost only for security.", + "oneOf": [ + { + "type": "array", + "description": "List of allowed domains or patterns (e.g., ['github.com', '*.example.com'])", + "items": { + "type": "string" + } + }, + { + "type": "string", + "description": "Single allowed domain (e.g., 'github.com')" + } + ] + }, + "args": { + "type": "array", + "description": "Optional additional arguments to append to the generated MCP server command", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + ] + }, + "serena": { + "description": "Serena MCP server for AI-powered code intelligence with language service integration", + "oneOf": [ + { + "type": "null", + "description": "Enable Serena with default settings" + }, + { + "type": "array", + "description": "Short syntax: array of language identifiers to enable (e.g., [\"go\", \"typescript\"])", + "items": { + "type": "string", + "enum": ["go", "typescript", "python", "java", "rust", "csharp"] + } + }, + { + "type": "object", + "description": "Serena configuration with custom version and language-specific settings", + "properties": { + "version": { + "type": ["string", "number"], + "description": "Optional Serena MCP version. Numeric values are automatically converted to strings at runtime.", + "examples": ["latest", "0.1.0", 1.0] + }, + "args": { + "type": "array", + "description": "Optional additional arguments to append to the generated MCP server command", + "items": { + "type": "string" + } + }, + "languages": { + "type": "object", + "description": "Language-specific configuration for Serena language services", + "properties": { + "go": { + "oneOf": [ + { + "type": "null", + "description": "Enable Go language service with default version" + }, + { + "type": "object", + "properties": { + "version": { + "type": ["string", "number"], + "description": "Go version (e.g., \"1.21\", 1.21)" + }, + "go-mod-file": { + "type": "string", + "description": "Path to go.mod file for Go version detection (e.g., \"go.mod\", \"backend/go.mod\")" + }, + "gopls-version": { + "type": "string", + "description": "Version of gopls to install (e.g., \"latest\", \"v0.14.2\")" + } + }, + "additionalProperties": false + } + ] + }, + "typescript": { + "oneOf": [ + { + "type": "null", + "description": "Enable TypeScript language service with default version" + }, + { + "type": "object", + "properties": { + "version": { + "type": ["string", "number"], + "description": "Node.js version for TypeScript (e.g., \"22\", 22)" + } + }, + "additionalProperties": false + } + ] + }, + "python": { + "oneOf": [ + { + "type": "null", + "description": "Enable Python language service with default version" + }, + { + "type": "object", + "properties": { + "version": { + "type": ["string", "number"], + "description": "Python version (e.g., \"3.12\", 3.12)" + } + }, + "additionalProperties": false + } + ] + }, + "java": { + "oneOf": [ + { + "type": "null", + "description": "Enable Java language service with default version" + }, + { + "type": "object", + "properties": { + "version": { + "type": ["string", "number"], + "description": "Java version (e.g., \"21\", 21)" + } + }, + "additionalProperties": false + } + ] + }, + "rust": { + "oneOf": [ + { + "type": "null", + "description": "Enable Rust language service with default version" + }, + { + "type": "object", + "properties": { + "version": { + "type": ["string", "number"], + "description": "Rust version (e.g., \"stable\", \"1.75\")" + } + }, + "additionalProperties": false + } + ] + }, + "csharp": { + "oneOf": [ + { + "type": "null", + "description": "Enable C# language service with default version" + }, + { + "type": "object", + "properties": { + "version": { + "type": ["string", "number"], + "description": ".NET version for C# (e.g., \"8.0\", 8.0)" + } + }, + "additionalProperties": false + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "agentic-workflows": { + "description": "GitHub Agentic Workflows MCP server for workflow introspection and analysis. Provides tools for checking status, compiling workflows, downloading logs, and auditing runs.", + "oneOf": [ + { + "type": "boolean", + "description": "Enable agentic-workflows tool with default settings" + }, + { + "type": "null", + "description": "Enable agentic-workflows tool with default settings (same as true)" + } + ], + "examples": [true, null] } }, "additionalProperties": { diff --git a/pkg/workflow/importable_tools_test.go b/pkg/workflow/importable_tools_test.go new file mode 100644 index 00000000000..170b258f7af --- /dev/null +++ b/pkg/workflow/importable_tools_test.go @@ -0,0 +1,539 @@ +package workflow_test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/githubnext/gh-aw/pkg/testutil" + "github.com/githubnext/gh-aw/pkg/workflow" +) + +// TestImportPlaywrightTool tests that playwright tool can be imported from a shared workflow +func TestImportPlaywrightTool(t *testing.T) { + tempDir := testutil.TempDir(t, "test-*") + + // Create a shared workflow with playwright tool + sharedPath := filepath.Join(tempDir, "shared-playwright.md") + sharedContent := `--- +description: "Shared playwright configuration" +tools: + playwright: + version: "v1.41.0" + allowed_domains: + - "example.com" + - "github.com" +network: + allowed: + - playwright +--- + +# Shared Playwright Configuration +` + if err := os.WriteFile(sharedPath, []byte(sharedContent), 0644); err != nil { + t.Fatalf("Failed to write shared file: %v", err) + } + + // Create main workflow that imports playwright + workflowPath := filepath.Join(tempDir, "main-workflow.md") + workflowContent := `--- +on: issues +engine: copilot +imports: + - shared-playwright.md +permissions: + contents: read + issues: read + pull-requests: read +--- + +# Main Workflow + +Uses imported playwright tool. +` + if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil { + t.Fatalf("Failed to write workflow file: %v", err) + } + + // Compile the workflow + compiler := workflow.NewCompiler(false, "", "test") + if err := compiler.CompileWorkflow(workflowPath); err != nil { + t.Fatalf("CompileWorkflow failed: %v", err) + } + + // Read the generated lock file + lockFilePath := strings.TrimSuffix(workflowPath, ".md") + ".lock.yml" + lockFileContent, err := os.ReadFile(lockFilePath) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + + workflowData := string(lockFileContent) + + // Verify playwright is configured in the MCP config + if !strings.Contains(workflowData, `"playwright"`) { + t.Error("Expected compiled workflow to contain playwright tool") + } + + // Verify playwright Docker image + if !strings.Contains(workflowData, "mcr.microsoft.com/playwright/mcp") { + t.Error("Expected compiled workflow to contain playwright Docker image") + } + + // Verify allowed domains are present + if !strings.Contains(workflowData, "example.com") { + t.Error("Expected compiled workflow to contain example.com domain") + } + if !strings.Contains(workflowData, "github.com") { + t.Error("Expected compiled workflow to contain github.com domain") + } +} + +// TestImportSerenaTool tests that serena tool can be imported from a shared workflow +func TestImportSerenaTool(t *testing.T) { + tempDir := testutil.TempDir(t, "test-*") + + // Create a shared workflow with serena tool + sharedPath := filepath.Join(tempDir, "shared-serena.md") + sharedContent := `--- +description: "Shared serena configuration" +tools: + serena: + - go + - typescript +--- + +# Shared Serena Configuration +` + if err := os.WriteFile(sharedPath, []byte(sharedContent), 0644); err != nil { + t.Fatalf("Failed to write shared file: %v", err) + } + + // Create main workflow that imports serena + workflowPath := filepath.Join(tempDir, "main-workflow.md") + workflowContent := `--- +on: issues +engine: copilot +imports: + - shared-serena.md +permissions: + contents: read + issues: read + pull-requests: read +--- + +# Main Workflow + +Uses imported serena tool. +` + if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil { + t.Fatalf("Failed to write workflow file: %v", err) + } + + // Compile the workflow + compiler := workflow.NewCompiler(false, "", "test") + if err := compiler.CompileWorkflow(workflowPath); err != nil { + t.Fatalf("CompileWorkflow failed: %v", err) + } + + // Read the generated lock file + lockFilePath := strings.TrimSuffix(workflowPath, ".md") + ".lock.yml" + lockFileContent, err := os.ReadFile(lockFilePath) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + + workflowData := string(lockFileContent) + + // Verify serena is configured in the MCP config + if !strings.Contains(workflowData, `"serena"`) { + t.Error("Expected compiled workflow to contain serena tool") + } + + // Verify serena command + if !strings.Contains(workflowData, "git+https://github.com/oraios/serena") { + t.Error("Expected compiled workflow to contain serena git repository") + } + + // Verify language service setup for Go + if !strings.Contains(workflowData, "Setup Go") { + t.Error("Expected compiled workflow to contain Go setup for serena") + } + + // Verify language service setup for TypeScript (Node.js) + if !strings.Contains(workflowData, "Setup Node.js") { + t.Error("Expected compiled workflow to contain Node.js setup for serena") + } +} + +// TestImportAgenticWorkflowsTool tests that agentic-workflows tool can be imported +func TestImportAgenticWorkflowsTool(t *testing.T) { + tempDir := testutil.TempDir(t, "test-*") + + // Create a shared workflow with agentic-workflows tool + sharedPath := filepath.Join(tempDir, "shared-aw.md") + sharedContent := `--- +description: "Shared agentic-workflows configuration" +tools: + agentic-workflows: true +permissions: + actions: read +--- + +# Shared Agentic Workflows Configuration +` + if err := os.WriteFile(sharedPath, []byte(sharedContent), 0644); err != nil { + t.Fatalf("Failed to write shared file: %v", err) + } + + // Create main workflow that imports agentic-workflows + workflowPath := filepath.Join(tempDir, "main-workflow.md") + workflowContent := `--- +on: issues +engine: copilot +imports: + - shared-aw.md +permissions: + actions: read + contents: read + issues: read + pull-requests: read +--- + +# Main Workflow + +Uses imported agentic-workflows tool. +` + if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil { + t.Fatalf("Failed to write workflow file: %v", err) + } + + // Compile the workflow + compiler := workflow.NewCompiler(false, "", "test") + if err := compiler.CompileWorkflow(workflowPath); err != nil { + t.Fatalf("CompileWorkflow failed: %v", err) + } + + // Read the generated lock file + lockFilePath := strings.TrimSuffix(workflowPath, ".md") + ".lock.yml" + lockFileContent, err := os.ReadFile(lockFilePath) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + + workflowData := string(lockFileContent) + + // Verify gh aw mcp-server command is present + if !strings.Contains(workflowData, `"aw", "mcp-server"`) { + t.Error("Expected compiled workflow to contain 'aw', 'mcp-server' command") + } + + // Verify gh CLI is used + if !strings.Contains(workflowData, `"command": "gh"`) { + t.Error("Expected compiled workflow to contain gh CLI command for agentic-workflows") + } +} + +// TestImportAllThreeTools tests importing all three tools together +func TestImportAllThreeTools(t *testing.T) { + tempDir := testutil.TempDir(t, "test-*") + + // Create a shared workflow with all three tools + sharedPath := filepath.Join(tempDir, "shared-all.md") + sharedContent := `--- +description: "Shared configuration with all tools" +tools: + agentic-workflows: true + serena: + - go + playwright: + version: "v1.41.0" + allowed_domains: + - "example.com" +permissions: + actions: read +network: + allowed: + - playwright +--- + +# Shared All Tools Configuration +` + if err := os.WriteFile(sharedPath, []byte(sharedContent), 0644); err != nil { + t.Fatalf("Failed to write shared file: %v", err) + } + + // Create main workflow that imports all tools + workflowPath := filepath.Join(tempDir, "main-workflow.md") + workflowContent := `--- +on: issues +engine: copilot +imports: + - shared-all.md +permissions: + actions: read + contents: read + issues: read + pull-requests: read +--- + +# Main Workflow + +Uses all imported tools. +` + if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil { + t.Fatalf("Failed to write workflow file: %v", err) + } + + // Compile the workflow + compiler := workflow.NewCompiler(false, "", "test") + if err := compiler.CompileWorkflow(workflowPath); err != nil { + t.Fatalf("CompileWorkflow failed: %v", err) + } + + // Read the generated lock file + lockFilePath := strings.TrimSuffix(workflowPath, ".md") + ".lock.yml" + lockFileContent, err := os.ReadFile(lockFilePath) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + + workflowData := string(lockFileContent) + + // Verify all three tools are present + if !strings.Contains(workflowData, `"playwright"`) { + t.Error("Expected compiled workflow to contain playwright tool") + } + if !strings.Contains(workflowData, `"serena"`) { + t.Error("Expected compiled workflow to contain serena tool") + } + if !strings.Contains(workflowData, `"aw", "mcp-server"`) { + t.Error("Expected compiled workflow to contain agentic-workflows tool") + } + + // Verify specific configurations + if !strings.Contains(workflowData, "mcr.microsoft.com/playwright/mcp") { + t.Error("Expected compiled workflow to contain playwright Docker image") + } + if !strings.Contains(workflowData, "git+https://github.com/oraios/serena") { + t.Error("Expected compiled workflow to contain serena git repository") + } + if !strings.Contains(workflowData, "example.com") { + t.Error("Expected compiled workflow to contain example.com domain for playwright") + } +} + +// TestImportSerenaWithLanguageConfig tests serena with detailed language configuration +func TestImportSerenaWithLanguageConfig(t *testing.T) { + tempDir := testutil.TempDir(t, "test-*") + + // Create a shared workflow with serena tool with detailed language config + sharedPath := filepath.Join(tempDir, "shared-serena-config.md") + sharedContent := `--- +description: "Shared serena with language config" +tools: + serena: + languages: + go: + version: "1.21" + gopls-version: "latest" + typescript: + version: "22" +--- + +# Shared Serena Language Configuration +` + if err := os.WriteFile(sharedPath, []byte(sharedContent), 0644); err != nil { + t.Fatalf("Failed to write shared file: %v", err) + } + + // Create main workflow that imports serena + workflowPath := filepath.Join(tempDir, "main-workflow.md") + workflowContent := `--- +on: issues +engine: copilot +imports: + - shared-serena-config.md +permissions: + contents: read + issues: read + pull-requests: read +--- + +# Main Workflow + +Uses imported serena with language config. +` + if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil { + t.Fatalf("Failed to write workflow file: %v", err) + } + + // Compile the workflow + compiler := workflow.NewCompiler(false, "", "test") + if err := compiler.CompileWorkflow(workflowPath); err != nil { + t.Fatalf("CompileWorkflow failed: %v", err) + } + + // Read the generated lock file + lockFilePath := strings.TrimSuffix(workflowPath, ".md") + ".lock.yml" + lockFileContent, err := os.ReadFile(lockFilePath) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + + workflowData := string(lockFileContent) + + // Verify serena is configured + if !strings.Contains(workflowData, `"serena"`) { + t.Error("Expected compiled workflow to contain serena tool") + } + + // Verify Go setup with version + if !strings.Contains(workflowData, "Setup Go") { + t.Error("Expected compiled workflow to contain Go setup") + } + if !strings.Contains(workflowData, "go-version: '1.21'") { + t.Error("Expected compiled workflow to contain Go version 1.21") + } + + // Verify Node.js setup with version + if !strings.Contains(workflowData, "Setup Node.js") { + t.Error("Expected compiled workflow to contain Node.js setup") + } + // Note: TypeScript version in serena config may use default Node.js version + // This is expected behavior as the TypeScript version configuration + // refers to Node.js version, and may fall back to defaults +} + +// TestImportPlaywrightWithCustomArgs tests playwright with custom arguments +func TestImportPlaywrightWithCustomArgs(t *testing.T) { + tempDir := testutil.TempDir(t, "test-*") + + // Create a shared workflow with playwright tool with custom args + sharedPath := filepath.Join(tempDir, "shared-playwright-args.md") + sharedContent := `--- +description: "Shared playwright with custom args" +tools: + playwright: + version: "v1.41.0" + allowed_domains: + - "example.com" + args: + - "--custom-flag" + - "value" +network: + allowed: + - playwright +--- + +# Shared Playwright with Args +` + if err := os.WriteFile(sharedPath, []byte(sharedContent), 0644); err != nil { + t.Fatalf("Failed to write shared file: %v", err) + } + + // Create main workflow that imports playwright + workflowPath := filepath.Join(tempDir, "main-workflow.md") + workflowContent := `--- +on: issues +engine: copilot +imports: + - shared-playwright-args.md +permissions: + contents: read + issues: read + pull-requests: read +--- + +# Main Workflow + +Uses imported playwright with custom args. +` + if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil { + t.Fatalf("Failed to write workflow file: %v", err) + } + + // Compile the workflow + compiler := workflow.NewCompiler(false, "", "test") + if err := compiler.CompileWorkflow(workflowPath); err != nil { + t.Fatalf("CompileWorkflow failed: %v", err) + } + + // Read the generated lock file + lockFilePath := strings.TrimSuffix(workflowPath, ".md") + ".lock.yml" + lockFileContent, err := os.ReadFile(lockFilePath) + if err != nil { + t.Fatalf("Failed to read lock file: %v", err) + } + + workflowData := string(lockFileContent) + + // Verify playwright is configured + if !strings.Contains(workflowData, `"playwright"`) { + t.Error("Expected compiled workflow to contain playwright tool") + } + + // Verify custom args are present + if !strings.Contains(workflowData, "--custom-flag") { + t.Error("Expected compiled workflow to contain --custom-flag custom argument") + } + if !strings.Contains(workflowData, "value") { + t.Error("Expected compiled workflow to contain custom argument value") + } +} + +// TestImportAgenticWorkflowsRequiresPermissions tests that agentic-workflows tool requires actions:read permission +func TestImportAgenticWorkflowsRequiresPermissions(t *testing.T) { + tempDir := testutil.TempDir(t, "test-*") + + // Create a shared workflow with agentic-workflows tool + sharedPath := filepath.Join(tempDir, "shared-aw.md") + sharedContent := `--- +description: "Shared agentic-workflows configuration" +tools: + agentic-workflows: true +permissions: + actions: read +--- + +# Shared Agentic Workflows Configuration +` + if err := os.WriteFile(sharedPath, []byte(sharedContent), 0644); err != nil { + t.Fatalf("Failed to write shared file: %v", err) + } + + // Create main workflow WITHOUT actions:read permission + workflowPath := filepath.Join(tempDir, "main-workflow.md") + workflowContent := `--- +on: issues +engine: copilot +imports: + - shared-aw.md +permissions: + contents: read + issues: read + pull-requests: read +--- + +# Main Workflow + +Missing actions:read permission. +` + if err := os.WriteFile(workflowPath, []byte(workflowContent), 0644); err != nil { + t.Fatalf("Failed to write workflow file: %v", err) + } + + // Compile the workflow - should fail due to missing permission + compiler := workflow.NewCompiler(false, "", "test") + err := compiler.CompileWorkflow(workflowPath) + + if err == nil { + t.Fatal("Expected CompileWorkflow to fail due to missing actions:read permission") + } + + // Verify error message mentions permissions + if !strings.Contains(err.Error(), "actions: read") { + t.Errorf("Expected error to mention 'actions: read', got: %v", err) + } +}