diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index ee9505e6388..8eca1627c10 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -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 diff --git a/pkg/workflow/playwright_validation.go b/pkg/workflow/playwright_validation.go index 0989c09e4e2..a7f3463ba12 100644 --- a/pkg/workflow/playwright_validation.go +++ b/pkg/workflow/playwright_validation.go @@ -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 @@ -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( + "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") diff --git a/pkg/workflow/playwright_validation_test.go b/pkg/workflow/playwright_validation_test.go index b6d9bee762d..faf664d294a 100644 --- a/pkg/workflow/playwright_validation_test.go +++ b/pkg/workflow/playwright_validation_test.go @@ -3,6 +3,8 @@ package workflow import ( + "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -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"}}, @@ -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()