github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/workflow/shared/shared.go (about)

     1  package shared
     2  
     3  import (
     4  	"encoding/base64"
     5  	"errors"
     6  	"fmt"
     7  	"net/url"
     8  	"path"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/AlecAivazis/survey/v2"
    13  	"github.com/ungtb10d/cli/v2/api"
    14  	"github.com/ungtb10d/cli/v2/internal/ghrepo"
    15  	"github.com/ungtb10d/cli/v2/pkg/iostreams"
    16  	"github.com/ungtb10d/cli/v2/pkg/prompt"
    17  )
    18  
    19  const (
    20  	Active             WorkflowState = "active"
    21  	DisabledManually   WorkflowState = "disabled_manually"
    22  	DisabledInactivity WorkflowState = "disabled_inactivity"
    23  )
    24  
    25  type WorkflowState string
    26  
    27  type Workflow struct {
    28  	Name  string
    29  	ID    int64
    30  	Path  string
    31  	State WorkflowState
    32  }
    33  
    34  type WorkflowsPayload struct {
    35  	Workflows []Workflow
    36  }
    37  
    38  func (w *Workflow) Disabled() bool {
    39  	return w.State != Active
    40  }
    41  
    42  func (w *Workflow) Base() string {
    43  	return path.Base(w.Path)
    44  }
    45  
    46  func GetWorkflows(client *api.Client, repo ghrepo.Interface, limit int) ([]Workflow, error) {
    47  	perPage := limit
    48  	page := 1
    49  	if limit > 100 || limit == 0 {
    50  		perPage = 100
    51  	}
    52  
    53  	workflows := []Workflow{}
    54  
    55  	for {
    56  		if limit > 0 && len(workflows) == limit {
    57  			break
    58  		}
    59  		var result WorkflowsPayload
    60  
    61  		path := fmt.Sprintf("repos/%s/actions/workflows?per_page=%d&page=%d", ghrepo.FullName(repo), perPage, page)
    62  
    63  		err := client.REST(repo.RepoHost(), "GET", path, nil, &result)
    64  		if err != nil {
    65  			return nil, err
    66  		}
    67  
    68  		for _, workflow := range result.Workflows {
    69  			workflows = append(workflows, workflow)
    70  			if limit > 0 && len(workflows) == limit {
    71  				break
    72  			}
    73  		}
    74  
    75  		if len(result.Workflows) < perPage {
    76  			break
    77  		}
    78  
    79  		page++
    80  	}
    81  
    82  	return workflows, nil
    83  }
    84  
    85  type FilteredAllError struct {
    86  	error
    87  }
    88  
    89  func SelectWorkflow(workflows []Workflow, promptMsg string, states []WorkflowState) (*Workflow, error) {
    90  	filtered := []Workflow{}
    91  	candidates := []string{}
    92  	for _, workflow := range workflows {
    93  		for _, state := range states {
    94  			if workflow.State == state {
    95  				filtered = append(filtered, workflow)
    96  				candidates = append(candidates, fmt.Sprintf("%s (%s)", workflow.Name, workflow.Base()))
    97  				break
    98  			}
    99  		}
   100  	}
   101  
   102  	if len(candidates) == 0 {
   103  		return nil, FilteredAllError{errors.New("")}
   104  	}
   105  
   106  	var selected int
   107  
   108  	//nolint:staticcheck // SA1019: prompt.SurveyAskOne is deprecated: use Prompter
   109  	err := prompt.SurveyAskOne(&survey.Select{
   110  		Message:  promptMsg,
   111  		Options:  candidates,
   112  		PageSize: 15,
   113  	}, &selected)
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  
   118  	return &filtered[selected], nil
   119  }
   120  
   121  // FindWorkflow looks up a workflow either by numeric database ID, file name, or its Name field
   122  func FindWorkflow(client *api.Client, repo ghrepo.Interface, workflowSelector string, states []WorkflowState) ([]Workflow, error) {
   123  	if workflowSelector == "" {
   124  		return nil, errors.New("empty workflow selector")
   125  	}
   126  
   127  	if _, err := strconv.Atoi(workflowSelector); err == nil || isWorkflowFile(workflowSelector) {
   128  		workflow, err := getWorkflowByID(client, repo, workflowSelector)
   129  		if err != nil {
   130  			return nil, err
   131  		}
   132  		return []Workflow{*workflow}, nil
   133  	}
   134  
   135  	return getWorkflowsByName(client, repo, workflowSelector, states)
   136  }
   137  
   138  func GetWorkflow(client *api.Client, repo ghrepo.Interface, workflowID int64) (*Workflow, error) {
   139  	return getWorkflowByID(client, repo, strconv.FormatInt(workflowID, 10))
   140  }
   141  
   142  func isWorkflowFile(f string) bool {
   143  	name := strings.ToLower(f)
   144  	return strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml")
   145  }
   146  
   147  // ID can be either a numeric database ID or the workflow file name
   148  func getWorkflowByID(client *api.Client, repo ghrepo.Interface, ID string) (*Workflow, error) {
   149  	var workflow Workflow
   150  
   151  	path := fmt.Sprintf("repos/%s/actions/workflows/%s", ghrepo.FullName(repo), url.PathEscape(ID))
   152  	if err := client.REST(repo.RepoHost(), "GET", path, nil, &workflow); err != nil {
   153  		return nil, err
   154  	}
   155  
   156  	return &workflow, nil
   157  }
   158  
   159  func getWorkflowsByName(client *api.Client, repo ghrepo.Interface, name string, states []WorkflowState) ([]Workflow, error) {
   160  	workflows, err := GetWorkflows(client, repo, 0)
   161  	if err != nil {
   162  		return nil, fmt.Errorf("couldn't fetch workflows for %s: %w", ghrepo.FullName(repo), err)
   163  	}
   164  
   165  	var filtered []Workflow
   166  	for _, workflow := range workflows {
   167  		if !strings.EqualFold(workflow.Name, name) {
   168  			continue
   169  		}
   170  		for _, state := range states {
   171  			if workflow.State == state {
   172  				filtered = append(filtered, workflow)
   173  				break
   174  			}
   175  		}
   176  	}
   177  
   178  	return filtered, nil
   179  }
   180  
   181  func ResolveWorkflow(io *iostreams.IOStreams, client *api.Client, repo ghrepo.Interface, prompt bool, workflowSelector string, states []WorkflowState) (*Workflow, error) {
   182  	if prompt {
   183  		workflows, err := GetWorkflows(client, repo, 0)
   184  		if len(workflows) == 0 {
   185  			err = errors.New("no workflows are enabled")
   186  		}
   187  
   188  		if err != nil {
   189  			var httpErr api.HTTPError
   190  			if errors.As(err, &httpErr) && httpErr.StatusCode == 404 {
   191  				err = errors.New("no workflows are enabled")
   192  			}
   193  
   194  			return nil, fmt.Errorf("could not fetch workflows for %s: %w", ghrepo.FullName(repo), err)
   195  		}
   196  
   197  		return SelectWorkflow(workflows, "Select a workflow", states)
   198  	}
   199  
   200  	workflows, err := FindWorkflow(client, repo, workflowSelector, states)
   201  	if err != nil {
   202  		return nil, err
   203  	}
   204  
   205  	if len(workflows) == 0 {
   206  		return nil, fmt.Errorf("could not find any workflows named %s", workflowSelector)
   207  	}
   208  
   209  	if len(workflows) == 1 {
   210  		return &workflows[0], nil
   211  	}
   212  
   213  	if !io.CanPrompt() {
   214  		errMsg := "could not resolve to a unique workflow; found:"
   215  		for _, workflow := range workflows {
   216  			errMsg += fmt.Sprintf(" %s", workflow.Base())
   217  		}
   218  		return nil, errors.New(errMsg)
   219  	}
   220  
   221  	return SelectWorkflow(workflows, "Which workflow do you mean?", states)
   222  }
   223  
   224  func GetWorkflowContent(client *api.Client, repo ghrepo.Interface, workflow Workflow, ref string) ([]byte, error) {
   225  	path := fmt.Sprintf("repos/%s/contents/%s", ghrepo.FullName(repo), workflow.Path)
   226  	if ref != "" {
   227  		q := fmt.Sprintf("?ref=%s", url.QueryEscape(ref))
   228  		path = path + q
   229  	}
   230  
   231  	type Result struct {
   232  		Content string
   233  	}
   234  
   235  	var result Result
   236  	err := client.REST(repo.RepoHost(), "GET", path, nil, &result)
   237  	if err != nil {
   238  		return nil, err
   239  	}
   240  
   241  	decoded, err := base64.StdEncoding.DecodeString(result.Content)
   242  	if err != nil {
   243  		return nil, fmt.Errorf("failed to decode workflow file: %w", err)
   244  	}
   245  
   246  	return decoded, nil
   247  }