Skip to content

Commit 2014b70

Browse files
Copilotmnkiefer
andcommitted
Phase 4: Extract include_processor.go (238 lines)
Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com>
1 parent a1bd62d commit 2014b70

File tree

2 files changed

+238
-227
lines changed

2 files changed

+238
-227
lines changed

pkg/parser/frontmatter.go

Lines changed: 0 additions & 227 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@ package parser
33
import (
44
"bufio"
55
"bytes"
6-
"encoding/json"
76
"fmt"
87
"os"
98
"path/filepath"
109
"strings"
1110

12-
"github.com/githubnext/gh-aw/pkg/console"
1311
"github.com/githubnext/gh-aw/pkg/logger"
1412
)
1513

@@ -455,231 +453,6 @@ func processImportsFromFrontmatterWithManifestAndSource(frontmatter map[string]a
455453
}, nil
456454
}
457455

458-
// ProcessIncludes processes @include, @import (deprecated), and {{#import: directives in markdown content
459-
// This matches the bash process_includes function behavior
460-
func ProcessIncludes(content, baseDir string, extractTools bool) (string, error) {
461-
visited := make(map[string]bool)
462-
return processIncludesWithVisited(content, baseDir, extractTools, visited)
463-
}
464-
465-
// processIncludesWithVisited processes import directives with cycle detection
466-
func processIncludesWithVisited(content, baseDir string, extractTools bool, visited map[string]bool) (string, error) {
467-
scanner := bufio.NewScanner(strings.NewReader(content))
468-
var result bytes.Buffer
469-
470-
for scanner.Scan() {
471-
line := scanner.Text()
472-
473-
// Parse import directive
474-
directive := ParseImportDirective(line)
475-
if directive != nil {
476-
// Emit deprecation warning for legacy syntax
477-
if directive.IsLegacy {
478-
// Security: Escape strings to prevent quote injection in warning messages
479-
// Use %q format specifier to safely quote strings containing special characters
480-
optionalMarker := ""
481-
if directive.IsOptional {
482-
optionalMarker = "?"
483-
}
484-
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Deprecated syntax: %q. Use {{#import%s %s}} instead.",
485-
directive.Original,
486-
optionalMarker,
487-
directive.Path)))
488-
}
489-
490-
isOptional := directive.IsOptional
491-
includePath := directive.Path
492-
493-
// Handle section references (file.md#Section)
494-
var filePath, sectionName string
495-
if strings.Contains(includePath, "#") {
496-
parts := strings.SplitN(includePath, "#", 2)
497-
filePath = parts[0]
498-
sectionName = parts[1]
499-
} else {
500-
filePath = includePath
501-
}
502-
503-
// Resolve file path first to get the canonical path
504-
fullPath, err := ResolveIncludePath(filePath, baseDir, nil)
505-
if err != nil {
506-
if isOptional {
507-
// For optional includes, show a friendly informational message to stdout
508-
if !extractTools {
509-
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Optional include file not found: %s. You can create this file to configure the workflow.", filePath)))
510-
}
511-
continue
512-
}
513-
// For required includes, fail compilation with an error
514-
return "", fmt.Errorf("failed to resolve required include '%s': %w", filePath, err)
515-
}
516-
517-
// Check for repeated imports using the resolved full path
518-
if visited[fullPath] {
519-
if !extractTools {
520-
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Already included: %s, skipping", filePath)))
521-
}
522-
continue
523-
}
524-
525-
// Mark as visited using the resolved full path
526-
visited[fullPath] = true
527-
528-
// Process the included file
529-
includedContent, err := processIncludedFileWithVisited(fullPath, sectionName, extractTools, visited)
530-
if err != nil {
531-
// For any processing errors, fail compilation
532-
return "", fmt.Errorf("failed to process included file '%s': %w", fullPath, err)
533-
}
534-
535-
if extractTools {
536-
// For tools mode, add each JSON on a separate line
537-
result.WriteString(includedContent + "\n")
538-
} else {
539-
result.WriteString(includedContent)
540-
}
541-
} else {
542-
// Regular line, just pass through (unless extracting tools)
543-
if !extractTools {
544-
result.WriteString(line + "\n")
545-
}
546-
}
547-
}
548-
549-
return result.String(), nil
550-
}
551-
552-
// processIncludedFile processes a single included file, optionally extracting a section
553-
// processIncludedFileWithVisited processes a single included file with cycle detection for nested includes
554-
func processIncludedFileWithVisited(filePath, sectionName string, extractTools bool, visited map[string]bool) (string, error) {
555-
content, err := os.ReadFile(filePath)
556-
if err != nil {
557-
return "", fmt.Errorf("failed to read included file %s: %w", filePath, err)
558-
}
559-
560-
// Validate included file frontmatter based on file location
561-
result, err := ExtractFrontmatterFromContent(string(content))
562-
if err != nil {
563-
return "", fmt.Errorf("failed to extract frontmatter from included file %s: %w", filePath, err)
564-
}
565-
566-
// Check if file is under .github/workflows/ for strict validation
567-
isWorkflowFile := isUnderWorkflowsDirectory(filePath)
568-
569-
// Always try strict validation first
570-
validationErr := ValidateIncludedFileFrontmatterWithSchemaAndLocation(result.Frontmatter, filePath)
571-
572-
if validationErr != nil {
573-
if isWorkflowFile {
574-
// For workflow files, strict validation must pass
575-
return "", fmt.Errorf("invalid frontmatter in included file %s: %w", filePath, validationErr)
576-
} else {
577-
// For non-workflow files, fall back to relaxed validation with warnings
578-
if len(result.Frontmatter) > 0 {
579-
// Valid fields for non-workflow frontmatter (fields that should not trigger warnings)
580-
// This list should match the properties defined in included_file_schema.json
581-
validFields := map[string]bool{
582-
"tools": true,
583-
"engine": true,
584-
"network": true,
585-
"mcp-servers": true,
586-
"imports": true,
587-
"name": true,
588-
"description": true,
589-
"steps": true,
590-
"safe-outputs": true,
591-
"safe-inputs": true,
592-
"services": true,
593-
"runtimes": true,
594-
"permissions": true,
595-
"secret-masking": true,
596-
"applyTo": true,
597-
"inputs": true,
598-
}
599-
600-
// Check for unexpected frontmatter fields
601-
unexpectedFields := make([]string, 0)
602-
for key := range result.Frontmatter {
603-
if !validFields[key] {
604-
unexpectedFields = append(unexpectedFields, key)
605-
}
606-
}
607-
608-
if len(unexpectedFields) > 0 {
609-
// Show warning for unexpected frontmatter fields
610-
fmt.Fprintf(os.Stderr, "%s\n", console.FormatWarningMessage(
611-
fmt.Sprintf("Ignoring unexpected frontmatter fields in %s: %s",
612-
filePath, strings.Join(unexpectedFields, ", "))))
613-
}
614-
615-
// Validate the tools, engine, network, and mcp-servers sections if present
616-
filteredFrontmatter := map[string]any{}
617-
if tools, hasTools := result.Frontmatter["tools"]; hasTools {
618-
filteredFrontmatter["tools"] = tools
619-
}
620-
if engine, hasEngine := result.Frontmatter["engine"]; hasEngine {
621-
filteredFrontmatter["engine"] = engine
622-
}
623-
if network, hasNetwork := result.Frontmatter["network"]; hasNetwork {
624-
filteredFrontmatter["network"] = network
625-
}
626-
if mcpServers, hasMCPServers := result.Frontmatter["mcp-servers"]; hasMCPServers {
627-
filteredFrontmatter["mcp-servers"] = mcpServers
628-
}
629-
// Note: we don't validate imports field as it's handled separately
630-
if len(filteredFrontmatter) > 0 {
631-
if err := ValidateIncludedFileFrontmatterWithSchemaAndLocation(filteredFrontmatter, filePath); err != nil {
632-
fmt.Fprintf(os.Stderr, "%s\n", console.FormatWarningMessage(
633-
fmt.Sprintf("Invalid configuration in %s: %v", filePath, err)))
634-
}
635-
}
636-
}
637-
}
638-
}
639-
640-
if extractTools {
641-
// Extract tools from frontmatter, using filtered frontmatter for non-workflow files with validation errors
642-
if validationErr == nil || isWorkflowFile {
643-
// If validation passed or it's a workflow file (which must have valid frontmatter), use original extraction
644-
return extractToolsFromContent(string(content))
645-
} else {
646-
// For non-workflow files with validation errors, only extract tools section
647-
if tools, hasTools := result.Frontmatter["tools"]; hasTools {
648-
toolsJSON, err := json.Marshal(tools)
649-
if err != nil {
650-
return "{}", nil
651-
}
652-
return strings.TrimSpace(string(toolsJSON)), nil
653-
}
654-
return "{}", nil
655-
}
656-
}
657-
658-
// Extract markdown content
659-
markdownContent, err := ExtractMarkdownContent(string(content))
660-
if err != nil {
661-
return "", fmt.Errorf("failed to extract markdown from %s: %w", filePath, err)
662-
}
663-
664-
// Process nested includes recursively
665-
includedDir := filepath.Dir(filePath)
666-
markdownContent, err = processIncludesWithVisited(markdownContent, includedDir, extractTools, visited)
667-
if err != nil {
668-
return "", fmt.Errorf("failed to process nested includes in %s: %w", filePath, err)
669-
}
670-
671-
// If section specified, extract only that section
672-
if sectionName != "" {
673-
sectionContent, err := ExtractMarkdownSection(markdownContent, sectionName)
674-
if err != nil {
675-
return "", fmt.Errorf("failed to extract section '%s' from %s: %w", sectionName, filePath, err)
676-
}
677-
return strings.Trim(sectionContent, "\n") + "\n", nil
678-
}
679-
680-
return strings.Trim(markdownContent, "\n") + "\n", nil
681-
}
682-
683456
// ExpandIncludes recursively expands @include and @import directives until no more remain
684457
// This matches the bash expand_includes function behavior
685458
func ExpandIncludes(content, baseDir string, extractTools bool) (string, error) {

0 commit comments

Comments
 (0)