diff --git a/pkg/parser/schema_test.go b/pkg/parser/schema_test.go index d41abe304c..b01c393f7d 100644 --- a/pkg/parser/schema_test.go +++ b/pkg/parser/schema_test.go @@ -1565,6 +1565,242 @@ func TestValidateIncludedFileFrontmatterWithSchema(t *testing.T) { } } +// TestSchemaRestrictionEnforcement verifies that schema restrictions are properly enforced +// when validating included files vs main workflows +func TestSchemaRestrictionEnforcement(t *testing.T) { + t.Run("engine.command rejected in included files", func(t *testing.T) { + // engine.command should fail for included files + includedFrontmatter := map[string]any{ + "engine": map[string]any{ + "id": "claude", + "command": "custom-command", + }, + "tools": map[string]any{ + "bash": true, + }, + } + + err := ValidateIncludedFileFrontmatterWithSchema(includedFrontmatter) + if err == nil { + t.Error("ValidateIncludedFileFrontmatterWithSchema() expected error for engine.command, got nil") + } else if !strings.Contains(err.Error(), "additional properties 'command' not allowed") { + t.Errorf("ValidateIncludedFileFrontmatterWithSchema() error = %v, expected 'additional properties 'command' not allowed'", err) + } + + // engine.command should pass for main workflows + mainFrontmatter := map[string]any{ + "on": "push", + "engine": map[string]any{ + "id": "claude", + "command": "custom-command", + }, + "tools": map[string]any{ + "bash": true, + }, + } + + err = ValidateMainWorkflowFrontmatterWithSchema(mainFrontmatter) + if err != nil { + t.Errorf("ValidateMainWorkflowFrontmatterWithSchema() unexpected error for engine.command: %v", err) + } + }) + + t.Run("MCP config in mcp-servers section allowed in both schemas", func(t *testing.T) { + // MCP servers configuration is allowed in both included files and main workflows + // using the mcp-servers top-level property + includedFrontmatter := map[string]any{ + "mcp-servers": map[string]any{ + "myMCP": map[string]any{ + "type": "stdio", + "command": "npx", + "args": []any{ + "-y", + "@modelcontextprotocol/server-filesystem", + "/tmp", + }, + "env": map[string]any{ + "API_KEY": "${{ secrets.API_KEY }}", + }, + "allowed": []string{"read_file", "write_file"}, + }, + }, + } + + err := ValidateIncludedFileFrontmatterWithSchema(includedFrontmatter) + if err != nil { + t.Errorf("ValidateIncludedFileFrontmatterWithSchema() unexpected error for MCP server config: %v", err) + } + + // Same configuration should pass for main workflows + mainFrontmatter := map[string]any{ + "on": "push", + "mcp-servers": map[string]any{ + "myMCP": map[string]any{ + "type": "stdio", + "command": "npx", + "args": []any{ + "-y", + "@modelcontextprotocol/server-filesystem", + "/tmp", + }, + "env": map[string]any{ + "API_KEY": "${{ secrets.API_KEY }}", + }, + "allowed": []string{"read_file", "write_file"}, + }, + }, + } + + err = ValidateMainWorkflowFrontmatterWithSchema(mainFrontmatter) + if err != nil { + t.Errorf("ValidateMainWorkflowFrontmatterWithSchema() unexpected error for MCP server config: %v", err) + } + }) + + t.Run("missing 'on' field in main workflow", func(t *testing.T) { + // Missing 'on' field should fail for main workflows + mainFrontmatter := map[string]any{ + "engine": "claude", + "tools": map[string]any{ + "bash": true, + }, + } + + err := ValidateMainWorkflowFrontmatterWithSchema(mainFrontmatter) + if err == nil { + t.Error("ValidateMainWorkflowFrontmatterWithSchema() expected error for missing 'on' field, got nil") + } else if !strings.Contains(err.Error(), "missing property 'on'") { + t.Errorf("ValidateMainWorkflowFrontmatterWithSchema() error = %v, expected 'missing property 'on''", err) + } + + // Missing 'on' field should pass for included files + includedFrontmatter := map[string]any{ + "engine": "claude", + "tools": map[string]any{ + "bash": true, + }, + } + + err = ValidateIncludedFileFrontmatterWithSchema(includedFrontmatter) + if err != nil { + t.Errorf("ValidateIncludedFileFrontmatterWithSchema() unexpected error for missing 'on' field: %v", err) + } + }) + + t.Run("'on' field rejected in included files", func(t *testing.T) { + // 'on' field should fail for included files + includedFrontmatter := map[string]any{ + "on": "push", + "tools": map[string]any{ + "bash": true, + }, + } + + err := ValidateIncludedFileFrontmatterWithSchema(includedFrontmatter) + if err == nil { + t.Error("ValidateIncludedFileFrontmatterWithSchema() expected error for 'on' field, got nil") + } else if !strings.Contains(err.Error(), "additional properties 'on' not allowed") { + t.Errorf("ValidateIncludedFileFrontmatterWithSchema() error = %v, expected 'additional properties 'on' not allowed'", err) + } + }) + + t.Run("valid included file with all allowed properties", func(t *testing.T) { + // Test that all 15 allowed properties work in included files + // Based on included_file_schema.json properties: description, metadata, inputs, applyTo, services, + // mcp-servers, steps, tools, engine, safe-outputs, safe-inputs, secret-masking, runtimes, network, permissions + includedFrontmatter := map[string]any{ + "description": "Shared configuration for workflows", + "metadata": map[string]any{ + "author": "Test User", + "version": "1.0.0", + }, + "inputs": map[string]any{ + "count": map[string]any{ + "description": "Number of items", + "type": "number", + "default": 100, + }, + }, + "applyTo": "**/*.py", + "services": map[string]any{ + "redis": map[string]any{ + "image": "redis:latest", + }, + }, + "mcp-servers": map[string]any{ + "filesystem": map[string]any{ + "command": "npx", + "args": []string{"-y", "@modelcontextprotocol/server-filesystem"}, + }, + }, + "steps": []any{ + map[string]any{ + "name": "Test step", + "run": "echo test", + }, + }, + "tools": map[string]any{ + "github": map[string]any{ + "allowed": []string{"issue_read"}, + }, + "bash": true, + }, + "engine": map[string]any{ + "id": "claude", + "model": "claude-3-5-sonnet-20241022", + }, + "safe-outputs": map[string]any{ + "jobs": map[string]any{}, + }, + "safe-inputs": map[string]any{}, + "secret-masking": map[string]any{ + "steps": []any{}, + }, + "runtimes": map[string]any{ + "node": map[string]any{ + "version": "20", + }, + }, + "network": map[string]any{ + "allowed": []string{"example.com"}, + }, + "permissions": map[string]any{ + "contents": "read", + "issues": "write", + }, + } + + err := ValidateIncludedFileFrontmatterWithSchema(includedFrontmatter) + if err != nil { + t.Errorf("ValidateIncludedFileFrontmatterWithSchema() unexpected error for valid included file with all allowed properties: %v", err) + } + }) + + t.Run("valid included file with subset of allowed properties", func(t *testing.T) { + // Test a realistic subset of properties commonly used in included files + includedFrontmatter := map[string]any{ + "description": "Common tools configuration", + "tools": map[string]any{ + "github": map[string]any{ + "allowed": []string{"issue_read", "pull_request_read"}, + }, + "bash": []string{"ls", "cat", "grep"}, + }, + "permissions": map[string]any{ + "contents": "read", + "issues": "read", + "pull-requests": "read", + }, + "network": "defaults", + } + + err := ValidateIncludedFileFrontmatterWithSchema(includedFrontmatter) + if err != nil { + t.Errorf("ValidateIncludedFileFrontmatterWithSchema() unexpected error for valid included file with subset of properties: %v", err) + } + }) +} + func TestValidateWithSchema(t *testing.T) { tests := []struct { name string diff --git a/pkg/workflow/data/action_pins.json b/pkg/workflow/data/action_pins.json index 0262bcec27..b8a317833e 100644 --- a/pkg/workflow/data/action_pins.json +++ b/pkg/workflow/data/action_pins.json @@ -1,3 +1,194 @@ { - "entries": {} + "entries": { + "actions/ai-inference@v2": { + "repo": "actions/ai-inference", + "version": "v2", + "sha": "334892bb203895caaed82ec52d23c1ed9385151e" + }, + "actions/attest-build-provenance@v2": { + "repo": "actions/attest-build-provenance", + "version": "v2", + "sha": "96b4a1ef7235a096b17240c259729fdd70c83d45" + }, + "actions/cache/restore@v4.3.0": { + "repo": "actions/cache/restore", + "version": "v4.3.0", + "sha": "0057852bfaa89a56745cba8c7296529d2fc39830" + }, + "actions/cache/save@v4.3.0": { + "repo": "actions/cache/save", + "version": "v4.3.0", + "sha": "0057852bfaa89a56745cba8c7296529d2fc39830" + }, + "actions/cache@v4.3.0": { + "repo": "actions/cache", + "version": "v4.3.0", + "sha": "0057852bfaa89a56745cba8c7296529d2fc39830" + }, + "actions/checkout@v4": { + "repo": "actions/checkout", + "version": "v4", + "sha": "34e114876b0b11c390a56381ad16ebd13914f8d5" + }, + "actions/checkout@v5.0.1": { + "repo": "actions/checkout", + "version": "v5.0.1", + "sha": "93cb6efe18208431cddfb8368fd83d5badbf9bfd" + }, + "actions/create-github-app-token@v2.2.1": { + "repo": "actions/create-github-app-token", + "version": "v2.2.1", + "sha": "29824e69f54612133e76f7eaac726eef6c875baf" + }, + "actions/download-artifact@v6.0.0": { + "repo": "actions/download-artifact", + "version": "v6.0.0", + "sha": "018cc2cf5baa6db3ef3c5f8a56943fffe632ef53" + }, + "actions/github-script@v7.0.1": { + "repo": "actions/github-script", + "version": "v7.1.0", + "sha": "f28e40c7f34bde8b3046d885e986cb6290c5673b" + }, + "actions/github-script@v8.0.0": { + "repo": "actions/github-script", + "version": "v8.0.0", + "sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd" + }, + "actions/setup-dotnet@v4": { + "repo": "actions/setup-dotnet", + "version": "v4.3.1", + "sha": "67a3573c9a986a3f9c594539f4ab511d57bb3ce9" + }, + "actions/setup-go@v6": { + "repo": "actions/setup-go", + "version": "v6", + "sha": "7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5" + }, + "actions/setup-go@v6.1.0": { + "repo": "actions/setup-go", + "version": "v6.1.0", + "sha": "4dc6199c7b1a012772edbd06daecab0f50c9053c" + }, + "actions/setup-java@v4": { + "repo": "actions/setup-java", + "version": "v4.8.0", + "sha": "c1e323688fd81a25caa38c78aa6df2d33d3e20d9" + }, + "actions/setup-node@v6": { + "repo": "actions/setup-node", + "version": "v6", + "sha": "6044e13b5dc448c55e2357c09f80417699197238" + }, + "actions/setup-node@v6.1.0": { + "repo": "actions/setup-node", + "version": "v6.1.0", + "sha": "395ad3262231945c25e8478fd5baf05154b1d79f" + }, + "actions/setup-python@v5.6.0": { + "repo": "actions/setup-python", + "version": "v5.6.0", + "sha": "a26af69be951a213d495a4c3e4e4022e16d87065" + }, + "actions/upload-artifact@v4": { + "repo": "actions/upload-artifact", + "version": "v4.6.2", + "sha": "ea165f8d65b6e75b540449e92b4886f43607fa02" + }, + "actions/upload-artifact@v5.0.0": { + "repo": "actions/upload-artifact", + "version": "v5.0.0", + "sha": "330a01c490aca151604b8cf639adc76d48f6c5d4" + }, + "actions/upload-artifact@v6.0.0": { + "repo": "actions/upload-artifact", + "version": "v6.0.0", + "sha": "b7c566a772e6b6bfb58ed0dc250532a479d7789f" + }, + "anchore/sbom-action@v0.20.10": { + "repo": "anchore/sbom-action", + "version": "v0.20.10", + "sha": "fbfd9c6c189226748411491745178e0c2017392d" + }, + "anchore/sbom-action@v0.20.11": { + "repo": "anchore/sbom-action", + "version": "v0.20.11", + "sha": "43a17d6e7add2b5535efe4dcae9952337c479a93" + }, + "astral-sh/setup-uv@v5.4.2": { + "repo": "astral-sh/setup-uv", + "version": "v5.4.2", + "sha": "d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86" + }, + "cli/gh-extension-precompile@v2.1.0": { + "repo": "cli/gh-extension-precompile", + "version": "v2.1.0", + "sha": "9e2237c30f869ad3bcaed6a4be2cd43564dd421b" + }, + "denoland/setup-deno@v2": { + "repo": "denoland/setup-deno", + "version": "v2.0.3", + "sha": "e95548e56dfa95d4e1a28d6f422fafe75c4c26fb" + }, + "docker/build-push-action@v6": { + "repo": "docker/build-push-action", + "version": "v6", + "sha": "263435318d21b8e681c14492fe198d362a7d2c83" + }, + "docker/login-action@v3": { + "repo": "docker/login-action", + "version": "v3", + "sha": "5e57cd118135c172c3672efd75eb46360885c0ef" + }, + "docker/setup-buildx-action@v3": { + "repo": "docker/setup-buildx-action", + "version": "v3", + "sha": "8d2750c68a42422c14e847fe6c8ac0403b4cbd6f" + }, + "erlef/setup-beam@v1": { + "repo": "erlef/setup-beam", + "version": "v1.20.4", + "sha": "dff508cca8ce57162e7aa6c4769a4f97c2fed638" + }, + "github/codeql-action/upload-sarif@v3": { + "repo": "github/codeql-action/upload-sarif", + "version": "v3.31.9", + "sha": "70c165ac82ca0e33a10e9741508dd0ccb4dcf080" + }, + "github/stale-repos@v3": { + "repo": "github/stale-repos", + "version": "v3", + "sha": "3477b6488008d9411aaf22a0924ec7c1f6a69980" + }, + "github/stale-repos@v3.0.2": { + "repo": "github/stale-repos", + "version": "v3.0.2", + "sha": "a21e55567b83cf3c3f3f9085d3038dc6cee02598" + }, + "haskell-actions/setup@v2": { + "repo": "haskell-actions/setup", + "version": "v2.9.1", + "sha": "55073cbd0e96181a9abd6ff4e7d289867dffc98d" + }, + "oven-sh/setup-bun@v2": { + "repo": "oven-sh/setup-bun", + "version": "v2.0.2", + "sha": "735343b667d3e6f658f44d0eca948eb6282f2b76" + }, + "ruby/setup-ruby@v1": { + "repo": "ruby/setup-ruby", + "version": "v1.275.0", + "sha": "d354de180d0c9e813cfddfcbdc079945d4be589b" + }, + "super-linter/super-linter@v8.2.1": { + "repo": "super-linter/super-linter", + "version": "v8.2.1", + "sha": "2bdd90ed3262e023ac84bf8fe35dc480721fc1f2" + }, + "super-linter/super-linter@v8.3.1": { + "repo": "super-linter/super-linter", + "version": "v8.3.1", + "sha": "47984f49b4e87383eed97890fe2dca6063bbd9c3" + } + } }