diff --git a/pkg/parser/schema_location_test.go b/pkg/parser/schema_location_test.go index 80d7f68c673..d6db6194bba 100644 --- a/pkg/parser/schema_location_test.go +++ b/pkg/parser/schema_location_test.go @@ -199,3 +199,77 @@ func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation(t *testing.T) { }) } } + +func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AdditionalProperties(t *testing.T) { + tests := []struct { + name string + frontmatter map[string]any + filePath string + wantErr bool + errContains string + }{ + { + name: "invalid permissions with additional property shows location", + frontmatter: map[string]any{ + "on": "push", + "permissions": map[string]any{ + "contents": "read", + "invalid_perm": "write", + }, + }, + filePath: "/test/workflow.md", + wantErr: true, + errContains: "/test/workflow.md:1:1:", + }, + { + name: "invalid trigger with additional property shows location", + frontmatter: map[string]any{ + "on": map[string]any{ + "push": map[string]any{ + "branches": []string{"main"}, + "invalid_prop": "value", + }, + }, + }, + filePath: "/test/workflow.md", + wantErr: true, + errContains: "/test/workflow.md:1:1:", + }, + { + name: "invalid tools configuration with additional property shows location", + frontmatter: map[string]any{ + "tools": map[string]any{ + "github": map[string]any{ + "allowed": []string{"create_issue"}, + "invalid_prop": "value", + }, + }, + }, + filePath: "/test/workflow.md", + wantErr: true, + errContains: "/test/workflow.md:1:1:", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(tt.frontmatter, tt.filePath) + + if tt.wantErr && err == nil { + t.Errorf("ValidateMainWorkflowFrontmatterWithSchemaAndLocation() expected error, got nil") + return + } + + if !tt.wantErr && err != nil { + t.Errorf("ValidateMainWorkflowFrontmatterWithSchemaAndLocation() error = %v", err) + return + } + + if tt.wantErr && err != nil && tt.errContains != "" { + if !strings.Contains(err.Error(), tt.errContains) { + t.Errorf("ValidateMainWorkflowFrontmatterWithSchemaAndLocation() error = %v, expected to contain %v", err, tt.errContains) + } + } + }) + } +} diff --git a/pkg/parser/schema_test.go b/pkg/parser/schema_test.go index d0555681510..21dd4a089e8 100644 --- a/pkg/parser/schema_test.go +++ b/pkg/parser/schema_test.go @@ -279,6 +279,197 @@ func TestValidateMainWorkflowFrontmatterWithSchema(t *testing.T) { wantErr: true, errContains: "missing property 'key'", }, + // Test cases for additional properties validation + { + name: "invalid permissions with additional property", + frontmatter: map[string]any{ + "on": "push", + "permissions": map[string]any{ + "contents": "read", + "invalid_perm": "write", + }, + }, + wantErr: true, + errContains: "additional properties 'invalid_perm' not allowed", + }, + { + name: "invalid on trigger with additional properties", + frontmatter: map[string]any{ + "on": map[string]any{ + "push": map[string]any{ + "branches": []string{"main"}, + "invalid_prop": "value", + }, + }, + }, + wantErr: true, + errContains: "additional properties 'invalid_prop' not allowed", + }, + { + name: "invalid schedule with additional properties", + frontmatter: map[string]any{ + "on": map[string]any{ + "schedule": []map[string]any{ + { + "cron": "0 9 * * *", + "invalid_prop": "value", + }, + }, + }, + }, + wantErr: true, + errContains: "additional properties 'invalid_prop' not allowed", + }, + { + name: "invalid workflow_dispatch with additional properties", + frontmatter: map[string]any{ + "on": map[string]any{ + "workflow_dispatch": map[string]any{ + "inputs": map[string]any{ + "test_input": map[string]any{ + "description": "Test input", + "type": "string", + }, + }, + "invalid_prop": "value", + }, + }, + }, + wantErr: true, + errContains: "additional properties 'invalid_prop' not allowed", + }, + { + name: "invalid concurrency with additional properties", + frontmatter: map[string]any{ + "concurrency": map[string]any{ + "group": "test-group", + "cancel-in-progress": true, + "invalid_prop": "value", + }, + }, + wantErr: true, + errContains: "additional properties 'invalid_prop' not allowed", + }, + { + name: "invalid runs-on object with additional properties", + frontmatter: map[string]any{ + "runs-on": map[string]any{ + "group": "test-group", + "labels": []string{"ubuntu-latest"}, + "invalid_prop": "value", + }, + }, + wantErr: true, + errContains: "additional properties 'invalid_prop' not allowed", + }, + { + name: "invalid github tools with additional properties", + frontmatter: map[string]any{ + "tools": map[string]any{ + "github": map[string]any{ + "allowed": []string{"create_issue"}, + "invalid_prop": "value", + }, + }, + }, + wantErr: true, + errContains: "additional properties 'invalid_prop' not allowed", + }, + { + name: "invalid claude tools with additional properties", + frontmatter: map[string]any{ + "tools": map[string]any{ + "claude": map[string]any{ + "allowed": []string{"WebFetch"}, + "invalid_prop": "value", + }, + }, + }, + wantErr: true, + errContains: "additional properties 'invalid_prop' not allowed", + }, + { + name: "invalid custom tool with additional properties", + frontmatter: map[string]any{ + "tools": map[string]any{ + "customTool": map[string]any{ + "allowed": []string{"function1"}, + "mcp": map[string]any{ + "type": "stdio", + "command": "my-tool", + }, + "invalid_prop": "value", + }, + }, + }, + wantErr: true, + errContains: "additional properties 'invalid_prop' not allowed", + }, + { + name: "invalid claude configuration with additional properties", + frontmatter: map[string]any{ + "claude": map[string]any{ + "model": "claude-3", + "invalid_prop": "value", + }, + }, + wantErr: true, + errContains: "additional properties 'invalid_prop' not allowed", + }, + { + name: "invalid output configuration with additional properties", + frontmatter: map[string]any{ + "output": map[string]any{ + "issue": map[string]any{ + "title-prefix": "[ai] ", + "invalid_prop": "value", + }, + }, + }, + wantErr: true, + errContains: "additional properties 'invalid_prop' not allowed", + }, + { + name: "valid new GitHub Actions properties - permissions with new properties", + frontmatter: map[string]any{ + "on": "push", + "permissions": map[string]any{ + "contents": "read", + "attestations": "write", + "id-token": "write", + "packages": "read", + "pages": "write", + "repository-projects": "none", + }, + }, + wantErr: false, + }, + { + name: "valid GitHub Actions defaults property", + frontmatter: map[string]any{ + "on": "push", + "defaults": map[string]any{ + "run": map[string]any{ + "shell": "bash", + "working-directory": "/app", + }, + }, + }, + wantErr: false, + }, + { + name: "invalid defaults with additional properties", + frontmatter: map[string]any{ + "defaults": map[string]any{ + "run": map[string]any{ + "shell": "bash", + "invalid_prop": "value", + }, + }, + }, + wantErr: true, + errContains: "additional properties 'invalid_prop' not allowed", + }, } for _, tt := range tests { diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 02f3dea895e..9c0b717cba3 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -39,7 +39,52 @@ }, "push": { "description": "Push event trigger", - "additionalProperties": true + "type": "object", + "additionalProperties": false, + "properties": { + "branches": { + "type": "array", + "description": "Branches to filter on", + "items": { + "type": "string" + } + }, + "branches-ignore": { + "type": "array", + "description": "Branches to ignore", + "items": { + "type": "string" + } + }, + "paths": { + "type": "array", + "description": "Paths to filter on", + "items": { + "type": "string" + } + }, + "paths-ignore": { + "type": "array", + "description": "Paths to ignore", + "items": { + "type": "string" + } + }, + "tags": { + "type": "array", + "description": "Tags to filter on", + "items": { + "type": "string" + } + }, + "tags-ignore": { + "type": "array", + "description": "Tags to ignore", + "items": { + "type": "string" + } + } + } }, "pull_request": { "description": "Pull request event trigger", @@ -85,15 +130,37 @@ "description": "Filter by draft state. Set to false to ignore draft PRs" } }, - "additionalProperties": true + "additionalProperties": false }, "issues": { "description": "Issues event trigger", - "additionalProperties": true + "type": "object", + "additionalProperties": false, + "properties": { + "types": { + "type": "array", + "description": "Types of issue events", + "items": { + "type": "string", + "enum": ["opened", "edited", "deleted", "transferred", "pinned", "unpinned", "closed", "reopened", "assigned", "unassigned", "labeled", "unlabeled", "locked", "unlocked", "milestoned", "demilestoned"] + } + } + } }, "issue_comment": { "description": "Issue comment event trigger", - "additionalProperties": true + "type": "object", + "additionalProperties": false, + "properties": { + "types": { + "type": "array", + "description": "Types of issue comment events", + "items": { + "type": "string", + "enum": ["created", "edited", "deleted"] + } + } + } }, "schedule": { "type": "array", @@ -106,79 +173,204 @@ "description": "Cron expression for schedule" } }, - "additionalProperties": true + "required": ["cron"], + "additionalProperties": false } }, "workflow_dispatch": { "description": "Manual workflow dispatch trigger", - "additionalProperties": true + "oneOf": [ + { + "type": "null", + "description": "Simple workflow dispatch trigger" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "inputs": { + "type": "object", + "description": "Input parameters for manual dispatch", + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "properties": { + "description": { + "type": "string", + "description": "Input description" + }, + "required": { + "type": "boolean", + "description": "Whether input is required" + }, + "default": { + "type": "string", + "description": "Default value" + }, + "type": { + "type": "string", + "enum": ["string", "choice", "boolean"], + "description": "Input type" + }, + "options": { + "type": "array", + "description": "Options for choice type", + "items": { + "type": "string" + } + } + } + } + } + } + } + ] }, "workflow_run": { "description": "Workflow run trigger", - "additionalProperties": true + "type": "object", + "additionalProperties": false, + "properties": { + "workflows": { + "type": "array", + "description": "List of workflows to trigger on", + "items": { + "type": "string" + } + }, + "types": { + "type": "array", + "description": "Types of workflow run events", + "items": { + "type": "string", + "enum": ["completed", "requested"] + } + }, + "branches": { + "type": "array", + "description": "Branches to filter on", + "items": { + "type": "string" + } + }, + "branches-ignore": { + "type": "array", + "description": "Branches to ignore", + "items": { + "type": "string" + } + } + } }, "release": { "description": "Release event trigger", - "additionalProperties": true + "type": "object", + "additionalProperties": false, + "properties": { + "types": { + "type": "array", + "description": "Types of release events", + "items": { + "type": "string", + "enum": ["published", "unpublished", "created", "edited", "deleted", "prereleased", "released"] + } + } + } + }, + "pull_request_review_comment": { + "description": "Pull request review comment event trigger", + "type": "object", + "additionalProperties": false, + "properties": { + "types": { + "type": "array", + "description": "Types of pull request review comment events", + "items": { + "type": "string", + "enum": ["created", "edited", "deleted"] + } + } + } } }, - "additionalProperties": true + "additionalProperties": false } ] }, "permissions": { - "description": "Permissions for the workflow", + "description": "You can modify the default permissions granted to the GITHUB_TOKEN, adding or removing access as required, so that you only allow the minimum required access.", "oneOf": [ { "type": "string", + "enum": ["read-all", "write-all", "read", "write"], "description": "Simple permissions string" }, { "type": "object", "description": "Detailed permissions object", + "additionalProperties": false, "properties": { - "contents": { + "actions": { "type": "string", "enum": ["read", "write", "none"] }, - "issues": { - "type": "string", + "attestations": { + "type": "string", "enum": ["read", "write", "none"] }, - "pull-requests": { + "checks": { "type": "string", "enum": ["read", "write", "none"] }, - "discussions": { + "contents": { "type": "string", "enum": ["read", "write", "none"] }, - "actions": { + "deployments": { "type": "string", "enum": ["read", "write", "none"] }, - "checks": { + "discussions": { "type": "string", "enum": ["read", "write", "none"] }, - "statuses": { + "id-token": { "type": "string", "enum": ["read", "write", "none"] }, + "issues": { + "type": "string", + "enum": ["read", "write", "none"] + }, "models": { + "type": "string", + "enum": ["read", "none"] + }, + "packages": { "type": "string", "enum": ["read", "write", "none"] }, - "deployments": { + "pages": { + "type": "string", + "enum": ["read", "write", "none"] + }, + "pull-requests": { + "type": "string", + "enum": ["read", "write", "none"] + }, + "repository-projects": { "type": "string", "enum": ["read", "write", "none"] }, "security-events": { "type": "string", "enum": ["read", "write", "none"] + }, + "statuses": { + "type": "string", + "enum": ["read", "write", "none"] } - }, - "additionalProperties": true + } } ] }, @@ -186,6 +378,139 @@ "type": "string", "description": "Custom run name for the workflow" }, + "defaults": { + "type": "object", + "description": "Default settings that will apply to all jobs in the workflow", + "additionalProperties": false, + "properties": { + "run": { + "type": "object", + "description": "Default shell and working directory", + "additionalProperties": false, + "properties": { + "shell": { + "type": "string", + "description": "Default shell for run steps" + }, + "working-directory": { + "type": "string", + "description": "Default working directory for run steps" + } + } + } + } + }, + "jobs": { + "type": "object", + "description": "Groups together all the jobs that run in the workflow", + "additionalProperties": { + "type": "object", + "description": "Job definition", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Name of the job" + }, + "runs-on": { + "oneOf": [ + { + "type": "string", + "description": "Runner type as string" + }, + { + "type": "array", + "description": "Runner type as array", + "items": { + "type": "string" + } + }, + { + "type": "object", + "description": "Runner type as object", + "additionalProperties": false + } + ] + }, + "steps": { + "type": "array", + "description": "Job steps", + "items": { + "type": "object", + "additionalProperties": false + } + }, + "if": { + "type": "string", + "description": "Conditional execution for the job" + }, + "needs": { + "oneOf": [ + { + "type": "string", + "description": "Single job dependency" + }, + { + "type": "array", + "description": "Multiple job dependencies", + "items": { + "type": "string" + } + } + ] + }, + "env": { + "type": "object", + "description": "Environment variables for the job", + "additionalProperties": { + "type": "string" + } + }, + "permissions": { + "$ref": "#/properties/permissions" + }, + "timeout-minutes": { + "type": "integer", + "description": "Job timeout in minutes" + }, + "strategy": { + "type": "object", + "description": "Matrix strategy for the job", + "additionalProperties": false + }, + "continue-on-error": { + "type": "boolean", + "description": "Continue workflow on job failure" + }, + "container": { + "type": "object", + "description": "Container to run the job in", + "additionalProperties": false + }, + "services": { + "type": "object", + "description": "Service containers for the job", + "additionalProperties": { + "type": "object", + "additionalProperties": false + } + }, + "outputs": { + "type": "object", + "description": "Job outputs", + "additionalProperties": { + "type": "string" + } + }, + "defaults": { + "$ref": "#/properties/defaults" + }, + "concurrency": { + "$ref": "#/properties/concurrency" + } + } + } + }, "runs-on": { "oneOf": [ { @@ -202,7 +527,20 @@ { "type": "object", "description": "Runner type as object", - "additionalProperties": true + "additionalProperties": false, + "properties": { + "group": { + "type": "string", + "description": "Runner group" + }, + "labels": { + "type": "array", + "description": "Runner labels", + "items": { + "type": "string" + } + } + } } ] }, @@ -219,7 +557,18 @@ { "type": "object", "description": "Concurrency configuration object", - "additionalProperties": true + "additionalProperties": false, + "properties": { + "group": { + "type": "string", + "description": "Concurrency group name" + }, + "cancel-in-progress": { + "type": "boolean", + "description": "Cancel in-progress jobs in the same concurrency group" + } + }, + "required": ["group"] } ] }, @@ -243,11 +592,49 @@ }, "steps": { "description": "Custom workflow steps", - "additionalProperties": true + "oneOf": [ + { + "type": "object", + "additionalProperties": true + }, + { + "type": "array", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "additionalProperties": true + } + ] + } + } + ] }, "post-steps": { "description": "Custom workflow steps to run after AI execution", - "additionalProperties": true + "oneOf": [ + { + "type": "object", + "additionalProperties": true + }, + { + "type": "array", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "additionalProperties": true + } + ] + } + } + ] }, "engine": { "description": "AI engine configuration", @@ -282,7 +669,45 @@ }, "claude": { "description": "Claude-specific configuration", - "additionalProperties": true + "type": "object", + "additionalProperties": false, + "properties": { + "model": { + "type": "string", + "description": "Claude model to use" + }, + "version": { + "type": "string", + "description": "Claude version" + }, + "allowed": { + "description": "Allowed Claude tools", + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + } + ] + } + } }, "tools": { "type": "object", @@ -305,9 +730,17 @@ "items": { "type": "string" } + }, + "use_docker_mcp": { + "type": "boolean", + "description": "Whether to use Docker MCP for GitHub tools" + }, + "docker_image_version": { + "type": "string", + "description": "Docker image version for GitHub MCP server" } }, - "additionalProperties": true + "additionalProperties": false } ] }, @@ -333,12 +766,24 @@ }, { "type": "object", - "additionalProperties": true + "additionalProperties": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } } ] } }, - "additionalProperties": true + "additionalProperties": false } ] } @@ -366,7 +811,7 @@ } } }, - "additionalProperties": true + "additionalProperties": false } ] }