diff --git a/.changeset/patch-consolidate-validation-functions.md b/.changeset/patch-consolidate-validation-functions.md new file mode 100644 index 0000000000..006df38434 --- /dev/null +++ b/.changeset/patch-consolidate-validation-functions.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Consolidate generic validation functions into validation.go diff --git a/.github/instructions/github-agentic-workflows.instructions.md b/.github/instructions/github-agentic-workflows.instructions.md index 3d78fb5a2e..001e02741c 100644 --- a/.github/instructions/github-agentic-workflows.instructions.md +++ b/.github/instructions/github-agentic-workflows.instructions.md @@ -55,13 +55,6 @@ The YAML frontmatter supports these fields: - **`steps:`** - Custom workflow steps (object) - **`post-steps:`** - Custom workflow steps to run after AI execution (object) -### Documentation Fields (Silently Ignored) - -- **`description:`** - Workflow description (string, for documentation only) -- **`applyTo:`** - File pattern scope (string, for documentation only) - -These fields are ignored during compilation and do not affect workflow behavior. Use them to document workflow purpose and scope. - ### Agentic Workflow Specific Fields - **`engine:`** - AI processor configuration diff --git a/pkg/workflow/cache.go b/pkg/workflow/cache.go index a76897d62e..a899c464ff 100644 --- a/pkg/workflow/cache.go +++ b/pkg/workflow/cache.go @@ -28,18 +28,6 @@ func generateDefaultCacheKey(cacheID string) string { return fmt.Sprintf("memory-%s-${{ github.workflow }}-${{ github.run_id }}", cacheID) } -// validateNoDuplicateCacheIDs checks for duplicate cache IDs and returns an error if found -func validateNoDuplicateCacheIDs(caches []CacheMemoryEntry) error { - seen := make(map[string]bool) - for _, cache := range caches { - if seen[cache.ID] { - return fmt.Errorf("duplicate cache-memory ID '%s' found. Each cache must have a unique ID", cache.ID) - } - seen[cache.ID] = true - } - return nil -} - // extractCacheMemoryConfig extracts cache-memory configuration from tools section func (c *Compiler) extractCacheMemoryConfig(tools map[string]any) (*CacheMemoryConfig, error) { cacheMemoryValue, exists := tools["cache-memory"] diff --git a/pkg/workflow/compiler.go b/pkg/workflow/compiler.go index d49cea1223..0fcf30a9a3 100644 --- a/pkg/workflow/compiler.go +++ b/pkg/workflow/compiler.go @@ -16,7 +16,6 @@ import ( "github.com/githubnext/gh-aw/pkg/parser" "github.com/githubnext/gh-aw/pkg/workflow/pretty" "github.com/goccy/go-yaml" - "github.com/santhosh-tekuri/jsonschema/v6" ) var log = logger.New("workflow:compiler") @@ -421,52 +420,6 @@ func (c *Compiler) CompileWorkflow(markdownPath string) error { return nil } -// validateGitHubActionsSchema validates the generated YAML content against the GitHub Actions workflow schema -func (c *Compiler) validateGitHubActionsSchema(yamlContent string) error { - // Convert YAML to any for JSON conversion - var workflowData any - if err := yaml.Unmarshal([]byte(yamlContent), &workflowData); err != nil { - return fmt.Errorf("failed to parse YAML for schema validation: %w", err) - } - - // Convert to JSON for schema validation - jsonData, err := json.Marshal(workflowData) - if err != nil { - return fmt.Errorf("failed to convert YAML to JSON for validation: %w", err) - } - - // Parse the embedded schema - var schemaDoc any - if err := json.Unmarshal([]byte(githubWorkflowSchema), &schemaDoc); err != nil { - return fmt.Errorf("failed to parse embedded GitHub Actions schema: %w", err) - } - - // Create compiler and add the schema as a resource - loader := jsonschema.NewCompiler() - schemaURL := "https://json.schemastore.org/github-workflow.json" - if err := loader.AddResource(schemaURL, schemaDoc); err != nil { - return fmt.Errorf("failed to add schema resource: %w", err) - } - - // Compile the schema - schema, err := loader.Compile(schemaURL) - if err != nil { - return fmt.Errorf("failed to compile GitHub Actions schema: %w", err) - } - - // Validate the JSON data against the schema - var jsonObj any - if err := json.Unmarshal(jsonData, &jsonObj); err != nil { - return fmt.Errorf("failed to unmarshal JSON for validation: %w", err) - } - - if err := schema.Validate(jsonObj); err != nil { - return fmt.Errorf("GitHub Actions schema validation failed: %w", err) - } - - return nil -} - // ParseWorkflowFile parses a markdown workflow file and extracts all necessary data func (c *Compiler) ParseWorkflowFile(markdownPath string) (*WorkflowData, error) { log.Printf("Reading file: %s", markdownPath) diff --git a/pkg/workflow/redact_secrets.go b/pkg/workflow/redact_secrets.go index f0ff322211..4754b27ad7 100644 --- a/pkg/workflow/redact_secrets.go +++ b/pkg/workflow/redact_secrets.go @@ -92,17 +92,3 @@ func (c *Compiler) generateSecretRedactionStep(yaml *strings.Builder, yamlConten yaml.WriteString(fmt.Sprintf(" SECRET_%s: ${{ secrets.%s }}\n", escapedSecretName, secretName)) } } - -// validateSecretReferences validates that secret references are valid -func validateSecretReferences(secrets []string) error { - // Secret names must be valid environment variable names - secretNamePattern := regexp.MustCompile(`^[A-Z][A-Z0-9_]*$`) - - for _, secret := range secrets { - if !secretNamePattern.MatchString(secret) { - return fmt.Errorf("invalid secret name: %s", secret) - } - } - - return nil -} diff --git a/pkg/workflow/validation.go b/pkg/workflow/validation.go index 69c8498377..ab822dec01 100644 --- a/pkg/workflow/validation.go +++ b/pkg/workflow/validation.go @@ -1,14 +1,18 @@ package workflow import ( + "encoding/json" "errors" "fmt" "os" + "regexp" "strings" "github.com/githubnext/gh-aw/pkg/console" "github.com/githubnext/gh-aw/pkg/logger" "github.com/githubnext/gh-aw/pkg/workflow/pretty" + "github.com/goccy/go-yaml" + "github.com/santhosh-tekuri/jsonschema/v6" ) var validationLog = logger.New("workflow:validation") @@ -194,3 +198,75 @@ func collectPackagesFromWorkflow( return packages } + +// validateGitHubActionsSchema validates the generated YAML content against the GitHub Actions workflow schema +func (c *Compiler) validateGitHubActionsSchema(yamlContent string) error { + // Convert YAML to any for JSON conversion + var workflowData any + if err := yaml.Unmarshal([]byte(yamlContent), &workflowData); err != nil { + return fmt.Errorf("failed to parse YAML for schema validation: %w", err) + } + + // Convert to JSON for schema validation + jsonData, err := json.Marshal(workflowData) + if err != nil { + return fmt.Errorf("failed to convert YAML to JSON for validation: %w", err) + } + + // Parse the embedded schema + var schemaDoc any + if err := json.Unmarshal([]byte(githubWorkflowSchema), &schemaDoc); err != nil { + return fmt.Errorf("failed to parse embedded GitHub Actions schema: %w", err) + } + + // Create compiler and add the schema as a resource + loader := jsonschema.NewCompiler() + schemaURL := "https://json.schemastore.org/github-workflow.json" + if err := loader.AddResource(schemaURL, schemaDoc); err != nil { + return fmt.Errorf("failed to add schema resource: %w", err) + } + + // Compile the schema + schema, err := loader.Compile(schemaURL) + if err != nil { + return fmt.Errorf("failed to compile GitHub Actions schema: %w", err) + } + + // Validate the JSON data against the schema + var jsonObj any + if err := json.Unmarshal(jsonData, &jsonObj); err != nil { + return fmt.Errorf("failed to unmarshal JSON for validation: %w", err) + } + + if err := schema.Validate(jsonObj); err != nil { + return fmt.Errorf("GitHub Actions schema validation failed: %w", err) + } + + return nil +} + +// validateNoDuplicateCacheIDs checks for duplicate cache IDs and returns an error if found +func validateNoDuplicateCacheIDs(caches []CacheMemoryEntry) error { + seen := make(map[string]bool) + for _, cache := range caches { + if seen[cache.ID] { + return fmt.Errorf("duplicate cache-memory ID '%s' found. Each cache must have a unique ID", cache.ID) + } + seen[cache.ID] = true + } + return nil +} + +// validateSecretReferences validates that secret references are valid +func validateSecretReferences(secrets []string) error { + // Secret names must be valid environment variable names + secretNamePattern := regexp.MustCompile(`^[A-Z][A-Z0-9_]*$`) + + for _, secret := range secrets { + if !secretNamePattern.MatchString(secret) { + return fmt.Errorf("invalid secret name: %s", secret) + } + } + + return nil +}