validateRuntimeImportFiles validates expressions in all runtime-import files at compile time. This catches expression errors early, before the workflow runs. workspaceDir should be the root of the repository (containing .github folder). It returns any best-effort sub-agent frontmatter warnings and a
(markdownContent string, workspaceDir string)
| 91 | // It returns any best-effort sub-agent frontmatter warnings and a non-nil error for fatal |
| 92 | // expression validation failures. |
| 93 | func validateRuntimeImportFiles(markdownContent string, workspaceDir string) ([]string, error) { |
| 94 | expressionValidationLog.Print("Validating runtime-import files") |
| 95 | |
| 96 | // Extract all runtime-import file paths |
| 97 | paths := extractRuntimeImportPaths(markdownContent) |
| 98 | if len(paths) == 0 { |
| 99 | expressionValidationLog.Print("No runtime-import files to validate") |
| 100 | return nil, nil |
| 101 | } |
| 102 | |
| 103 | expressionValidationLog.Printf("Found %d runtime-import file(s) to validate", len(paths)) |
| 104 | |
| 105 | var validationErrors []string |
| 106 | var subAgentWarnings []string |
| 107 | |
| 108 | for _, filePath := range paths { |
| 109 | // Normalize the path to be relative to .github folder |
| 110 | normalizedPath := filePath |
| 111 | if strings.HasPrefix(normalizedPath, constants.GithubDir) { |
| 112 | normalizedPath = normalizedPath[len(constants.GithubDir):] // Remove ".github/" |
| 113 | } else if strings.HasPrefix(normalizedPath, ".github\\") { |
| 114 | normalizedPath = normalizedPath[8:] // Remove ".github\" (Windows) |
| 115 | } |
| 116 | if strings.HasPrefix(normalizedPath, "./") { |
| 117 | normalizedPath = normalizedPath[2:] // Remove "./" |
| 118 | } else if strings.HasPrefix(normalizedPath, ".\\") { |
| 119 | normalizedPath = normalizedPath[2:] // Remove ".\" (Windows) |
| 120 | } |
| 121 | |
| 122 | // Build absolute path to the file |
| 123 | githubFolder := filepath.Join(workspaceDir, ".github") |
| 124 | absolutePath := filepath.Join(githubFolder, normalizedPath) |
| 125 | |
| 126 | // Security check: ensure the resolved path is within the .github folder |
| 127 | // Use filepath.Rel to check if the path escapes the .github folder |
| 128 | normalizedGithubFolder := filepath.Clean(githubFolder) |
| 129 | normalizedAbsolutePath := filepath.Clean(absolutePath) |
| 130 | relativePath, err := filepath.Rel(normalizedGithubFolder, normalizedAbsolutePath) |
| 131 | if err != nil || relativePath == ".." || strings.HasPrefix(relativePath, ".."+string(filepath.Separator)) || filepath.IsAbs(relativePath) { |
| 132 | validationErrors = append(validationErrors, fmt.Sprintf("%s: Security: Path must be within .github folder (resolves to: %s)", filePath, relativePath)) |
| 133 | continue |
| 134 | } |
| 135 | |
| 136 | // Check if file exists; missing files (optional or not) are deferred to runtime |
| 137 | if _, err := os.Stat(absolutePath); os.IsNotExist(err) { |
| 138 | expressionValidationLog.Printf("Skipping validation for non-existent file: %s", filePath) |
| 139 | continue |
| 140 | } |
| 141 | |
| 142 | // Read the file content |
| 143 | content, err := os.ReadFile(absolutePath) |
| 144 | if err != nil { |
| 145 | validationErrors = append(validationErrors, fmt.Sprintf("%s: failed to read file: %v", filePath, err)) |
| 146 | continue |
| 147 | } |
| 148 | |
| 149 | // Validate expressions in the imported file |
| 150 | if err := validateExpressionSafety(string(content)); err != nil { |