Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/patch-cache-schema-compilation.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

75 changes: 70 additions & 5 deletions pkg/parser/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"regexp"
"sort"
"strings"
"sync"

"github.com/githubnext/gh-aw/pkg/console"
"github.com/githubnext/gh-aw/pkg/constants"
Expand Down Expand Up @@ -120,24 +121,88 @@ func ValidateMCPConfigWithSchema(mcpConfig map[string]any, toolName string) erro
}

// validateWithSchema validates frontmatter against a JSON schema
func validateWithSchema(frontmatter map[string]any, schemaJSON, context string) error {
// Cached compiled schemas to avoid recompiling on every validation
var (
mainWorkflowSchemaOnce sync.Once
includedFileSchemaOnce sync.Once
mcpConfigSchemaOnce sync.Once

compiledMainWorkflowSchema *jsonschema.Schema
compiledIncludedFileSchema *jsonschema.Schema
compiledMcpConfigSchema *jsonschema.Schema

mainWorkflowSchemaError error
includedFileSchemaError error
mcpConfigSchemaError error
)

// getCompiledMainWorkflowSchema returns the compiled main workflow schema, compiling it once and caching
func getCompiledMainWorkflowSchema() (*jsonschema.Schema, error) {
mainWorkflowSchemaOnce.Do(func() {
compiledMainWorkflowSchema, mainWorkflowSchemaError = compileSchema(mainWorkflowSchema, "http://contoso.com/main-workflow-schema.json")
})
return compiledMainWorkflowSchema, mainWorkflowSchemaError
}

// getCompiledIncludedFileSchema returns the compiled included file schema, compiling it once and caching
func getCompiledIncludedFileSchema() (*jsonschema.Schema, error) {
includedFileSchemaOnce.Do(func() {
compiledIncludedFileSchema, includedFileSchemaError = compileSchema(includedFileSchema, "http://contoso.com/included-file-schema.json")
})
return compiledIncludedFileSchema, includedFileSchemaError
}

// getCompiledMcpConfigSchema returns the compiled MCP config schema, compiling it once and caching
func getCompiledMcpConfigSchema() (*jsonschema.Schema, error) {
mcpConfigSchemaOnce.Do(func() {
compiledMcpConfigSchema, mcpConfigSchemaError = compileSchema(mcpConfigSchema, "http://contoso.com/mcp-config-schema.json")
})
return compiledMcpConfigSchema, mcpConfigSchemaError
}

// compileSchema compiles a JSON schema from a JSON string
func compileSchema(schemaJSON, schemaURL string) (*jsonschema.Schema, error) {
// Create a new compiler
compiler := jsonschema.NewCompiler()

// Parse the schema JSON first
var schemaDoc any
if err := json.Unmarshal([]byte(schemaJSON), &schemaDoc); err != nil {
return fmt.Errorf("schema validation error for %s: failed to parse schema JSON: %w", context, err)
return nil, fmt.Errorf("failed to parse schema JSON: %w", err)
}

// Add the schema as a resource with a temporary URL
schemaURL := "http://contoso.com/schema.json"
// Add the schema as a resource
if err := compiler.AddResource(schemaURL, schemaDoc); err != nil {
return fmt.Errorf("schema validation error for %s: failed to add schema resource: %w", context, err)
return nil, fmt.Errorf("failed to add schema resource: %w", err)
}

// Compile the schema
schema, err := compiler.Compile(schemaURL)
if err != nil {
return nil, fmt.Errorf("failed to compile schema: %w", err)
}

return schema, nil
}

func validateWithSchema(frontmatter map[string]any, schemaJSON, context string) error {
// Determine which cached schema to use based on the schemaJSON
var schema *jsonschema.Schema
var err error

switch schemaJSON {
case mainWorkflowSchema:
schema, err = getCompiledMainWorkflowSchema()
case includedFileSchema:
schema, err = getCompiledIncludedFileSchema()
case mcpConfigSchema:
schema, err = getCompiledMcpConfigSchema()
default:
// Fallback for unknown schemas (shouldn't happen in normal operation)
// Compile the schema on-the-fly
schema, err = compileSchema(schemaJSON, "http://contoso.com/schema.json")
}
Comment on lines +193 to +204
Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The switch statement uses string comparison of potentially large JSON schemas. Consider using an enumeration or schema identifier instead of comparing the entire schemaJSON string content, as these comparisons are executed on every validation call.

Copilot uses AI. Check for mistakes.

if err != nil {
return fmt.Errorf("schema validation error for %s: %w", context, err)
}
Expand Down
58 changes: 42 additions & 16 deletions pkg/workflow/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"regexp"
"strings"
"sync"

"github.com/cli/go-gh/v2"
"github.com/githubnext/gh-aw/pkg/console"
Expand Down Expand Up @@ -218,6 +219,44 @@ func collectPackagesFromWorkflow(
}

// validateGitHubActionsSchema validates the generated YAML content against the GitHub Actions workflow schema
// Cached compiled schema to avoid recompiling on every validation
var (
compiledSchemaOnce sync.Once
compiledSchema *jsonschema.Schema
schemaCompileError error
)

// getCompiledSchema returns the compiled GitHub Actions schema, compiling it once and caching
func getCompiledSchema() (*jsonschema.Schema, error) {
compiledSchemaOnce.Do(func() {
// Parse the embedded schema
var schemaDoc any
if err := json.Unmarshal([]byte(githubWorkflowSchema), &schemaDoc); err != nil {
schemaCompileError = fmt.Errorf("failed to parse embedded GitHub Actions schema: %w", err)
return
}

// 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 {
schemaCompileError = fmt.Errorf("failed to add schema resource: %w", err)
return
}

// Compile the schema once
schema, err := loader.Compile(schemaURL)
if err != nil {
schemaCompileError = fmt.Errorf("failed to compile GitHub Actions schema: %w", err)
return
}

compiledSchema = schema
})

return compiledSchema, schemaCompileError
}

func (c *Compiler) validateGitHubActionsSchema(yamlContent string) error {
// Convert YAML to any for JSON conversion
var workflowData any
Expand All @@ -231,23 +270,10 @@ func (c *Compiler) validateGitHubActionsSchema(yamlContent string) error {
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)
// Get the cached compiled schema
schema, err := getCompiledSchema()
if err != nil {
return fmt.Errorf("failed to compile GitHub Actions schema: %w", err)
return err
}

// Validate the JSON data against the schema
Expand Down
Loading