Skip to content
24 changes: 18 additions & 6 deletions pkg/workflow/bundler_safety_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,12 @@ func validateNoLocalRequires(bundledContent string) error {

if len(foundRequires) > 0 {
bundlerSafetyLog.Printf("Validation failed: found %d un-inlined local require statements", len(foundRequires))
return fmt.Errorf("bundled JavaScript contains %d local require(s) that were not inlined:\n %s",
len(foundRequires), strings.Join(foundRequires, "\n "))
return NewValidationError(
"bundled-javascript",
fmt.Sprintf("%d un-inlined requires", len(foundRequires)),
"bundled JavaScript contains local require() statements that were not inlined during bundling",
fmt.Sprintf("Found un-inlined requires:\n\n%s\n\nThis indicates a bundling failure. Check:\n1. All required files are in actions/setup/js/\n2. Bundler configuration includes all dependencies\n3. No circular dependencies exist\n\nRun 'make build' to regenerate bundles", strings.Join(foundRequires, "\n")),
)
}

bundlerSafetyLog.Print("Validation successful: no local require statements found")
Expand Down Expand Up @@ -117,8 +121,12 @@ func validateNoModuleReferences(bundledContent string) error {

if len(foundReferences) > 0 {
bundlerSafetyLog.Printf("Validation failed: found %d module references", len(foundReferences))
return fmt.Errorf("bundled JavaScript for GitHub Script mode contains %d module reference(s) that should have been removed:\n %s\n\nGitHub Script mode does not support module.exports or exports; these references must be removed during bundling",
len(foundReferences), strings.Join(foundReferences, "\n "))
return NewValidationError(
"bundled-javascript",
fmt.Sprintf("%d module references", len(foundReferences)),
"bundled JavaScript for GitHub Script mode contains module.exports or exports references",
fmt.Sprintf("Found module references:\n\n%s\n\nGitHub Script mode does not support CommonJS module system. Check:\n1. Bundle configuration removes module references\n2. Code doesn't use module.exports or exports\n3. Using appropriate runtime mode (consider 'nodejs' mode if module system is needed)\n\nRun 'make build' to regenerate bundles", strings.Join(foundReferences, "\n")),
)
}

bundlerSafetyLog.Print("Validation successful: no module references found")
Expand Down Expand Up @@ -201,8 +209,12 @@ func ValidateEmbeddedResourceRequires(sources map[string]string) error {

if len(missingDeps) > 0 {
bundlerSafetyLog.Printf("Validation failed: found %d missing dependencies", len(missingDeps))
return fmt.Errorf("embedded JavaScript files have %d missing local require(s):\n %s\n\nThese files must be added to GetJavaScriptSources() in js.go",
len(missingDeps), strings.Join(missingDeps, "\n "))
return NewValidationError(
"embedded-javascript",
fmt.Sprintf("%d missing dependencies", len(missingDeps)),
"embedded JavaScript files have missing local require() dependencies",
fmt.Sprintf("Missing dependencies:\n\n%s\n\nTo fix:\n1. Add missing .cjs files to actions/setup/js/\n2. Update GetJavaScriptSources() in pkg/workflow/js.go to include them\n3. Ensure file paths match require() statements\n4. Run 'make build' to regenerate bundles\n\nExample:\n//go:embed actions/setup/js/missing-file.cjs\nvar missingFileSource string", strings.Join(missingDeps, "\n")),
)
}

bundlerSafetyLog.Printf("Validation successful: all local requires are available in sources")
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/domains_protocol_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Test HTTPS-only wildcard domains.
on: push
permissions:
contents: read
issues: write
issues: read
engine: copilot
strict: false
network:
Expand Down
16 changes: 12 additions & 4 deletions pkg/workflow/expression_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,12 @@ func validateExpressionSafety(markdownContent string) error {
allowedList.WriteString(" - inputs.* (workflow_call)\n")
allowedList.WriteString(" - env.*\n")

return fmt.Errorf("unauthorized expressions:%s\nallowed:%s",
unauthorizedList.String(), allowedList.String())
return NewValidationError(
"expressions",
fmt.Sprintf("%d unauthorized expressions found", len(unauthorizedExpressions)),
fmt.Sprintf("expressions are not in the allowed list:%s", unauthorizedList.String()),
fmt.Sprintf("Use only allowed expressions:%s\nFor more details, see the expression security documentation.", allowedList.String()),
)
}

expressionValidationLog.Print("Expression safety validation passed")
Expand Down Expand Up @@ -432,8 +436,12 @@ func validateRuntimeImportFiles(markdownContent string, workspaceDir string) err

if len(validationErrors) > 0 {
expressionValidationLog.Printf("Runtime-import validation failed: %d file(s) with errors", len(validationErrors))
return fmt.Errorf("runtime-import files contain expression errors:\n\n%s",
strings.Join(validationErrors, "\n\n"))
return NewValidationError(
"runtime-import",
fmt.Sprintf("%d files with errors", len(validationErrors)),
fmt.Sprintf("runtime-import files contain expression errors:\n\n%s", strings.Join(validationErrors, "\n\n")),
"Fix the expression errors in the imported files listed above. Each file must only use allowed GitHub Actions expressions. See expression security documentation for details.",
)
}

expressionValidationLog.Print("All runtime-import files validated successfully")
Expand Down
14 changes: 12 additions & 2 deletions pkg/workflow/features_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,12 @@ func validateActionTag(value any) error {
// Convert to string
strVal, ok := value.(string)
if !ok {
return fmt.Errorf("action-tag must be a string, got %T", value)
return NewValidationError(
"features.action-tag",
fmt.Sprintf("%T", value),
fmt.Sprintf("action-tag must be a string, got %T", value),
"Provide a string value for action-tag. Example:\nfeatures:\n action-tag: \"a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0\"",
)
}

// Allow empty string (falls back to version)
Expand All @@ -75,7 +80,12 @@ func validateActionTag(value any) error {

// Validate it's a full SHA (40 hex characters)
if !isValidFullSHA(strVal) {
return fmt.Errorf("action-tag must be a full 40-character commit SHA, got %q (length: %d). Short SHAs are not allowed. Use 'git rev-parse <ref>' to get the full SHA", strVal, len(strVal))
return NewValidationError(
"features.action-tag",
strVal,
fmt.Sprintf("action-tag must be a full 40-character commit SHA (length: %d). Short SHAs are not allowed", len(strVal)),
"Use 'git rev-parse <ref>' to get the full SHA. Example:\n\n$ git rev-parse HEAD\na1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0\n\nThen use in workflow:\nfeatures:\n action-tag: \"a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0\"",
)
}

return nil
Expand Down
24 changes: 21 additions & 3 deletions pkg/workflow/mcp_gateway_schema_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,22 +67,40 @@ func getCompiledMCPGatewaySchema() (*jsonschema.Schema, error) {
// Parse the embedded schema
var schemaDoc any
if err := json.Unmarshal([]byte(mcpGatewayConfigSchema), &schemaDoc); err != nil {
mcpGatewaySchemaCompileError = fmt.Errorf("failed to parse embedded MCP Gateway configuration schema: %w", err)
mcpGatewaySchemaCompileError = NewOperationError(
"compile",
"MCP Gateway schema",
"embedded schema",
err,
"This is an internal error with the embedded MCP Gateway schema. Please report this issue:\nhttps://github.com/githubnext/gh-aw/issues/new",
)
return
}

// Create compiler and add the schema as a resource
loader := jsonschema.NewCompiler()
schemaURL := "https://docs.github.com/gh-aw/schemas/mcp-gateway-config.schema.json"
if err := loader.AddResource(schemaURL, schemaDoc); err != nil {
mcpGatewaySchemaCompileError = fmt.Errorf("failed to add MCP Gateway schema resource: %w", err)
mcpGatewaySchemaCompileError = NewOperationError(
"compile",
"MCP Gateway schema",
schemaURL,
err,
"This is an internal error with the MCP Gateway schema resource. Please report this issue:\nhttps://github.com/githubnext/gh-aw/issues/new",
)
return
}

// Compile the schema once
schema, err := loader.Compile(schemaURL)
if err != nil {
mcpGatewaySchemaCompileError = fmt.Errorf("failed to compile MCP Gateway configuration schema: %w", err)
mcpGatewaySchemaCompileError = NewOperationError(
"compile",
"MCP Gateway schema",
schemaURL,
err,
"This is an internal error compiling the MCP Gateway schema. Please report this issue:\nhttps://github.com/githubnext/gh-aw/issues/new",
)
return
}

Expand Down
15 changes: 13 additions & 2 deletions pkg/workflow/npm_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,13 @@ func (c *Compiler) validateNpxPackages(workflowData *WorkflowData) error {
_, err := exec.LookPath("npm")
if err != nil {
npmValidationLog.Print("npm command not found, cannot validate npx packages")
return fmt.Errorf("npm command not found - cannot validate npx packages. Install Node.js/npm or disable validation")
return NewOperationError(
"validate",
"npx packages",
"",
err,
"Install Node.js and npm to enable npx package validation:\n\nUsing nvm (recommended):\n$ nvm install --lts\n\nOr download from https://nodejs.org\n\nAlternatively, disable validation by setting GH_AW_SKIP_NPX_VALIDATION=true",
)
}

var errors []string
Expand All @@ -80,7 +86,12 @@ func (c *Compiler) validateNpxPackages(workflowData *WorkflowData) error {

if len(errors) > 0 {
npmValidationLog.Printf("npx package validation failed with %d errors", len(errors))
return fmt.Errorf("npx package validation failed:\n - %s", strings.Join(errors, "\n - "))
return NewValidationError(
"npx.packages",
fmt.Sprintf("%d packages not found", len(errors)),
"npx packages not found on npm registry",
fmt.Sprintf("Fix package names or verify they exist on npm:\n\n%s\n\nCheck package availability:\n$ npm view <package-name>\n\nSearch for similar packages:\n$ npm search <keyword>", strings.Join(errors, "\n")),
)
}

npmValidationLog.Print("All npx packages validated successfully")
Expand Down
15 changes: 13 additions & 2 deletions pkg/workflow/pip_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,13 @@ func (c *Compiler) validateUvPackages(workflowData *WorkflowData) error {
_, pip3Err := exec.LookPath("pip3")
if pip3Err != nil {
pipValidationLog.Print("Neither uv nor pip commands found, cannot validate")
return fmt.Errorf("uv and pip commands not found - cannot validate uv packages. Install uv/pip or disable validation")
return NewOperationError(
"validate",
"uv packages",
"",
pip3Err,
"Install uv or pip to enable package validation:\n\nInstall uv (recommended):\n$ curl -LsSf https://astral.sh/uv/install.sh | sh\n\nOr install pip:\n$ python -m ensurepip --upgrade\n\nAlternatively, disable validation by setting GH_AW_SKIP_UV_VALIDATION=true",
)
}
pipCmd = "pip3"
pipValidationLog.Print("Using pip3 for validation")
Expand Down Expand Up @@ -166,7 +172,12 @@ func (c *Compiler) validateUvPackages(workflowData *WorkflowData) error {
}

if len(errors) > 0 {
return fmt.Errorf("uv package validation requires network access:\n - %s", strings.Join(errors, "\n - "))
return NewValidationError(
"uv.packages",
fmt.Sprintf("%d packages require validation", len(errors)),
"uv package validation requires network access or local cache",
fmt.Sprintf("Ensure network access or cache uv packages locally:\n\n%s\n\nCache packages:\n$ uv pip install <package-name> --no-cache\n\nOr connect to network for validation", strings.Join(errors, "\n")),
)
}

return nil
Expand Down
28 changes: 24 additions & 4 deletions pkg/workflow/runtime_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,12 @@ func (c *Compiler) validateContainerImages(workflowData *WorkflowData) error {
}

if len(errors) > 0 {
return fmt.Errorf("container image validation failed:\n - %s", strings.Join(errors, "\n - "))
return NewValidationError(
"container.images",
fmt.Sprintf("%d images failed validation", len(errors)),
"container image validation failed",
fmt.Sprintf("Fix the following container image issues:\n\n%s\n\nEnsure:\n1. Container images exist and are accessible\n2. Registry URLs are correct\n3. Image tags are specified\n4. You have pull permissions for private images", strings.Join(errors, "\n")),
)
}

return nil
Expand Down Expand Up @@ -170,7 +175,12 @@ func (c *Compiler) validateRuntimePackages(workflowData *WorkflowData) error {
}

if len(errors) > 0 {
return fmt.Errorf("runtime package validation failed:\n - %s", strings.Join(errors, "\n - "))
return NewValidationError(
"runtime.packages",
fmt.Sprintf("%d package validation errors", len(errors)),
"runtime package validation failed",
fmt.Sprintf("Fix the following package issues:\n\n%s\n\nEnsure:\n1. Package names are spelled correctly\n2. Packages exist in their respective registries (npm, PyPI)\n3. Package managers (npm, pip, uv) are installed\n4. Network access is available for registry checks", strings.Join(errors, "\n")),
)
}

return nil
Expand Down Expand Up @@ -261,7 +271,12 @@ 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)
return NewValidationError(
"sandbox.cache-memory",
cache.ID,
"duplicate cache-memory ID found - each cache must have a unique ID",
"Change the cache ID to a unique value. Example:\n\nsandbox:\n cache-memory:\n - id: cache-1\n size: 100MB\n - id: cache-2 # Use unique IDs\n size: 50MB",
)
}
seen[cache.ID] = true
}
Expand All @@ -275,7 +290,12 @@ func validateSecretReferences(secrets []string) error {

for _, secret := range secrets {
if !secretNamePattern.MatchString(secret) {
return fmt.Errorf("invalid secret name: %s. Secret names must start with an uppercase letter and contain only uppercase letters, numbers, and underscores. Example: MY_SECRET_KEY", secret)
return NewValidationError(
"secrets",
secret,
"invalid secret name format - must follow environment variable naming conventions",
"Secret names must:\n- Start with an uppercase letter\n- Contain only uppercase letters, numbers, and underscores\n\nExamples:\n MY_SECRET_KEY ✓\n API_TOKEN_123 ✓\n mySecretKey ✗ (lowercase)\n 123_SECRET ✗ (starts with number)\n MY-SECRET ✗ (hyphens not allowed)",
)
}
}

Expand Down
Loading
Loading