@@ -371,7 +371,7 @@ func ExtractMarkdown(filePath string) (string, error) {
371371// ProcessImportsFromFrontmatter processes imports field from frontmatter
372372// Returns merged tools and engines from imported files
373373func ProcessImportsFromFrontmatter (frontmatter map [string ]any , baseDir string ) (mergedTools string , mergedEngines []string , err error ) {
374- result , err := ProcessImportsFromFrontmatterWithManifest (frontmatter , baseDir )
374+ result , err := ProcessImportsFromFrontmatterWithManifest (frontmatter , baseDir , nil )
375375 if err != nil {
376376 return "" , nil , err
377377 }
@@ -389,7 +389,7 @@ type importQueueItem struct {
389389// ProcessImportsFromFrontmatterWithManifest processes imports field from frontmatter
390390// Returns result containing merged tools, engines, markdown content, and list of imported files
391391// Uses BFS traversal with queues for deterministic ordering and cycle detection
392- func ProcessImportsFromFrontmatterWithManifest (frontmatter map [string ]any , baseDir string ) (* ImportsResult , error ) {
392+ func ProcessImportsFromFrontmatterWithManifest (frontmatter map [string ]any , baseDir string , cache * ImportCache ) (* ImportsResult , error ) {
393393 // Check if imports field exists
394394 importsField , exists := frontmatter ["imports" ]
395395 if ! exists {
@@ -451,7 +451,7 @@ func ProcessImportsFromFrontmatterWithManifest(frontmatter map[string]any, baseD
451451 }
452452
453453 // Resolve import path (supports workflowspec format)
454- fullPath , err := resolveIncludePath (filePath , baseDir )
454+ fullPath , err := resolveIncludePath (filePath , baseDir , cache )
455455 if err != nil {
456456 return nil , fmt .Errorf ("failed to resolve import '%s': %w" , filePath , err )
457457 }
@@ -558,7 +558,7 @@ func ProcessImportsFromFrontmatterWithManifest(frontmatter map[string]any, baseD
558558 }
559559
560560 // Resolve nested import path relative to the workflows directory, not the nested file's directory
561- nestedFullPath , err := resolveIncludePath (nestedFilePath , baseDir )
561+ nestedFullPath , err := resolveIncludePath (nestedFilePath , baseDir , cache )
562562 if err != nil {
563563 return nil , fmt .Errorf ("failed to resolve nested import '%s' from '%s': %w" , nestedFilePath , item .fullPath , err )
564564 }
@@ -724,7 +724,7 @@ func processIncludesWithVisited(content, baseDir string, extractTools bool, visi
724724 }
725725
726726 // Resolve file path first to get the canonical path
727- fullPath , err := resolveIncludePath (filePath , baseDir )
727+ fullPath , err := resolveIncludePath (filePath , baseDir , nil )
728728 if err != nil {
729729 if isOptional {
730730 // For optional includes, show a friendly informational message to stdout
@@ -796,12 +796,12 @@ func isUnderWorkflowsDirectory(filePath string) bool {
796796}
797797
798798// resolveIncludePath resolves include path based on workflowspec format or relative path
799- func resolveIncludePath (filePath , baseDir string ) (string , error ) {
799+ func resolveIncludePath (filePath , baseDir string , cache * ImportCache ) (string , error ) {
800800 // Check if this is a workflowspec (contains owner/repo/path format)
801801 // Format: owner/repo/path@ref or owner/repo/path@ref#section
802802 if isWorkflowSpec (filePath ) {
803- // Download from GitHub using workflowspec
804- return downloadIncludeFromWorkflowSpec (filePath )
803+ // Download from GitHub using workflowspec (with cache support)
804+ return downloadIncludeFromWorkflowSpec (filePath , cache )
805805 }
806806
807807 // Regular path, resolve relative to base directory
@@ -850,7 +850,8 @@ func isWorkflowSpec(path string) bool {
850850}
851851
852852// downloadIncludeFromWorkflowSpec downloads an include file from GitHub using workflowspec
853- func downloadIncludeFromWorkflowSpec (spec string ) (string , error ) {
853+ // It first checks the cache, and only downloads if not cached
854+ func downloadIncludeFromWorkflowSpec (spec string , cache * ImportCache ) (string , error ) {
854855 // Parse the workflowspec
855856 // Format: owner/repo/path@ref or owner/repo/path@ref#section
856857
@@ -880,13 +881,47 @@ func downloadIncludeFromWorkflowSpec(spec string) (string, error) {
880881 repo := slashParts [1 ]
881882 filePath := strings .Join (slashParts [2 :], "/" )
882883
884+ // Resolve ref to SHA for cache lookup
885+ var sha string
886+ if cache != nil {
887+ // Only resolve SHA if we're using the cache
888+ resolvedSHA , err := resolveRefToSHA (owner , repo , ref )
889+ if err != nil {
890+ // If the error is an authentication error, propagate it immediately
891+ lowerErr := strings .ToLower (err .Error ())
892+ if strings .Contains (lowerErr , "auth" ) || strings .Contains (lowerErr , "unauthoriz" ) || strings .Contains (lowerErr , "forbidden" ) || strings .Contains (lowerErr , "token" ) || strings .Contains (lowerErr , "permission denied" ) {
893+ return "" , fmt .Errorf ("failed to resolve ref to SHA due to authentication error: %w" , err )
894+ }
895+ log .Printf ("Failed to resolve ref to SHA, will skip cache: %v" , err )
896+ // Continue without caching if SHA resolution fails
897+ } else {
898+ sha = resolvedSHA
899+ // Check cache using SHA
900+ if cachedPath , found := cache .Get (owner , repo , filePath , sha ); found {
901+ log .Printf ("Using cached import: %s/%s/%s@%s (SHA: %s)" , owner , repo , filePath , ref , sha )
902+ return cachedPath , nil
903+ }
904+ }
905+ }
906+
883907 // Download the file content from GitHub
884908 content , err := downloadFileFromGitHub (owner , repo , filePath , ref )
885909 if err != nil {
886910 return "" , fmt .Errorf ("failed to download include from %s: %w" , spec , err )
887911 }
888912
889- // Create a temporary file to store the downloaded content
913+ // If cache is available and we have a SHA, store in cache
914+ if cache != nil && sha != "" {
915+ cachedPath , err := cache .Set (owner , repo , filePath , sha , content )
916+ if err != nil {
917+ log .Printf ("Failed to cache import: %v" , err )
918+ // Don't fail the compilation, fall back to temp file
919+ } else {
920+ return cachedPath , nil
921+ }
922+ }
923+
924+ // Fallback: Create a temporary file to store the downloaded content
890925 tempFile , err := os .CreateTemp ("" , "gh-aw-include-*.md" )
891926 if err != nil {
892927 return "" , fmt .Errorf ("failed to create temp file: %w" , err )
@@ -906,7 +941,52 @@ func downloadIncludeFromWorkflowSpec(spec string) (string, error) {
906941 return tempFile .Name (), nil
907942}
908943
909- // downloadFileFromGitHub downloads a file from GitHub using gh CLI
944+ // resolveRefToSHA resolves a git ref (branch, tag, or SHA) to its commit SHA
945+ func resolveRefToSHA (owner , repo , ref string ) (string , error ) {
946+ // If ref is already a full SHA (40 hex characters), return it as-is
947+ if len (ref ) == 40 && isHexString (ref ) {
948+ return ref , nil
949+ }
950+
951+ // Use gh CLI to get the commit SHA for the ref
952+ // This works for branches, tags, and short SHAs
953+ cmd := exec .Command ("gh" , "api" , fmt .Sprintf ("/repos/%s/%s/commits/%s" , owner , repo , ref ), "--jq" , ".sha" )
954+
955+ output , err := cmd .CombinedOutput ()
956+ if err != nil {
957+ outputStr := string (output )
958+ if strings .Contains (outputStr , "GH_TOKEN" ) || strings .Contains (outputStr , "authentication" ) || strings .Contains (outputStr , "not logged into" ) {
959+ return "" , fmt .Errorf ("failed to resolve ref to SHA: GitHub authentication required. Please run 'gh auth login' or set GH_TOKEN/GITHUB_TOKEN environment variable: %w" , err )
960+ }
961+ return "" , fmt .Errorf ("failed to resolve ref %s to SHA for %s/%s: %s: %w" , ref , owner , repo , strings .TrimSpace (outputStr ), err )
962+ }
963+
964+ sha := strings .TrimSpace (string (output ))
965+ if sha == "" {
966+ return "" , fmt .Errorf ("empty SHA returned for ref %s in %s/%s" , ref , owner , repo )
967+ }
968+
969+ // Validate it's a valid SHA (40 hex characters)
970+ if len (sha ) != 40 || ! isHexString (sha ) {
971+ return "" , fmt .Errorf ("invalid SHA format returned: %s" , sha )
972+ }
973+
974+ return sha , nil
975+ }
976+
977+ // isHexString checks if a string contains only hexadecimal characters
978+ func isHexString (s string ) bool {
979+ if len (s ) == 0 {
980+ return false
981+ }
982+ for _ , c := range s {
983+ if ! ((c >= '0' && c <= '9' ) || (c >= 'a' && c <= 'f' ) || (c >= 'A' && c <= 'F' )) {
984+ return false
985+ }
986+ }
987+ return true
988+ }
989+
910990func downloadFileFromGitHub (owner , repo , path , ref string ) ([]byte , error ) {
911991 // Use go-gh/v2 to download the file
912992 stdout , stderr , err := gh .Exec ("api" , fmt .Sprintf ("/repos/%s/%s/contents/%s?ref=%s" , owner , repo , path , ref ), "--jq" , ".content" )
@@ -1321,7 +1401,7 @@ func processIncludesForField(content, baseDir string, extractFunc func(string) (
13211401 }
13221402
13231403 // Resolve file path
1324- fullPath , err := resolveIncludePath (filePath , baseDir )
1404+ fullPath , err := resolveIncludePath (filePath , baseDir , nil )
13251405 if err != nil {
13261406 if isOptional {
13271407 // For optional includes, skip extraction
0 commit comments