package shared import ( "bytes" "encoding/base64" "errors" "fmt" "io" "net/url" "path" "strconv" "strings" "github.com/cli/cli/v2/api" "github.com/cli/cli/v2/internal/ghrepo" "github.com/cli/cli/v2/pkg/cmdutil" "github.com/cli/cli/v2/pkg/iostreams" "github.com/cli/go-gh/v2/pkg/asciisanitizer" "golang.org/x/text/transform" ) const ( Active WorkflowState = "active" DisabledManually WorkflowState = "disabled_manually" DisabledInactivity WorkflowState = "disabled_inactivity" ) type iprompter interface { Select(string, string, []string) (int, error) } type WorkflowState string type Workflow struct { Name string ID int64 Path string State WorkflowState } type WorkflowsPayload struct { Workflows []Workflow } func (w *Workflow) Disabled() bool { return w.State != Active } func (w *Workflow) Base() string { return path.Base(w.Path) } func (w *Workflow) ExportData(fields []string) map[string]interface{} { return cmdutil.StructExportData(w, fields) } func GetWorkflows(client *api.Client, repo ghrepo.Interface, limit int) ([]Workflow, error) { perPage := limit page := 1 if limit > 100 || limit == 0 { perPage = 100 } workflows := []Workflow{} for { if limit > 0 && len(workflows) == limit { break } var result WorkflowsPayload path := fmt.Sprintf("repos/%s/actions/workflows?per_page=%d&page=%d", ghrepo.FullName(repo), perPage, page) err := client.REST(repo.RepoHost(), "GET", path, nil, &result) if err != nil { return nil, err } for _, workflow := range result.Workflows { workflows = append(workflows, workflow) if limit > 0 && len(workflows) == limit { break } } if len(result.Workflows) < perPage { break } page++ } return workflows, nil } type FilteredAllError struct { error } func selectWorkflow(p iprompter, workflows []Workflow, promptMsg string, states []WorkflowState) (*Workflow, error) { filtered := []Workflow{} candidates := []string{} for _, workflow := range workflows { for _, state := range states { if workflow.State == state { filtered = append(filtered, workflow) candidates = append(candidates, fmt.Sprintf("%s (%s)", workflow.Name, workflow.Base())) break } } } if len(candidates) == 0 { return nil, FilteredAllError{errors.New("")} } selected, err := p.Select(promptMsg, "", candidates) if err != nil { return nil, err } return &filtered[selected], nil } // FindWorkflow looks up a workflow either by numeric database ID, file name, or its Name field func FindWorkflow(client *api.Client, repo ghrepo.Interface, workflowSelector string, states []WorkflowState) ([]Workflow, error) { if workflowSelector == "" { return nil, errors.New("empty workflow selector") } if _, err := strconv.Atoi(workflowSelector); err == nil || isWorkflowFile(workflowSelector) { workflow, err := getWorkflowByID(client, repo, workflowSelector) if err != nil { var httpErr api.HTTPError if errors.As(err, &httpErr) { if httpErr.StatusCode == 404 { httpErr.Message = fmt.Sprintf("workflow %s not found on the default branch", workflowSelector) } return nil, httpErr } return nil, err } return []Workflow{*workflow}, nil } return getWorkflowsByName(client, repo, workflowSelector, states) } func GetWorkflow(client *api.Client, repo ghrepo.Interface, workflowID int64) (*Workflow, error) { return getWorkflowByID(client, repo, strconv.FormatInt(workflowID, 10)) } func isWorkflowFile(f string) bool { name := strings.ToLower(f) return strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml") } // ID can be either a numeric database ID or the workflow file name func getWorkflowByID(client *api.Client, repo ghrepo.Interface, ID string) (*Workflow, error) { var workflow Workflow path := fmt.Sprintf("repos/%s/actions/workflows/%s", ghrepo.FullName(repo), url.PathEscape(ID)) if err := client.REST(repo.RepoHost(), "GET", path, nil, &workflow); err != nil { return nil, err } return &workflow, nil } func getWorkflowsByName(client *api.Client, repo ghrepo.Interface, name string, states []WorkflowState) ([]Workflow, error) { workflows, err := GetWorkflows(client, repo, 0) if err != nil { return nil, fmt.Errorf("couldn't fetch workflows for %s: %w", ghrepo.FullName(repo), err) } var filtered []Workflow for _, workflow := range workflows { if !strings.EqualFold(workflow.Name, name) { continue } for _, state := range states { if workflow.State == state { filtered = append(filtered, workflow) break } } } return filtered, nil } func ResolveWorkflow(p iprompter, io *iostreams.IOStreams, client *api.Client, repo ghrepo.Interface, prompt bool, workflowSelector string, states []WorkflowState) (*Workflow, error) { if prompt { workflows, err := GetWorkflows(client, repo, 0) if len(workflows) == 0 { err = errors.New("no workflows are enabled") } if err != nil { var httpErr api.HTTPError if errors.As(err, &httpErr) && httpErr.StatusCode == 404 { err = errors.New("no workflows are enabled") } return nil, fmt.Errorf("could not fetch workflows for %s: %w", ghrepo.FullName(repo), err) } return selectWorkflow(p, workflows, "Select a workflow", states) } workflows, err := FindWorkflow(client, repo, workflowSelector, states) if err != nil { return nil, err } if len(workflows) == 0 { return nil, fmt.Errorf("could not find any workflows named %s", workflowSelector) } if len(workflows) == 1 { return &workflows[0], nil } if !io.CanPrompt() { errMsg := "could not resolve to a unique workflow; found:" for _, workflow := range workflows { errMsg += fmt.Sprintf(" %s", workflow.Base()) } return nil, errors.New(errMsg) } return selectWorkflow(p, workflows, "Which workflow do you mean?", states) } func GetWorkflowContent(client *api.Client, repo ghrepo.Interface, workflow Workflow, ref string) ([]byte, error) { path := fmt.Sprintf("repos/%s/contents/%s", ghrepo.FullName(repo), workflow.Path) if ref != "" { q := fmt.Sprintf("?ref=%s", url.QueryEscape(ref)) path = path + q } type Result struct { Content string } var result Result err := client.REST(repo.RepoHost(), "GET", path, nil, &result) if err != nil { return nil, err } decoded, err := base64.StdEncoding.DecodeString(result.Content) if err != nil { return nil, fmt.Errorf("failed to decode workflow file: %w", err) } sanitized, err := io.ReadAll(transform.NewReader(bytes.NewReader(decoded), &asciisanitizer.Sanitizer{})) if err != nil { return nil, err } return sanitized, nil }