github.com/nektos/act@v0.2.63-0.20240520024548-8acde99bfa9c/pkg/model/workflow.go (about)

     1  package model
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"reflect"
     7  	"regexp"
     8  	"strconv"
     9  	"strings"
    10  
    11  	"github.com/nektos/act/pkg/common"
    12  	log "github.com/sirupsen/logrus"
    13  	"gopkg.in/yaml.v3"
    14  )
    15  
    16  // Workflow is the structure of the files in .github/workflows
    17  type Workflow struct {
    18  	File     string
    19  	Name     string            `yaml:"name"`
    20  	RawOn    yaml.Node         `yaml:"on"`
    21  	Env      map[string]string `yaml:"env"`
    22  	Jobs     map[string]*Job   `yaml:"jobs"`
    23  	Defaults Defaults          `yaml:"defaults"`
    24  }
    25  
    26  // On events for the workflow
    27  func (w *Workflow) On() []string {
    28  	switch w.RawOn.Kind {
    29  	case yaml.ScalarNode:
    30  		var val string
    31  		err := w.RawOn.Decode(&val)
    32  		if err != nil {
    33  			log.Fatal(err)
    34  		}
    35  		return []string{val}
    36  	case yaml.SequenceNode:
    37  		var val []string
    38  		err := w.RawOn.Decode(&val)
    39  		if err != nil {
    40  			log.Fatal(err)
    41  		}
    42  		return val
    43  	case yaml.MappingNode:
    44  		var val map[string]interface{}
    45  		err := w.RawOn.Decode(&val)
    46  		if err != nil {
    47  			log.Fatal(err)
    48  		}
    49  		var keys []string
    50  		for k := range val {
    51  			keys = append(keys, k)
    52  		}
    53  		return keys
    54  	}
    55  	return nil
    56  }
    57  
    58  func (w *Workflow) OnEvent(event string) interface{} {
    59  	if w.RawOn.Kind == yaml.MappingNode {
    60  		var val map[string]interface{}
    61  		if !decodeNode(w.RawOn, &val) {
    62  			return nil
    63  		}
    64  		return val[event]
    65  	}
    66  	return nil
    67  }
    68  
    69  type WorkflowDispatchInput struct {
    70  	Description string   `yaml:"description"`
    71  	Required    bool     `yaml:"required"`
    72  	Default     string   `yaml:"default"`
    73  	Type        string   `yaml:"type"`
    74  	Options     []string `yaml:"options"`
    75  }
    76  
    77  type WorkflowDispatch struct {
    78  	Inputs map[string]WorkflowDispatchInput `yaml:"inputs"`
    79  }
    80  
    81  func (w *Workflow) WorkflowDispatchConfig() *WorkflowDispatch {
    82  	switch w.RawOn.Kind {
    83  	case yaml.ScalarNode:
    84  		var val string
    85  		if !decodeNode(w.RawOn, &val) {
    86  			return nil
    87  		}
    88  		if val == "workflow_dispatch" {
    89  			return &WorkflowDispatch{}
    90  		}
    91  	case yaml.SequenceNode:
    92  		var val []string
    93  		if !decodeNode(w.RawOn, &val) {
    94  			return nil
    95  		}
    96  		for _, v := range val {
    97  			if v == "workflow_dispatch" {
    98  				return &WorkflowDispatch{}
    99  			}
   100  		}
   101  	case yaml.MappingNode:
   102  		var val map[string]yaml.Node
   103  		if !decodeNode(w.RawOn, &val) {
   104  			return nil
   105  		}
   106  
   107  		n, found := val["workflow_dispatch"]
   108  		var workflowDispatch WorkflowDispatch
   109  		if found && decodeNode(n, &workflowDispatch) {
   110  			return &workflowDispatch
   111  		}
   112  	default:
   113  		return nil
   114  	}
   115  	return nil
   116  }
   117  
   118  type WorkflowCallInput struct {
   119  	Description string `yaml:"description"`
   120  	Required    bool   `yaml:"required"`
   121  	Default     string `yaml:"default"`
   122  	Type        string `yaml:"type"`
   123  }
   124  
   125  type WorkflowCallOutput struct {
   126  	Description string `yaml:"description"`
   127  	Value       string `yaml:"value"`
   128  }
   129  
   130  type WorkflowCall struct {
   131  	Inputs  map[string]WorkflowCallInput  `yaml:"inputs"`
   132  	Outputs map[string]WorkflowCallOutput `yaml:"outputs"`
   133  }
   134  
   135  type WorkflowCallResult struct {
   136  	Outputs map[string]string
   137  }
   138  
   139  func (w *Workflow) WorkflowCallConfig() *WorkflowCall {
   140  	if w.RawOn.Kind != yaml.MappingNode {
   141  		// The callers expect for "on: workflow_call" and "on: [ workflow_call ]" a non nil return value
   142  		return &WorkflowCall{}
   143  	}
   144  
   145  	var val map[string]yaml.Node
   146  	if !decodeNode(w.RawOn, &val) {
   147  		return &WorkflowCall{}
   148  	}
   149  
   150  	var config WorkflowCall
   151  	node := val["workflow_call"]
   152  	if !decodeNode(node, &config) {
   153  		return &WorkflowCall{}
   154  	}
   155  
   156  	return &config
   157  }
   158  
   159  // Job is the structure of one job in a workflow
   160  type Job struct {
   161  	Name           string                    `yaml:"name"`
   162  	RawNeeds       yaml.Node                 `yaml:"needs"`
   163  	RawRunsOn      yaml.Node                 `yaml:"runs-on"`
   164  	Env            yaml.Node                 `yaml:"env"`
   165  	If             yaml.Node                 `yaml:"if"`
   166  	Steps          []*Step                   `yaml:"steps"`
   167  	TimeoutMinutes string                    `yaml:"timeout-minutes"`
   168  	Services       map[string]*ContainerSpec `yaml:"services"`
   169  	Strategy       *Strategy                 `yaml:"strategy"`
   170  	RawContainer   yaml.Node                 `yaml:"container"`
   171  	Defaults       Defaults                  `yaml:"defaults"`
   172  	Outputs        map[string]string         `yaml:"outputs"`
   173  	Uses           string                    `yaml:"uses"`
   174  	With           map[string]interface{}    `yaml:"with"`
   175  	RawSecrets     yaml.Node                 `yaml:"secrets"`
   176  	Result         string
   177  }
   178  
   179  // Strategy for the job
   180  type Strategy struct {
   181  	FailFast          bool
   182  	MaxParallel       int
   183  	FailFastString    string    `yaml:"fail-fast"`
   184  	MaxParallelString string    `yaml:"max-parallel"`
   185  	RawMatrix         yaml.Node `yaml:"matrix"`
   186  }
   187  
   188  // Default settings that will apply to all steps in the job or workflow
   189  type Defaults struct {
   190  	Run RunDefaults `yaml:"run"`
   191  }
   192  
   193  // Defaults for all run steps in the job or workflow
   194  type RunDefaults struct {
   195  	Shell            string `yaml:"shell"`
   196  	WorkingDirectory string `yaml:"working-directory"`
   197  }
   198  
   199  // GetMaxParallel sets default and returns value for `max-parallel`
   200  func (s Strategy) GetMaxParallel() int {
   201  	// MaxParallel default value is `GitHub will maximize the number of jobs run in parallel depending on the available runners on GitHub-hosted virtual machines`
   202  	// So I take the liberty to hardcode default limit to 4 and this is because:
   203  	// 1: tl;dr: self-hosted does only 1 parallel job - https://github.com/actions/runner/issues/639#issuecomment-825212735
   204  	// 2: GH has 20 parallel job limit (for free tier) - https://github.com/github/docs/blob/3ae84420bd10997bb5f35f629ebb7160fe776eae/content/actions/reference/usage-limits-billing-and-administration.md?plain=1#L45
   205  	// 3: I want to add support for MaxParallel to act and 20! parallel jobs is a bit overkill IMHO
   206  	maxParallel := 4
   207  	if s.MaxParallelString != "" {
   208  		var err error
   209  		if maxParallel, err = strconv.Atoi(s.MaxParallelString); err != nil {
   210  			log.Errorf("Failed to parse 'max-parallel' option: %v", err)
   211  		}
   212  	}
   213  	return maxParallel
   214  }
   215  
   216  // GetFailFast sets default and returns value for `fail-fast`
   217  func (s Strategy) GetFailFast() bool {
   218  	// FailFast option is true by default: https://github.com/github/docs/blob/3ae84420bd10997bb5f35f629ebb7160fe776eae/content/actions/reference/workflow-syntax-for-github-actions.md?plain=1#L1107
   219  	failFast := true
   220  	log.Debug(s.FailFastString)
   221  	if s.FailFastString != "" {
   222  		var err error
   223  		if failFast, err = strconv.ParseBool(s.FailFastString); err != nil {
   224  			log.Errorf("Failed to parse 'fail-fast' option: %v", err)
   225  		}
   226  	}
   227  	return failFast
   228  }
   229  
   230  func (j *Job) InheritSecrets() bool {
   231  	if j.RawSecrets.Kind != yaml.ScalarNode {
   232  		return false
   233  	}
   234  
   235  	var val string
   236  	if !decodeNode(j.RawSecrets, &val) {
   237  		return false
   238  	}
   239  
   240  	return val == "inherit"
   241  }
   242  
   243  func (j *Job) Secrets() map[string]string {
   244  	if j.RawSecrets.Kind != yaml.MappingNode {
   245  		return nil
   246  	}
   247  
   248  	var val map[string]string
   249  	if !decodeNode(j.RawSecrets, &val) {
   250  		return nil
   251  	}
   252  
   253  	return val
   254  }
   255  
   256  // Container details for the job
   257  func (j *Job) Container() *ContainerSpec {
   258  	var val *ContainerSpec
   259  	switch j.RawContainer.Kind {
   260  	case yaml.ScalarNode:
   261  		val = new(ContainerSpec)
   262  		if !decodeNode(j.RawContainer, &val.Image) {
   263  			return nil
   264  		}
   265  	case yaml.MappingNode:
   266  		val = new(ContainerSpec)
   267  		if !decodeNode(j.RawContainer, val) {
   268  			return nil
   269  		}
   270  	}
   271  	return val
   272  }
   273  
   274  // Needs list for Job
   275  func (j *Job) Needs() []string {
   276  	switch j.RawNeeds.Kind {
   277  	case yaml.ScalarNode:
   278  		var val string
   279  		if !decodeNode(j.RawNeeds, &val) {
   280  			return nil
   281  		}
   282  		return []string{val}
   283  	case yaml.SequenceNode:
   284  		var val []string
   285  		if !decodeNode(j.RawNeeds, &val) {
   286  			return nil
   287  		}
   288  		return val
   289  	}
   290  	return nil
   291  }
   292  
   293  // RunsOn list for Job
   294  func (j *Job) RunsOn() []string {
   295  	switch j.RawRunsOn.Kind {
   296  	case yaml.MappingNode:
   297  		var val struct {
   298  			Group  string
   299  			Labels yaml.Node
   300  		}
   301  
   302  		if !decodeNode(j.RawRunsOn, &val) {
   303  			return nil
   304  		}
   305  
   306  		labels := nodeAsStringSlice(val.Labels)
   307  
   308  		if val.Group != "" {
   309  			labels = append(labels, val.Group)
   310  		}
   311  
   312  		return labels
   313  	default:
   314  		return nodeAsStringSlice(j.RawRunsOn)
   315  	}
   316  }
   317  
   318  func nodeAsStringSlice(node yaml.Node) []string {
   319  	switch node.Kind {
   320  	case yaml.ScalarNode:
   321  		var val string
   322  		if !decodeNode(node, &val) {
   323  			return nil
   324  		}
   325  		return []string{val}
   326  	case yaml.SequenceNode:
   327  		var val []string
   328  		if !decodeNode(node, &val) {
   329  			return nil
   330  		}
   331  		return val
   332  	}
   333  	return nil
   334  }
   335  
   336  func environment(yml yaml.Node) map[string]string {
   337  	env := make(map[string]string)
   338  	if yml.Kind == yaml.MappingNode {
   339  		if !decodeNode(yml, &env) {
   340  			return nil
   341  		}
   342  	}
   343  	return env
   344  }
   345  
   346  // Environment returns string-based key=value map for a job
   347  func (j *Job) Environment() map[string]string {
   348  	return environment(j.Env)
   349  }
   350  
   351  // Matrix decodes RawMatrix YAML node
   352  func (j *Job) Matrix() map[string][]interface{} {
   353  	if j.Strategy.RawMatrix.Kind == yaml.MappingNode {
   354  		var val map[string][]interface{}
   355  		if !decodeNode(j.Strategy.RawMatrix, &val) {
   356  			return nil
   357  		}
   358  		return val
   359  	}
   360  	return nil
   361  }
   362  
   363  // GetMatrixes returns the matrix cross product
   364  // It skips includes and hard fails excludes for non-existing keys
   365  //
   366  //nolint:gocyclo
   367  func (j *Job) GetMatrixes() ([]map[string]interface{}, error) {
   368  	matrixes := make([]map[string]interface{}, 0)
   369  	if j.Strategy != nil {
   370  		j.Strategy.FailFast = j.Strategy.GetFailFast()
   371  		j.Strategy.MaxParallel = j.Strategy.GetMaxParallel()
   372  
   373  		if m := j.Matrix(); m != nil {
   374  			includes := make([]map[string]interface{}, 0)
   375  			extraIncludes := make([]map[string]interface{}, 0)
   376  			for _, v := range m["include"] {
   377  				switch t := v.(type) {
   378  				case []interface{}:
   379  					for _, i := range t {
   380  						i := i.(map[string]interface{})
   381  						extraInclude := true
   382  						for k := range i {
   383  							if _, ok := m[k]; ok {
   384  								includes = append(includes, i)
   385  								extraInclude = false
   386  								break
   387  							}
   388  						}
   389  						if extraInclude {
   390  							extraIncludes = append(extraIncludes, i)
   391  						}
   392  					}
   393  				case interface{}:
   394  					v := v.(map[string]interface{})
   395  					extraInclude := true
   396  					for k := range v {
   397  						if _, ok := m[k]; ok {
   398  							includes = append(includes, v)
   399  							extraInclude = false
   400  							break
   401  						}
   402  					}
   403  					if extraInclude {
   404  						extraIncludes = append(extraIncludes, v)
   405  					}
   406  				}
   407  			}
   408  			delete(m, "include")
   409  
   410  			excludes := make([]map[string]interface{}, 0)
   411  			for _, e := range m["exclude"] {
   412  				e := e.(map[string]interface{})
   413  				for k := range e {
   414  					if _, ok := m[k]; ok {
   415  						excludes = append(excludes, e)
   416  					} else {
   417  						// We fail completely here because that's what GitHub does for non-existing matrix keys, fail on exclude, silent skip on include
   418  						return nil, fmt.Errorf("the workflow is not valid. Matrix exclude key %q does not match any key within the matrix", k)
   419  					}
   420  				}
   421  			}
   422  			delete(m, "exclude")
   423  
   424  			matrixProduct := common.CartesianProduct(m)
   425  		MATRIX:
   426  			for _, matrix := range matrixProduct {
   427  				for _, exclude := range excludes {
   428  					if commonKeysMatch(matrix, exclude) {
   429  						log.Debugf("Skipping matrix '%v' due to exclude '%v'", matrix, exclude)
   430  						continue MATRIX
   431  					}
   432  				}
   433  				matrixes = append(matrixes, matrix)
   434  			}
   435  			for _, include := range includes {
   436  				matched := false
   437  				for _, matrix := range matrixes {
   438  					if commonKeysMatch2(matrix, include, m) {
   439  						matched = true
   440  						log.Debugf("Adding include values '%v' to existing entry", include)
   441  						for k, v := range include {
   442  							matrix[k] = v
   443  						}
   444  					}
   445  				}
   446  				if !matched {
   447  					extraIncludes = append(extraIncludes, include)
   448  				}
   449  			}
   450  			for _, include := range extraIncludes {
   451  				log.Debugf("Adding include '%v'", include)
   452  				matrixes = append(matrixes, include)
   453  			}
   454  			if len(matrixes) == 0 {
   455  				matrixes = append(matrixes, make(map[string]interface{}))
   456  			}
   457  		} else {
   458  			matrixes = append(matrixes, make(map[string]interface{}))
   459  		}
   460  	} else {
   461  		matrixes = append(matrixes, make(map[string]interface{}))
   462  		log.Debugf("Empty Strategy, matrixes=%v", matrixes)
   463  	}
   464  	return matrixes, nil
   465  }
   466  
   467  func commonKeysMatch(a map[string]interface{}, b map[string]interface{}) bool {
   468  	for aKey, aVal := range a {
   469  		if bVal, ok := b[aKey]; ok && !reflect.DeepEqual(aVal, bVal) {
   470  			return false
   471  		}
   472  	}
   473  	return true
   474  }
   475  
   476  func commonKeysMatch2(a map[string]interface{}, b map[string]interface{}, m map[string][]interface{}) bool {
   477  	for aKey, aVal := range a {
   478  		_, useKey := m[aKey]
   479  		if bVal, ok := b[aKey]; useKey && ok && !reflect.DeepEqual(aVal, bVal) {
   480  			return false
   481  		}
   482  	}
   483  	return true
   484  }
   485  
   486  // JobType describes what type of job we are about to run
   487  type JobType int
   488  
   489  const (
   490  	// JobTypeDefault is all jobs that have a `run` attribute
   491  	JobTypeDefault JobType = iota
   492  
   493  	// JobTypeReusableWorkflowLocal is all jobs that have a `uses` that is a local workflow in the .github/workflows directory
   494  	JobTypeReusableWorkflowLocal
   495  
   496  	// JobTypeReusableWorkflowRemote is all jobs that have a `uses` that references a workflow file in a github repo
   497  	JobTypeReusableWorkflowRemote
   498  
   499  	// JobTypeInvalid represents a job which is not configured correctly
   500  	JobTypeInvalid
   501  )
   502  
   503  func (j JobType) String() string {
   504  	switch j {
   505  	case JobTypeDefault:
   506  		return "default"
   507  	case JobTypeReusableWorkflowLocal:
   508  		return "local-reusable-workflow"
   509  	case JobTypeReusableWorkflowRemote:
   510  		return "remote-reusable-workflow"
   511  	}
   512  	return "unknown"
   513  }
   514  
   515  // Type returns the type of the job
   516  func (j *Job) Type() (JobType, error) {
   517  	isReusable := j.Uses != ""
   518  
   519  	if isReusable {
   520  		isYaml, _ := regexp.MatchString(`\.(ya?ml)(?:$|@)`, j.Uses)
   521  
   522  		if isYaml {
   523  			isLocalPath := strings.HasPrefix(j.Uses, "./")
   524  			isRemotePath, _ := regexp.MatchString(`^[^.](.+?/){2,}.+\.ya?ml@`, j.Uses)
   525  			hasVersion, _ := regexp.MatchString(`\.ya?ml@`, j.Uses)
   526  
   527  			if isLocalPath {
   528  				return JobTypeReusableWorkflowLocal, nil
   529  			} else if isRemotePath && hasVersion {
   530  				return JobTypeReusableWorkflowRemote, nil
   531  			}
   532  		}
   533  
   534  		return JobTypeInvalid, fmt.Errorf("`uses` key references invalid workflow path '%s'. Must start with './' if it's a local workflow, or must start with '<org>/<repo>/' and include an '@' if it's a remote workflow", j.Uses)
   535  	}
   536  
   537  	return JobTypeDefault, nil
   538  }
   539  
   540  // ContainerSpec is the specification of the container to use for the job
   541  type ContainerSpec struct {
   542  	Image       string            `yaml:"image"`
   543  	Env         map[string]string `yaml:"env"`
   544  	Ports       []string          `yaml:"ports"`
   545  	Volumes     []string          `yaml:"volumes"`
   546  	Options     string            `yaml:"options"`
   547  	Credentials map[string]string `yaml:"credentials"`
   548  	Entrypoint  string
   549  	Args        string
   550  	Name        string
   551  	Reuse       bool
   552  }
   553  
   554  // Step is the structure of one step in a job
   555  type Step struct {
   556  	ID                 string            `yaml:"id"`
   557  	If                 yaml.Node         `yaml:"if"`
   558  	Name               string            `yaml:"name"`
   559  	Uses               string            `yaml:"uses"`
   560  	Run                string            `yaml:"run"`
   561  	WorkingDirectory   string            `yaml:"working-directory"`
   562  	Shell              string            `yaml:"shell"`
   563  	Env                yaml.Node         `yaml:"env"`
   564  	With               map[string]string `yaml:"with"`
   565  	RawContinueOnError string            `yaml:"continue-on-error"`
   566  	TimeoutMinutes     string            `yaml:"timeout-minutes"`
   567  }
   568  
   569  // String gets the name of step
   570  func (s *Step) String() string {
   571  	if s.Name != "" {
   572  		return s.Name
   573  	} else if s.Uses != "" {
   574  		return s.Uses
   575  	} else if s.Run != "" {
   576  		return s.Run
   577  	}
   578  	return s.ID
   579  }
   580  
   581  // Environment returns string-based key=value map for a step
   582  func (s *Step) Environment() map[string]string {
   583  	return environment(s.Env)
   584  }
   585  
   586  // GetEnv gets the env for a step
   587  func (s *Step) GetEnv() map[string]string {
   588  	env := s.Environment()
   589  
   590  	for k, v := range s.With {
   591  		envKey := regexp.MustCompile("[^A-Z0-9-]").ReplaceAllString(strings.ToUpper(k), "_")
   592  		envKey = fmt.Sprintf("INPUT_%s", strings.ToUpper(envKey))
   593  		env[envKey] = v
   594  	}
   595  	return env
   596  }
   597  
   598  // ShellCommand returns the command for the shell
   599  func (s *Step) ShellCommand() string {
   600  	shellCommand := ""
   601  
   602  	//Reference: https://github.com/actions/runner/blob/8109c962f09d9acc473d92c595ff43afceddb347/src/Runner.Worker/Handlers/ScriptHandlerHelpers.cs#L9-L17
   603  	switch s.Shell {
   604  	case "", "bash":
   605  		shellCommand = "bash --noprofile --norc -e -o pipefail {0}"
   606  	case "pwsh":
   607  		shellCommand = "pwsh -command . '{0}'"
   608  	case "python":
   609  		shellCommand = "python {0}"
   610  	case "sh":
   611  		shellCommand = "sh -e {0}"
   612  	case "cmd":
   613  		shellCommand = "cmd /D /E:ON /V:OFF /S /C \"CALL \"{0}\"\""
   614  	case "powershell":
   615  		shellCommand = "powershell -command . '{0}'"
   616  	default:
   617  		shellCommand = s.Shell
   618  	}
   619  	return shellCommand
   620  }
   621  
   622  // StepType describes what type of step we are about to run
   623  type StepType int
   624  
   625  const (
   626  	// StepTypeRun is all steps that have a `run` attribute
   627  	StepTypeRun StepType = iota
   628  
   629  	// StepTypeUsesDockerURL is all steps that have a `uses` that is of the form `docker://...`
   630  	StepTypeUsesDockerURL
   631  
   632  	// StepTypeUsesActionLocal is all steps that have a `uses` that is a local action in a subdirectory
   633  	StepTypeUsesActionLocal
   634  
   635  	// StepTypeUsesActionRemote is all steps that have a `uses` that is a reference to a github repo
   636  	StepTypeUsesActionRemote
   637  
   638  	// StepTypeReusableWorkflowLocal is all steps that have a `uses` that is a local workflow in the .github/workflows directory
   639  	StepTypeReusableWorkflowLocal
   640  
   641  	// StepTypeReusableWorkflowRemote is all steps that have a `uses` that references a workflow file in a github repo
   642  	StepTypeReusableWorkflowRemote
   643  
   644  	// StepTypeInvalid is for steps that have invalid step action
   645  	StepTypeInvalid
   646  )
   647  
   648  func (s StepType) String() string {
   649  	switch s {
   650  	case StepTypeInvalid:
   651  		return "invalid"
   652  	case StepTypeRun:
   653  		return "run"
   654  	case StepTypeUsesActionLocal:
   655  		return "local-action"
   656  	case StepTypeUsesActionRemote:
   657  		return "remote-action"
   658  	case StepTypeUsesDockerURL:
   659  		return "docker"
   660  	case StepTypeReusableWorkflowLocal:
   661  		return "local-reusable-workflow"
   662  	case StepTypeReusableWorkflowRemote:
   663  		return "remote-reusable-workflow"
   664  	}
   665  	return "unknown"
   666  }
   667  
   668  // Type returns the type of the step
   669  func (s *Step) Type() StepType {
   670  	if s.Run == "" && s.Uses == "" {
   671  		return StepTypeInvalid
   672  	}
   673  
   674  	if s.Run != "" {
   675  		if s.Uses != "" {
   676  			return StepTypeInvalid
   677  		}
   678  		return StepTypeRun
   679  	} else if strings.HasPrefix(s.Uses, "docker://") {
   680  		return StepTypeUsesDockerURL
   681  	} else if strings.HasPrefix(s.Uses, "./.github/workflows") && (strings.HasSuffix(s.Uses, ".yml") || strings.HasSuffix(s.Uses, ".yaml")) {
   682  		return StepTypeReusableWorkflowLocal
   683  	} else if !strings.HasPrefix(s.Uses, "./") && strings.Contains(s.Uses, ".github/workflows") && (strings.Contains(s.Uses, ".yml@") || strings.Contains(s.Uses, ".yaml@")) {
   684  		return StepTypeReusableWorkflowRemote
   685  	} else if strings.HasPrefix(s.Uses, "./") {
   686  		return StepTypeUsesActionLocal
   687  	}
   688  	return StepTypeUsesActionRemote
   689  }
   690  
   691  // ReadWorkflow returns a list of jobs for a given workflow file reader
   692  func ReadWorkflow(in io.Reader) (*Workflow, error) {
   693  	w := new(Workflow)
   694  	err := yaml.NewDecoder(in).Decode(w)
   695  	return w, err
   696  }
   697  
   698  // GetJob will get a job by name in the workflow
   699  func (w *Workflow) GetJob(jobID string) *Job {
   700  	for id, j := range w.Jobs {
   701  		if jobID == id {
   702  			if j.Name == "" {
   703  				j.Name = id
   704  			}
   705  			if j.If.Value == "" {
   706  				j.If.Value = "success()"
   707  			}
   708  			return j
   709  		}
   710  	}
   711  	return nil
   712  }
   713  
   714  // GetJobIDs will get all the job names in the workflow
   715  func (w *Workflow) GetJobIDs() []string {
   716  	ids := make([]string, 0)
   717  	for id := range w.Jobs {
   718  		ids = append(ids, id)
   719  	}
   720  	return ids
   721  }
   722  
   723  var OnDecodeNodeError = func(node yaml.Node, out interface{}, err error) {
   724  	log.Fatalf("Failed to decode node %v into %T: %v", node, out, err)
   725  }
   726  
   727  func decodeNode(node yaml.Node, out interface{}) bool {
   728  	if err := node.Decode(out); err != nil {
   729  		if OnDecodeNodeError != nil {
   730  			OnDecodeNodeError(node, out, err)
   731  		}
   732  		return false
   733  	}
   734  	return true
   735  }