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
15 changes: 12 additions & 3 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4766,9 +4766,18 @@
}
},
"mode": {
"type": "string",
"description": "Integration mode: 'cli' (recommended) installs @playwright/cli via npm for token-efficient CLI invocations \u2014 use playwright-cli commands in bash and localhost to reach local servers; 'mcp' (deprecated) runs a Docker-based MCP server.",
"enum": ["cli", "mcp"]
"description": "Integration mode: 'cli' (recommended) installs @playwright/cli via npm for token-efficient CLI invocations \u2014 use playwright-cli commands in bash and localhost to reach local servers; 'mcp' (deprecated) runs a Docker-based MCP server. Must be a literal value; GitHub Actions expressions are rejected.",
"oneOf": [
{
"type": "string",
"enum": ["cli", "mcp"]
},
{
"type": "string",
"pattern": "^\\$\\{\\{.*\\}\\}$",
"description": "Not allowed at runtime: mode must be a literal 'cli' or 'mcp' value, not a GitHub Actions expression."
}
]
}
},
"additionalProperties": false
Expand Down
16 changes: 13 additions & 3 deletions pkg/workflow/playwright_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ import (

var playwrightValidationLog = logger.New("workflow:playwright_validation")

// validatePlaywrightMode warns when the playwright tool is configured in MCP
// mode. MCP mode is deprecated; use mode: cli instead for token-efficient,
// container-free browser automation.
// validatePlaywrightMode validates that Playwright mode is static, then warns
// when the tool is configured in MCP mode. MCP mode is deprecated; use mode:
// cli instead for token-efficient, container-free browser automation.
func (c *Compiler) validatePlaywrightMode(workflowData *WorkflowData) error {
if workflowData == nil || workflowData.Tools == nil {
return nil
Expand All @@ -49,6 +49,16 @@ func (c *Compiler) validatePlaywrightMode(workflowData *WorkflowData) error {
if !ok || playwrightTool == false {
return nil
}
if config, ok := playwrightTool.(map[string]any); ok {
if mode, ok := config["mode"].(string); ok && hasExpressionMarker(mode) {
return NewValidationError(
Comment on lines +52 to +54
"tools.playwright.mode",
mode,
"mode must be a literal value; expressions are not allowed",
"Set mode to either mcp or cli",
)
}
}

if isPlaywrightCLIMode(workflowData.Tools) {
playwrightValidationLog.Print("playwright mode: cli — no deprecation warning")
Expand Down
38 changes: 38 additions & 0 deletions pkg/workflow/playwright_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
package workflow

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -54,6 +56,12 @@ func TestValidatePlaywrightMode(t *testing.T) {
tools: map[string]any{"playwright": map[string]any{"mode": "CLI"}},
expectWarn: false,
},
{
name: "playwright mode expression is rejected",
tools: map[string]any{"playwright": map[string]any{"mode": "${{ inputs.playwright-mode }}"}},
expectError: true,
errorSubstr: "mode must be a literal value; expressions are not allowed",
},
{
name: "playwright mcp mode in strict mode warns only",
tools: map[string]any{"playwright": map[string]any{"mode": "mcp"}},
Expand Down Expand Up @@ -90,6 +98,36 @@ func TestValidatePlaywrightMode(t *testing.T) {
}
}

// TestCompileWorkflowRejectsPlaywrightModeExpression ensures that a full compile
// of a workflow with an expression-valued tools.playwright.mode surfaces the
// field-specific error from validatePlaywrightMode, rather than the generic
// JSON schema enum error. This guards against the schema's enum constraint
// preempting the dedicated validator.
func TestCompileWorkflowRejectsPlaywrightModeExpression(t *testing.T) {
tmpDir := t.TempDir()
mdPath := filepath.Join(tmpDir, "test-workflow.md")
content := `---
on: push
engine: claude
tools:
playwright:
mode: ${{ inputs.playwright-mode }}
---

# Test Workflow

Test playwright mode expression rejection.
`
require.NoError(t, os.WriteFile(mdPath, []byte(content), 0644))

compiler := NewCompiler()
err := compiler.CompileWorkflow(mdPath)

require.Error(t, err, "expected compilation to fail for expression-valued playwright mode")
assert.Contains(t, err.Error(), "tools.playwright.mode")
assert.Contains(t, err.Error(), "mode must be a literal value; expressions are not allowed")
}

// TestValidatePlaywrightModeNilWorkflow ensures no panic on nil/empty input.
func TestValidatePlaywrightModeNilWorkflow(t *testing.T) {
compiler := NewCompiler()
Expand Down
Loading