@@ -3,13 +3,11 @@ package parser
33import (
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
685458func ExpandIncludes (content , baseDir string , extractTools bool ) (string , error ) {
0 commit comments