fetchAndSaveRemoteDispatchWorkflows fetches and saves the workflow files referenced in the safe-outputs.dispatch-workflow configuration of a remote workflow. Each listed workflow name (without extension) is resolved as a sibling file (" .md") in the same directory as the source workflow and dow
(ctx context.Context, content string, spec *WorkflowSpec, targetDir string, verbose bool, force bool, tracker *FileTracker, downloaders ...fileDownloadFn)
| 78 | // An optional downloader function may be provided as the last argument to override the default |
| 79 | // parser.DownloadFileFromGitHub implementation (used in tests to avoid real network calls). |
| 80 | func fetchAndSaveRemoteDispatchWorkflows(ctx context.Context, content string, spec *WorkflowSpec, targetDir string, verbose bool, force bool, tracker *FileTracker, downloaders ...fileDownloadFn) error { |
| 81 | remoteWorkflowLog.Printf("Fetching remote dispatch workflows: repo=%s, targetDir=%s, force=%v", spec.RepoSlug, targetDir, force) |
| 82 | downloader := fileDownloadFn(parser.DownloadFileFromGitHub) |
| 83 | if len(downloaders) > 0 && downloaders[0] != nil { |
| 84 | downloader = downloaders[0] |
| 85 | } |
| 86 | if spec.RepoSlug == "" { |
| 87 | return nil |
| 88 | } |
| 89 | |
| 90 | parts := strings.SplitN(spec.RepoSlug, "/", 2) |
| 91 | if len(parts) != 2 { |
| 92 | return nil |
| 93 | } |
| 94 | owner, repo := parts[0], parts[1] |
| 95 | ref := spec.Version |
| 96 | if ref == "" { |
| 97 | defaultBranch, err := getRepoDefaultBranch(ctx, spec.RepoSlug) |
| 98 | if err != nil { |
| 99 | remoteWorkflowLog.Printf("Failed to resolve default branch for %s, falling back to 'main': %v", spec.RepoSlug, err) |
| 100 | ref = "main" |
| 101 | } else { |
| 102 | ref = defaultBranch |
| 103 | } |
| 104 | spec.Version = ref |
| 105 | } |
| 106 | |
| 107 | workflowNames := extractDispatchWorkflowNames(content) |
| 108 | if len(workflowNames) == 0 { |
| 109 | return nil |
| 110 | } |
| 111 | |
| 112 | remoteWorkflowLog.Printf("Found %d dispatch workflow(s) to fetch from %s@%s", len(workflowNames), spec.RepoSlug, ref) |
| 113 | |
| 114 | // workflowBaseDir is the directory of the source workflow in the remote repo |
| 115 | // (e.g. ".github/workflows"). Dispatch-workflow names are resolved relative to it. |
| 116 | workflowBaseDir := getParentDir(spec.WorkflowPath) |
| 117 | |
| 118 | // Pre-compute the absolute target directory for path-traversal boundary checks. |
| 119 | absTargetDir, err := filepath.Abs(targetDir) |
| 120 | if err != nil { |
| 121 | remoteWorkflowLog.Printf("Failed to resolve absolute path for target directory %s: %v", targetDir, err) |
| 122 | return nil |
| 123 | } |
| 124 | |
| 125 | for _, workflowName := range workflowNames { |
| 126 | // Build the remote file path for this dispatch workflow |
| 127 | var remoteFilePath string |
| 128 | if workflowBaseDir != "" { |
| 129 | remoteFilePath = path.Join(workflowBaseDir, workflowName+".md") |
| 130 | } else { |
| 131 | remoteFilePath = workflowName + ".md" |
| 132 | } |
| 133 | remoteFilePath = path.Clean(remoteFilePath) |
| 134 | |
| 135 | // The local path is just the workflow filename in targetDir |
| 136 | localRelPath := filepath.Clean(workflowName + ".md") |
| 137 | targetPath := filepath.Join(targetDir, localRelPath) |