diff --git a/pkg/parser/schema_test.go b/pkg/parser/schema_test.go index e082085b430..515c8cd9a4f 100644 --- a/pkg/parser/schema_test.go +++ b/pkg/parser/schema_test.go @@ -2703,3 +2703,32 @@ func TestValidateMainWorkflowFrontmatter_OnPermissionsUnknownScopeRejected(t *te t.Error("unknown scope in on.permissions should be rejected by schema validation") } } + +func TestValidateMainWorkflowFrontmatter_JobsInputsRejectedBeforeSchema(t *testing.T) { + frontmatter := map[string]any{ + "on": "push", + "engine": "copilot", + "jobs": map[string]any{ + "my-job": map[string]any{ + "runs-on": "ubuntu-latest", + "inputs": map[string]any{ + "name": map[string]any{"description": "test"}, + }, + "steps": []any{map[string]any{"run": "echo hi"}}, + }, + }, + } + + err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/tmp/gh-aw/jobs-inputs-pre-schema-test.md") + if err == nil { + t.Fatal("expected jobs..inputs validation error") + } + + errMsg := err.Error() + if !strings.Contains(errMsg, "jobs.my-job.inputs: inputs are not supported on jobs") { + t.Fatalf("expected actionable jobs.inputs error, got: %v", err) + } + if strings.Contains(errMsg, "Unknown property: inputs") { + t.Fatalf("expected pre-schema validation error instead of schema unknown-property error, got: %v", err) + } +} diff --git a/pkg/parser/schema_triggers.go b/pkg/parser/schema_triggers.go index 2cdecf150ce..14aa5c9b8ba 100644 --- a/pkg/parser/schema_triggers.go +++ b/pkg/parser/schema_triggers.go @@ -68,6 +68,30 @@ func validateCommandTriggerConflicts(frontmatter map[string]any) error { return nil } +func validateUnsupportedJobInputs(frontmatter map[string]any) error { + jobsValue, hasJobs := frontmatter["jobs"] + if !hasJobs || jobsValue == nil { + return nil + } + + jobsMap, ok := jobsValue.(map[string]any) + if !ok { + return nil + } + + for jobName, jobValue := range jobsMap { + jobMap, ok := jobValue.(map[string]any) + if !ok { + continue + } + if _, hasInputs := jobMap["inputs"]; hasInputs { + return fmt.Errorf("jobs.%s.inputs: inputs are not supported on jobs; use 'env' to pass values to job steps", jobName) + } + } + + return nil +} + // IsLabelOnlyEvent checks if an event configuration only contains labeled/unlabeled types // This is exported for use in the compiler to validate command trigger combinations func IsLabelOnlyEvent(eventValue any) bool { diff --git a/pkg/parser/schema_validation.go b/pkg/parser/schema_validation.go index 56b04d3bfb1..1f18f4de222 100644 --- a/pkg/parser/schema_validation.go +++ b/pkg/parser/schema_validation.go @@ -121,6 +121,9 @@ func ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter map[string if err := validateCommandTriggerConflicts(filtered); err != nil { return err } + if err := validateUnsupportedJobInputs(filtered); err != nil { + return err + } // Then run the standard schema validation with location if err := validateWithSchemaAndLocation(filtered, mainWorkflowSchema, "main workflow file", filePath); err != nil { diff --git a/pkg/workflow/compiler_custom_jobs.go b/pkg/workflow/compiler_custom_jobs.go index ec80f26e5c5..5bc343abff2 100644 --- a/pkg/workflow/compiler_custom_jobs.go +++ b/pkg/workflow/compiler_custom_jobs.go @@ -175,6 +175,10 @@ func (c *Compiler) extractCustomJobProperties(job *Job, jobName string, configMa } func (c *Compiler) extractCustomJobCoreProperties(job *Job, jobName string, configMap map[string]any) error { + if _, hasInputs := configMap["inputs"]; hasInputs { + return fmt.Errorf("jobs.%s.inputs: inputs are not supported on jobs; use 'env' to pass values to job steps", jobName) + } + if err := c.extractCustomJobRunsOn(job, jobName, configMap); err != nil { return err } diff --git a/pkg/workflow/compiler_custom_jobs_test.go b/pkg/workflow/compiler_custom_jobs_test.go index 45e2066a72c..3c52ec8b2c2 100644 --- a/pkg/workflow/compiler_custom_jobs_test.go +++ b/pkg/workflow/compiler_custom_jobs_test.go @@ -202,6 +202,33 @@ func TestBuildCustomJob_InvalidTimeoutMinutesError(t *testing.T) { require.ErrorContains(t, err, "timeout-minutes") } +func TestBuildCustomJob_InputsNotSupportedError(t *testing.T) { + compiler := NewCompiler() + compiler.jobManager = NewJobManager() + + data := &WorkflowData{Name: "Test"} + configMap := map[string]any{ + "runs-on": "ubuntu-latest", + "inputs": map[string]any{ + "my-input": map[string]any{"description": "an input"}, + }, + "steps": []any{map[string]any{"run": "echo hello"}}, + } + + _, err := compiler.buildCustomJob( + "my-job", + configMap, + data, + false, + map[string]struct{}{}, + map[string]struct{}{}, + ) + + require.Error(t, err) + require.ErrorContains(t, err, "jobs.my-job.inputs") + require.ErrorContains(t, err, "inputs are not supported on jobs") +} + func TestBuildCustomJob_UsesReusableWorkflow(t *testing.T) { compiler := NewCompiler() compiler.jobManager = NewJobManager() diff --git a/pkg/workflow/compiler_string_api_test.go b/pkg/workflow/compiler_string_api_test.go index fe0d0737c57..4826d4772ad 100644 --- a/pkg/workflow/compiler_string_api_test.go +++ b/pkg/workflow/compiler_string_api_test.go @@ -187,6 +187,37 @@ bogus-field: true assert.Contains(t, errorStr, "virtual/workflow.md:3:1: error:") } +func TestParseWorkflowString_JobsInputsValidationReportedBeforeSchemaErrors(t *testing.T) { + markdown := `--- +name: jobs-inputs-test +on: push +engine: copilot +jobs: + my-job: + runs-on: ubuntu-latest + inputs: + greeting: + description: Greeting + steps: + - run: echo hi +--- + +# Test +` + + compiler := NewCompiler( + WithNoEmit(true), + WithSkipValidation(true), + ) + + _, err := compiler.ParseWorkflowString(markdown, "virtual/workflow.md") + require.Error(t, err) + + errorStr := err.Error() + assert.Contains(t, errorStr, "jobs.my-job.inputs: inputs are not supported on jobs") + assert.NotContains(t, errorStr, "Unknown property: inputs") +} + func TestCompileToYAML_BasicCompilation(t *testing.T) { markdown := `--- name: compile-test