2020-02-04 18:38:41 -06:00
package model
import (
2020-02-07 00:17:58 -06:00
"fmt"
2020-02-04 18:38:41 -06:00
"io"
2020-12-08 12:13:07 -06:00
"reflect"
2020-02-10 01:03:12 -06:00
"regexp"
2021-08-09 10:35:05 -05:00
"strconv"
2020-02-07 00:17:58 -06:00
"strings"
2020-02-04 18:38:41 -06:00
2020-02-23 17:01:25 -06:00
"github.com/nektos/act/pkg/common"
log "github.com/sirupsen/logrus"
2020-02-10 18:35:00 -06:00
"gopkg.in/yaml.v3"
2020-02-04 18:38:41 -06:00
)
// Workflow is the structure of the files in .github/workflows
type Workflow struct {
2021-08-30 10:38:03 -05:00
File string
2020-08-28 13:52:25 -05:00
Name string ` yaml:"name" `
RawOn yaml . Node ` yaml:"on" `
Env map [ string ] string ` yaml:"env" `
Jobs map [ string ] * Job ` yaml:"jobs" `
Defaults Defaults ` yaml:"defaults" `
2020-02-10 18:35:00 -06:00
}
// On events for the workflow
func ( w * Workflow ) On ( ) [ ] string {
switch w . RawOn . Kind {
case yaml . ScalarNode :
var val string
2020-02-10 18:53:14 -06:00
err := w . RawOn . Decode ( & val )
if err != nil {
log . Fatal ( err )
}
2020-02-10 18:35:00 -06:00
return [ ] string { val }
case yaml . SequenceNode :
var val [ ] string
2020-02-10 18:53:14 -06:00
err := w . RawOn . Decode ( & val )
if err != nil {
log . Fatal ( err )
}
2020-02-10 18:35:00 -06:00
return val
case yaml . MappingNode :
var val map [ string ] interface { }
2020-02-10 18:53:14 -06:00
err := w . RawOn . Decode ( & val )
if err != nil {
log . Fatal ( err )
}
2020-02-10 18:35:00 -06:00
var keys [ ] string
for k := range val {
keys = append ( keys , k )
}
return keys
}
return nil
2020-02-04 18:38:41 -06:00
}
2022-10-17 11:25:26 -05:00
func ( w * Workflow ) OnEvent ( event string ) interface { } {
if w . RawOn . Kind == yaml . MappingNode {
var val map [ string ] interface { }
err := w . RawOn . Decode ( & val )
if err != nil {
log . Fatal ( err )
}
return val [ event ]
}
return nil
}
type WorkflowDispatchInput struct {
Description string ` yaml:"description" `
Required bool ` yaml:"required" `
Default string ` yaml:"default" `
Type string ` yaml:"type" `
Options [ ] string ` yaml:"options" `
}
type WorkflowDispatch struct {
Inputs map [ string ] WorkflowDispatchInput ` yaml:"inputs" `
}
func ( w * Workflow ) WorkflowDispatchConfig ( ) * WorkflowDispatch {
if w . RawOn . Kind != yaml . MappingNode {
return nil
}
var val map [ string ] yaml . Node
err := w . RawOn . Decode ( & val )
if err != nil {
log . Fatal ( err )
}
var config WorkflowDispatch
node := val [ "workflow_dispatch" ]
err = node . Decode ( & config )
if err != nil {
log . Fatal ( err )
}
return & config
}
2020-02-04 18:38:41 -06:00
// Job is the structure of one job in a workflow
type Job struct {
2020-02-07 00:17:58 -06:00
Name string ` yaml:"name" `
2020-02-10 18:35:00 -06:00
RawNeeds yaml . Node ` yaml:"needs" `
2020-03-16 16:58:10 -05:00
RawRunsOn yaml . Node ` yaml:"runs-on" `
2021-08-09 10:35:05 -05:00
Env yaml . Node ` yaml:"env" `
2021-05-05 15:04:03 -05:00
If yaml . Node ` yaml:"if" `
2020-02-07 00:17:58 -06:00
Steps [ ] * Step ` yaml:"steps" `
2022-07-07 19:31:19 -05:00
TimeoutMinutes string ` yaml:"timeout-minutes" `
2020-02-07 00:17:58 -06:00
Services map [ string ] * ContainerSpec ` yaml:"services" `
2020-02-14 02:41:20 -06:00
Strategy * Strategy ` yaml:"strategy" `
2020-02-25 00:35:08 -06:00
RawContainer yaml . Node ` yaml:"container" `
2020-08-28 13:52:25 -05:00
Defaults Defaults ` yaml:"defaults" `
2021-07-01 10:20:20 -05:00
Outputs map [ string ] string ` yaml:"outputs" `
2022-03-30 12:20:45 -05:00
Uses string ` yaml:"uses" `
2021-12-08 14:57:42 -06:00
Result string
2020-02-14 02:41:20 -06:00
}
// Strategy for the job
type Strategy struct {
2021-08-09 10:35:05 -05:00
FailFast bool
MaxParallel int
FailFastString string ` yaml:"fail-fast" `
MaxParallelString string ` yaml:"max-parallel" `
RawMatrix yaml . Node ` yaml:"matrix" `
2020-02-07 00:17:58 -06:00
}
2020-08-28 13:52:25 -05:00
// Default settings that will apply to all steps in the job or workflow
type Defaults struct {
Run RunDefaults ` yaml:"run" `
}
// Defaults for all run steps in the job or workflow
type RunDefaults struct {
Shell string ` yaml:"shell" `
WorkingDirectory string ` yaml:"working-directory" `
}
2021-08-09 10:35:05 -05:00
// GetMaxParallel sets default and returns value for `max-parallel`
func ( s Strategy ) GetMaxParallel ( ) int {
// MaxParallel default value is `GitHub will maximize the number of jobs run in parallel depending on the available runners on GitHub-hosted virtual machines`
// So I take the liberty to hardcode default limit to 4 and this is because:
// 1: tl;dr: self-hosted does only 1 parallel job - https://github.com/actions/runner/issues/639#issuecomment-825212735
// 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
// 3: I want to add support for MaxParallel to act and 20! parallel jobs is a bit overkill IMHO
maxParallel := 4
if s . MaxParallelString != "" {
var err error
if maxParallel , err = strconv . Atoi ( s . MaxParallelString ) ; err != nil {
log . Errorf ( "Failed to parse 'max-parallel' option: %v" , err )
}
}
return maxParallel
}
// GetFailFast sets default and returns value for `fail-fast`
func ( s Strategy ) GetFailFast ( ) bool {
// 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
failFast := true
log . Debug ( s . FailFastString )
if s . FailFastString != "" {
var err error
if failFast , err = strconv . ParseBool ( s . FailFastString ) ; err != nil {
log . Errorf ( "Failed to parse 'fail-fast' option: %v" , err )
}
}
return failFast
}
2020-02-25 00:35:08 -06:00
// Container details for the job
func ( j * Job ) Container ( ) * ContainerSpec {
var val * ContainerSpec
switch j . RawContainer . Kind {
case yaml . ScalarNode :
val = new ( ContainerSpec )
err := j . RawContainer . Decode ( & val . Image )
if err != nil {
log . Fatal ( err )
}
case yaml . MappingNode :
val = new ( ContainerSpec )
err := j . RawContainer . Decode ( val )
if err != nil {
log . Fatal ( err )
}
}
return val
}
2020-02-10 18:35:00 -06:00
// Needs list for Job
func ( j * Job ) Needs ( ) [ ] string {
switch j . RawNeeds . Kind {
case yaml . ScalarNode :
var val string
2020-02-10 18:53:14 -06:00
err := j . RawNeeds . Decode ( & val )
if err != nil {
log . Fatal ( err )
}
2020-02-10 18:35:00 -06:00
return [ ] string { val }
case yaml . SequenceNode :
var val [ ] string
2020-02-10 18:53:14 -06:00
err := j . RawNeeds . Decode ( & val )
if err != nil {
log . Fatal ( err )
}
2020-02-10 18:35:00 -06:00
return val
}
return nil
}
2020-03-16 16:58:10 -05:00
// RunsOn list for Job
func ( j * Job ) RunsOn ( ) [ ] string {
switch j . RawRunsOn . Kind {
case yaml . ScalarNode :
var val string
err := j . RawRunsOn . Decode ( & val )
if err != nil {
log . Fatal ( err )
}
return [ ] string { val }
case yaml . SequenceNode :
var val [ ] string
err := j . RawRunsOn . Decode ( & val )
if err != nil {
log . Fatal ( err )
}
return val
}
return nil
}
2021-08-09 10:35:05 -05:00
func environment ( yml yaml . Node ) map [ string ] string {
2021-06-06 09:53:18 -05:00
env := make ( map [ string ] string )
2021-08-09 10:35:05 -05:00
if yml . Kind == yaml . MappingNode {
if err := yml . Decode ( & env ) ; err != nil {
log . Fatal ( err )
2021-06-06 09:53:18 -05:00
}
}
return env
}
2021-08-09 10:35:05 -05:00
// Environments returns string-based key=value map for a job
2021-06-06 09:53:18 -05:00
func ( j * Job ) Environment ( ) map [ string ] string {
return environment ( j . Env )
}
2021-08-09 10:35:05 -05:00
// Matrix decodes RawMatrix YAML node
2021-06-06 09:53:18 -05:00
func ( j * Job ) Matrix ( ) map [ string ] [ ] interface { } {
2021-08-09 10:35:05 -05:00
if j . Strategy . RawMatrix . Kind == yaml . MappingNode {
var val map [ string ] [ ] interface { }
if err := j . Strategy . RawMatrix . Decode ( & val ) ; err != nil {
log . Fatal ( err )
2021-06-06 09:53:18 -05:00
}
2021-08-09 10:35:05 -05:00
return val
2021-06-06 09:53:18 -05:00
}
return nil
}
2020-02-23 17:01:25 -06:00
// GetMatrixes returns the matrix cross product
2021-08-09 10:35:05 -05:00
// It skips includes and hard fails excludes for non-existing keys
2022-10-06 16:58:16 -05:00
//
//nolint:gocyclo
2020-02-23 17:01:25 -06:00
func ( j * Job ) GetMatrixes ( ) [ ] map [ string ] interface { } {
matrixes := make ( [ ] map [ string ] interface { } , 0 )
if j . Strategy != nil {
2021-08-09 10:35:05 -05:00
j . Strategy . FailFast = j . Strategy . GetFailFast ( )
j . Strategy . MaxParallel = j . Strategy . GetMaxParallel ( )
if m := j . Matrix ( ) ; m != nil {
includes := make ( [ ] map [ string ] interface { } , 0 )
2022-06-20 17:33:07 -05:00
extraIncludes := make ( [ ] map [ string ] interface { } , 0 )
2021-08-09 10:35:05 -05:00
for _ , v := range m [ "include" ] {
switch t := v . ( type ) {
case [ ] interface { } :
for _ , i := range t {
i := i . ( map [ string ] interface { } )
2022-06-20 17:33:07 -05:00
extraInclude := true
2021-08-09 10:35:05 -05:00
for k := range i {
if _ , ok := m [ k ] ; ok {
includes = append ( includes , i )
2022-06-20 17:33:07 -05:00
extraInclude = false
2021-08-09 10:35:05 -05:00
break
}
}
2022-06-20 17:33:07 -05:00
if extraInclude {
extraIncludes = append ( extraIncludes , i )
}
2021-08-09 10:35:05 -05:00
}
case interface { } :
v := v . ( map [ string ] interface { } )
2022-06-20 17:33:07 -05:00
extraInclude := true
2021-08-09 10:35:05 -05:00
for k := range v {
if _ , ok := m [ k ] ; ok {
includes = append ( includes , v )
2022-06-20 17:33:07 -05:00
extraInclude = false
2021-08-09 10:35:05 -05:00
break
}
}
2022-06-20 17:33:07 -05:00
if extraInclude {
extraIncludes = append ( extraIncludes , v )
}
2021-06-06 09:53:18 -05:00
}
}
2021-08-09 10:35:05 -05:00
delete ( m , "include" )
2020-02-23 17:01:25 -06:00
2021-08-09 10:35:05 -05:00
excludes := make ( [ ] map [ string ] interface { } , 0 )
for _ , e := range m [ "exclude" ] {
e := e . ( map [ string ] interface { } )
for k := range e {
if _ , ok := m [ k ] ; ok {
excludes = append ( excludes , e )
} else {
// We fail completely here because that's what GitHub does for non-existing matrix keys, fail on exclude, silent skip on include
log . Fatalf ( "The workflow is not valid. Matrix exclude key '%s' does not match any key within the matrix" , k )
}
}
}
delete ( m , "exclude" )
2020-02-23 17:01:25 -06:00
2021-08-09 10:35:05 -05:00
matrixProduct := common . CartesianProduct ( m )
MATRIX :
for _ , matrix := range matrixProduct {
for _ , exclude := range excludes {
if commonKeysMatch ( matrix , exclude ) {
log . Debugf ( "Skipping matrix '%v' due to exclude '%v'" , matrix , exclude )
continue MATRIX
}
2020-02-23 17:01:25 -06:00
}
2021-08-09 10:35:05 -05:00
matrixes = append ( matrixes , matrix )
2020-02-23 17:01:25 -06:00
}
2021-08-09 10:35:05 -05:00
for _ , include := range includes {
2022-06-20 17:33:07 -05:00
matched := false
for _ , matrix := range matrixes {
if commonKeysMatch2 ( matrix , include , m ) {
matched = true
log . Debugf ( "Adding include values '%v' to existing entry" , include )
for k , v := range include {
matrix [ k ] = v
}
}
}
if ! matched {
extraIncludes = append ( extraIncludes , include )
}
}
for _ , include := range extraIncludes {
2021-08-09 10:35:05 -05:00
log . Debugf ( "Adding include '%v'" , include )
matrixes = append ( matrixes , include )
}
2022-06-20 17:33:07 -05:00
if len ( matrixes ) == 0 {
matrixes = append ( matrixes , make ( map [ string ] interface { } ) )
}
2021-08-09 10:35:05 -05:00
} else {
matrixes = append ( matrixes , make ( map [ string ] interface { } ) )
2020-12-08 12:13:07 -06:00
}
2020-02-23 17:01:25 -06:00
} else {
matrixes = append ( matrixes , make ( map [ string ] interface { } ) )
}
return matrixes
}
func commonKeysMatch ( a map [ string ] interface { } , b map [ string ] interface { } ) bool {
for aKey , aVal := range a {
2021-01-14 23:37:38 -06:00
if bVal , ok := b [ aKey ] ; ok && ! reflect . DeepEqual ( aVal , bVal ) {
2020-02-23 17:01:25 -06:00
return false
}
}
return true
}
2022-06-20 17:33:07 -05:00
func commonKeysMatch2 ( a map [ string ] interface { } , b map [ string ] interface { } , m map [ string ] [ ] interface { } ) bool {
for aKey , aVal := range a {
_ , useKey := m [ aKey ]
if bVal , ok := b [ aKey ] ; useKey && ok && ! reflect . DeepEqual ( aVal , bVal ) {
return false
}
}
return true
}
2022-11-01 10:58:07 -05:00
// JobType describes what type of job we are about to run
type JobType int
const (
// StepTypeRun is all steps that have a `run` attribute
JobTypeDefault JobType = iota
// StepTypeReusableWorkflowLocal is all steps that have a `uses` that is a local workflow in the .github/workflows directory
JobTypeReusableWorkflowLocal
// JobTypeReusableWorkflowRemote is all steps that have a `uses` that references a workflow file in a github repo
JobTypeReusableWorkflowRemote
)
func ( j JobType ) String ( ) string {
switch j {
case JobTypeDefault :
return "default"
case JobTypeReusableWorkflowLocal :
return "local-reusable-workflow"
case JobTypeReusableWorkflowRemote :
return "remote-reusable-workflow"
}
return "unknown"
}
// Type returns the type of the job
func ( j * Job ) Type ( ) JobType {
if strings . HasPrefix ( j . Uses , "./.github/workflows" ) && ( strings . HasSuffix ( j . Uses , ".yml" ) || strings . HasSuffix ( j . Uses , ".yaml" ) ) {
return JobTypeReusableWorkflowLocal
} else if ! strings . HasPrefix ( j . Uses , "./" ) && strings . Contains ( j . Uses , ".github/workflows" ) && ( strings . Contains ( j . Uses , ".yml@" ) || strings . Contains ( j . Uses , ".yaml@" ) ) {
return JobTypeReusableWorkflowRemote
}
return JobTypeDefault
}
2020-02-07 00:17:58 -06:00
// ContainerSpec is the specification of the container to use for the job
type ContainerSpec struct {
2021-11-27 12:05:56 -06:00
Image string ` yaml:"image" `
Env map [ string ] string ` yaml:"env" `
Ports [ ] string ` yaml:"ports" `
Volumes [ ] string ` yaml:"volumes" `
Options string ` yaml:"options" `
Credentials map [ string ] string ` yaml:"credentials" `
Entrypoint string
Args string
Name string
Reuse bool
2020-02-04 18:38:41 -06:00
}
// Step is the structure of one step in a job
type Step struct {
2022-09-08 09:20:39 -05:00
ID string ` yaml:"id" `
If yaml . Node ` yaml:"if" `
Name string ` yaml:"name" `
Uses string ` yaml:"uses" `
Run string ` yaml:"run" `
WorkingDirectory string ` yaml:"working-directory" `
Shell string ` yaml:"shell" `
Env yaml . Node ` yaml:"env" `
With map [ string ] string ` yaml:"with" `
RawContinueOnError string ` yaml:"continue-on-error" `
TimeoutMinutes string ` yaml:"timeout-minutes" `
2020-02-04 18:38:41 -06:00
}
2020-02-11 11:10:35 -06:00
// String gets the name of step
func ( s * Step ) String ( ) string {
if s . Name != "" {
return s . Name
} else if s . Uses != "" {
return s . Uses
} else if s . Run != "" {
return s . Run
}
return s . ID
}
2021-08-09 10:35:05 -05:00
// Environments returns string-based key=value map for a step
2022-10-06 16:58:16 -05:00
// Note: all keys are uppercase
2021-06-06 09:53:18 -05:00
func ( s * Step ) Environment ( ) map [ string ] string {
2022-10-06 16:58:16 -05:00
env := environment ( s . Env )
for k , v := range env {
delete ( env , k )
env [ strings . ToUpper ( k ) ] = v
}
return env
2021-06-06 09:53:18 -05:00
}
2020-02-07 00:17:58 -06:00
// GetEnv gets the env for a step
func ( s * Step ) GetEnv ( ) map [ string ] string {
2021-06-06 09:53:18 -05:00
env := s . Environment ( )
2020-02-07 00:17:58 -06:00
for k , v := range s . With {
2020-02-10 17:27:05 -06:00
envKey := regexp . MustCompile ( "[^A-Z0-9-]" ) . ReplaceAllString ( strings . ToUpper ( k ) , "_" )
envKey = fmt . Sprintf ( "INPUT_%s" , strings . ToUpper ( envKey ) )
2021-06-06 09:53:18 -05:00
env [ envKey ] = v
2020-02-07 00:17:58 -06:00
}
2021-06-06 09:53:18 -05:00
return env
2020-02-07 00:17:58 -06:00
}
2020-02-23 17:01:25 -06:00
// ShellCommand returns the command for the shell
func ( s * Step ) ShellCommand ( ) string {
shellCommand := ""
2021-03-29 12:06:51 -05:00
//Reference: https://github.com/actions/runner/blob/8109c962f09d9acc473d92c595ff43afceddb347/src/Runner.Worker/Handlers/ScriptHandlerHelpers.cs#L9-L17
2020-02-23 17:01:25 -06:00
switch s . Shell {
case "" , "bash" :
2021-05-05 18:11:43 -05:00
shellCommand = "bash --noprofile --norc -e -o pipefail {0}"
2020-02-23 17:01:25 -06:00
case "pwsh" :
2021-05-05 00:57:33 -05:00
shellCommand = "pwsh -command . '{0}'"
2020-02-23 17:01:25 -06:00
case "python" :
shellCommand = "python {0}"
case "sh" :
2021-05-05 18:11:43 -05:00
shellCommand = "sh -e -c {0}"
2020-02-23 17:01:25 -06:00
case "cmd" :
shellCommand = "%ComSpec% /D /E:ON /V:OFF /S /C \"CALL \"{0}\"\""
case "powershell" :
2021-03-29 12:06:51 -05:00
shellCommand = "powershell -command . '{0}'"
2020-02-23 17:01:25 -06:00
default :
shellCommand = s . Shell
}
return shellCommand
}
2020-02-10 01:03:12 -06:00
// StepType describes what type of step we are about to run
type StepType int
const (
// StepTypeRun is all steps that have a `run` attribute
StepTypeRun StepType = iota
2021-01-12 00:39:43 -06:00
// StepTypeUsesDockerURL is all steps that have a `uses` that is of the form `docker://...`
2020-02-10 01:03:12 -06:00
StepTypeUsesDockerURL
2021-01-12 00:39:43 -06:00
// StepTypeUsesActionLocal is all steps that have a `uses` that is a local action in a subdirectory
2020-02-10 01:03:12 -06:00
StepTypeUsesActionLocal
2021-01-12 00:39:43 -06:00
// StepTypeUsesActionRemote is all steps that have a `uses` that is a reference to a github repo
2020-02-10 01:03:12 -06:00
StepTypeUsesActionRemote
2021-04-01 13:36:41 -05:00
2022-11-01 10:58:07 -05:00
// StepTypeReusableWorkflowLocal is all steps that have a `uses` that is a local workflow in the .github/workflows directory
StepTypeReusableWorkflowLocal
// StepTypeReusableWorkflowRemote is all steps that have a `uses` that references a workflow file in a github repo
StepTypeReusableWorkflowRemote
2021-04-01 13:36:41 -05:00
// StepTypeInvalid is for steps that have invalid step action
StepTypeInvalid
2020-02-10 01:03:12 -06:00
)
2022-10-06 16:58:16 -05:00
func ( s StepType ) String ( ) string {
switch s {
case StepTypeInvalid :
return "invalid"
case StepTypeRun :
return "run"
case StepTypeUsesActionLocal :
return "local-action"
case StepTypeUsesActionRemote :
return "remote-action"
case StepTypeUsesDockerURL :
return "docker"
2022-11-01 10:58:07 -05:00
case StepTypeReusableWorkflowLocal :
return "local-reusable-workflow"
case StepTypeReusableWorkflowRemote :
return "remote-reusable-workflow"
2022-10-06 16:58:16 -05:00
}
return "unknown"
}
2020-02-10 01:03:12 -06:00
// Type returns the type of the step
func ( s * Step ) Type ( ) StepType {
2022-05-11 14:30:18 -05:00
if s . Run == "" && s . Uses == "" {
return StepTypeInvalid
}
2020-02-10 01:03:12 -06:00
if s . Run != "" {
2021-04-01 13:36:41 -05:00
if s . Uses != "" {
return StepTypeInvalid
}
2020-02-10 01:03:12 -06:00
return StepTypeRun
} else if strings . HasPrefix ( s . Uses , "docker://" ) {
return StepTypeUsesDockerURL
2022-11-01 10:58:07 -05:00
} else if strings . HasPrefix ( s . Uses , "./.github/workflows" ) && ( strings . HasSuffix ( s . Uses , ".yml" ) || strings . HasSuffix ( s . Uses , ".yaml" ) ) {
return StepTypeReusableWorkflowLocal
} else if ! strings . HasPrefix ( s . Uses , "./" ) && strings . Contains ( s . Uses , ".github/workflows" ) && ( strings . Contains ( s . Uses , ".yml@" ) || strings . Contains ( s . Uses , ".yaml@" ) ) {
return StepTypeReusableWorkflowRemote
2020-02-10 01:03:12 -06:00
} else if strings . HasPrefix ( s . Uses , "./" ) {
return StepTypeUsesActionLocal
}
return StepTypeUsesActionRemote
}
2020-02-04 18:38:41 -06:00
// ReadWorkflow returns a list of jobs for a given workflow file reader
func ReadWorkflow ( in io . Reader ) ( * Workflow , error ) {
w := new ( Workflow )
err := yaml . NewDecoder ( in ) . Decode ( w )
return w , err
}
// GetJob will get a job by name in the workflow
func ( w * Workflow ) GetJob ( jobID string ) * Job {
for id , j := range w . Jobs {
if jobID == id {
2020-02-17 12:11:16 -06:00
if j . Name == "" {
j . Name = id
}
2021-12-08 14:57:42 -06:00
if j . If . Value == "" {
j . If . Value = "success()"
}
2020-02-04 18:38:41 -06:00
return j
}
}
return nil
}
// GetJobIDs will get all the job names in the workflow
func ( w * Workflow ) GetJobIDs ( ) [ ] string {
ids := make ( [ ] string , 0 )
for id := range w . Jobs {
ids = append ( ids , id )
}
return ids
}