package download import ( "errors" "fmt" "path/filepath" "github.com/AlecAivazis/survey/v2" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/pkg/cmd/run/shared" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/cli/cli/v2/pkg/prompt" "github.com/cli/cli/v2/pkg/set" "github.com/spf13/cobra" ) type DownloadOptions struct { IO *iostreams.IOStreams Platform platform Prompter prompter DoPrompt bool RunID string DestinationDir string Names []string } type platform interface { List(runID string) ([]shared.Artifact, error) Download(url string, dir string) error } type prompter interface { Prompt(message string, options []string, result interface{}) error } func NewCmdDownload(f *cmdutil.Factory, runF func(*DownloadOptions) error) *cobra.Command { opts := &DownloadOptions{ IO: f.IOStreams, } cmd := &cobra.Command{ Use: "download []", Short: "Download artifacts generated by a workflow run", Long: heredoc.Doc(` Download artifacts generated by a GitHub Actions workflow run. The contents of each artifact will be extracted under separate directories based on the artifact name. If only a single artifact is specified, it will be extracted into the current directory. `), Args: cobra.MaximumNArgs(1), Example: heredoc.Doc(` # Download all artifacts generated by a workflow run $ gh run download # Download a specific artifact within a run $ gh run download -n # Download specific artifacts across all runs in a repository $ gh run download -n -n # Select artifacts to download interactively $ gh run download `), RunE: func(cmd *cobra.Command, args []string) error { if len(args) > 0 { opts.RunID = args[0] } else if len(opts.Names) == 0 && opts.IO.CanPrompt() { opts.DoPrompt = true } // support `-R, --repo` override baseRepo, err := f.BaseRepo() if err != nil { return err } httpClient, err := f.HttpClient() if err != nil { return err } opts.Platform = &apiPlatform{ client: httpClient, repo: baseRepo, } opts.Prompter = &surveyPrompter{} if runF != nil { return runF(opts) } return runDownload(opts) }, } cmd.Flags().StringVarP(&opts.DestinationDir, "dir", "D", ".", "The directory to download artifacts into") cmd.Flags().StringArrayVarP(&opts.Names, "name", "n", nil, "Only download artifacts that match any of the given names") return cmd } func runDownload(opts *DownloadOptions) error { opts.IO.StartProgressIndicator() artifacts, err := opts.Platform.List(opts.RunID) opts.IO.StopProgressIndicator() if err != nil { return fmt.Errorf("error fetching artifacts: %w", err) } numValidArtifacts := 0 for _, a := range artifacts { if a.Expired { continue } numValidArtifacts++ } if numValidArtifacts == 0 { return errors.New("no valid artifacts found to download") } wantNames := opts.Names if opts.DoPrompt { artifactNames := set.NewStringSet() for _, a := range artifacts { if !a.Expired { artifactNames.Add(a.Name) } } options := artifactNames.ToSlice() if len(options) > 10 { options = options[:10] } err := opts.Prompter.Prompt("Select artifacts to download:", options, &wantNames) if err != nil { return err } if len(wantNames) == 0 { return errors.New("no artifacts selected") } } opts.IO.StartProgressIndicator() defer opts.IO.StopProgressIndicator() // track downloaded artifacts and avoid re-downloading any of the same name downloaded := set.NewStringSet() for _, a := range artifacts { if a.Expired { continue } if downloaded.Contains(a.Name) { continue } if len(wantNames) > 0 && !matchAny(wantNames, a.Name) { continue } destDir := opts.DestinationDir if len(wantNames) != 1 { destDir = filepath.Join(destDir, a.Name) } err := opts.Platform.Download(a.DownloadURL, destDir) if err != nil { return fmt.Errorf("error downloading %s: %w", a.Name, err) } downloaded.Add(a.Name) } if downloaded.Len() == 0 { return errors.New("no artifact matches any of the names provided") } return nil } func matchAny(patterns []string, name string) bool { for _, p := range patterns { if name == p { return true } } return false } type surveyPrompter struct{} func (sp *surveyPrompter) Prompt(message string, options []string, result interface{}) error { return prompt.SurveyAskOne(&survey.MultiSelect{ Message: message, Options: options, }, result) }