From f319aca82d5fd2238c37073e73de8b5172f660fb Mon Sep 17 00:00:00 2001 From: Chris McGehee Date: Mon, 8 Nov 2021 10:26:59 -0800 Subject: [PATCH] Moving github worflow parsing to its own file --- checks/fileparser/github_workflow.go | 220 ++++++++++++++++++++++ checks/fileparser/github_workflow_test.go | 138 ++++++++++++++ checks/permissions.go | 3 +- checks/pinned_dependencies.go | 211 +-------------------- checks/pinned_dependencies_test.go | 118 ------------ go.mod | 2 + 6 files changed, 370 insertions(+), 322 deletions(-) create mode 100644 checks/fileparser/github_workflow.go create mode 100644 checks/fileparser/github_workflow_test.go diff --git a/checks/fileparser/github_workflow.go b/checks/fileparser/github_workflow.go new file mode 100644 index 00000000..75d40d6a --- /dev/null +++ b/checks/fileparser/github_workflow.go @@ -0,0 +1,220 @@ +// Copyright 2021 Security Scorecard Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fileparser + +import ( + "fmt" + "path" + "regexp" + "strings" + + "gopkg.in/yaml.v3" + + sce "github.com/ossf/scorecard/v3/errors" +) + +// defaultShellNonWindows is the default shell used for GitHub workflow actions for Linux and Mac. +const defaultShellNonWindows = "bash" + +// defaultShellWindows is the default shell used for GitHub workflow actions for Windows. +const defaultShellWindows = "pwsh" + +// Structure for workflow config. +// We only declare the fields we need. +// Github workflows format: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions +type GitHubActionWorkflowConfig struct { + Jobs map[string]GitHubActionWorkflowJob + Name string `yaml:"name"` +} + +// A Github Action Workflow Job. +// We only declare the fields we need. +// Github workflows format: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions +// nolint: govet +type GitHubActionWorkflowJob struct { + Name string `yaml:"name"` + Steps []GitHubActionWorkflowStep `yaml:"steps"` + Defaults struct { + Run struct { + Shell string `yaml:"shell"` + } `yaml:"run"` + } `yaml:"defaults"` + RunsOn stringOrSlice `yaml:"runs-on"` + Strategy struct { + // In most cases, the 'matrix' field will have a key of 'os' which is an array of strings, but there are + // some repos that have something like: 'matrix: ${{ fromJson(needs.matrix.outputs.latest) }}'. + Matrix interface{} `yaml:"matrix"` + } `yaml:"strategy"` +} + +// A Github Action Workflow Step. +// We only declare the fields we need. +// Github workflows format: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions +type GitHubActionWorkflowStep struct { + Name string `yaml:"name"` + ID string `yaml:"id"` + Shell string `yaml:"shell"` + Run string `yaml:"run"` + If string `yaml:"if"` + Uses stringWithLine `yaml:"uses"` +} + +// stringOrSlice is for fields that can be a single string or a slice of strings. If the field is a single string, +// this value will be a slice with a single string item. +type stringOrSlice []string + +func (s *stringOrSlice) UnmarshalYAML(value *yaml.Node) error { + var stringSlice []string + err := value.Decode(&stringSlice) + if err == nil { + *s = stringSlice + return nil + } + var single string + err = value.Decode(&single) + if err != nil { + return sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("error decoding stringOrSlice Value: %v", err)) + } + *s = []string{single} + return nil +} + +// stringWithLine is for when you want to keep track of the line number that the string came from. +type stringWithLine struct { + Value string + Line int +} + +func (ws *stringWithLine) UnmarshalYAML(value *yaml.Node) error { + err := value.Decode(&ws.Value) + if err != nil { + return sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("error decoding stringWithLine Value: %v", err)) + } + ws.Line = value.Line + + return nil +} + +// GetOSesForJob returns the OSes this job runs on. +func GetOSesForJob(job *GitHubActionWorkflowJob) ([]string, error) { + // The 'runs-on' field either lists the OS'es directly, or it can have an expression '${{ matrix.os }}' which + // is where the OS'es are actually listed. + getFromMatrix := len(job.RunsOn) == 1 && strings.Contains(job.RunsOn[0], "matrix.os") + if !getFromMatrix { + return job.RunsOn, nil + } + jobOSes := make([]string, 0) + // nolint: nestif + if m, ok := job.Strategy.Matrix.(map[string]interface{}); ok { + if osVal, ok := m["os"]; ok { + if oses, ok := osVal.([]interface{}); ok { + for _, os := range oses { + if strVal, ok := os.(string); ok { + jobOSes = append(jobOSes, strVal) + } + } + return jobOSes, nil + } + } + } + return jobOSes, sce.WithMessage(sce.ErrScorecardInternal, + fmt.Sprintf("unable to determine OS for job: %v", job.Name)) +} + +// JobAlwaysRunsOnWindows returns true if the only OS that this job runs on is Windows. +func JobAlwaysRunsOnWindows(job *GitHubActionWorkflowJob) (bool, error) { + jobOSes, err := GetOSesForJob(job) + if err != nil { + return false, err + } + for _, os := range jobOSes { + if !strings.HasPrefix(strings.ToLower(os), "windows") { + return false, nil + } + } + return true, nil +} + +// GetShellForStep returns the shell that is used to run the given step. +func GetShellForStep(step *GitHubActionWorkflowStep, job *GitHubActionWorkflowJob) (string, error) { + // https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#using-a-specific-shell. + if step.Shell != "" { + return step.Shell, nil + } + if job.Defaults.Run.Shell != "" { + return job.Defaults.Run.Shell, nil + } + + isStepWindows, err := IsStepWindows(step) + if err != nil { + return "", err + } + if isStepWindows { + return defaultShellWindows, nil + } + + alwaysRunsOnWindows, err := JobAlwaysRunsOnWindows(job) + if err != nil { + return "", err + } + if alwaysRunsOnWindows { + return defaultShellWindows, nil + } + + return defaultShellNonWindows, nil +} + +// IsStepWindows returns true if the step will be run on Windows. +func IsStepWindows(step *GitHubActionWorkflowStep) (bool, error) { + windowsRegexes := []string{ + // Looking for "if: runner.os == 'Windows'" (and variants) + `(?i)runner\.os\s*==\s*['"]windows['"]`, + // Looking for "if: ${{ startsWith(runner.os, 'Windows') }}" (and variants) + `(?i)\$\{\{\s*startsWith\(runner\.os,\s*['"]windows['"]\)`, + // Looking for "if: matrix.os == 'windows-2019'" (and variants) + `(?i)matrix\.os\s*==\s*['"]windows-`, + } + + for _, windowsRegex := range windowsRegexes { + matches, err := regexp.MatchString(windowsRegex, step.If) + if err != nil { + return false, sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("error matching Windows regex: %v", err)) + } + if matches { + return true, nil + } + } + + return false, nil +} + +// IsWorkflowFile returns true if this is a GitHub workflow file. +func IsWorkflowFile(pathfn string) bool { + // From https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions: + // "Workflow files use YAML syntax, and must have either a .yml or .yaml file extension." + switch path.Ext(pathfn) { + case ".yml", ".yaml": + return true + default: + return false + } +} + +// IsGitHubOwnedAction checks if this is a github specific action. +func IsGitHubOwnedAction(actionName string) bool { + a := strings.HasPrefix(actionName, "actions/") + c := strings.HasPrefix(actionName, "github/") + return a || c +} diff --git a/checks/fileparser/github_workflow_test.go b/checks/fileparser/github_workflow_test.go new file mode 100644 index 00000000..eca0706a --- /dev/null +++ b/checks/fileparser/github_workflow_test.go @@ -0,0 +1,138 @@ +// Copyright 2021 Security Scorecard Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fileparser + +import ( + "io/ioutil" + "testing" + + "gopkg.in/yaml.v3" + "gotest.tools/assert/cmp" +) + +func TestGitHubWorkflowShell(t *testing.T) { + t.Parallel() + + repeatItem := func(item string, count int) []string { + ret := make([]string, 0, count) + for i := 0; i < count; i++ { + ret = append(ret, item) + } + return ret + } + + tests := []struct { + name string + filename string + // The shells used in each step, listed in order that the steps are listed in the file + expectedShells []string + }{ + { + name: "all windows, shell specified in step", + filename: "../testdata/github-workflow-shells-all-windows-bash.yaml", + expectedShells: []string{"bash"}, + }, + { + name: "all windows, OSes listed in matrix.os", + filename: "../testdata/github-workflow-shells-all-windows-matrix.yaml", + expectedShells: []string{"pwsh"}, + }, + { + name: "all windows", + filename: "../testdata/github-workflow-shells-all-windows.yaml", + expectedShells: []string{"pwsh"}, + }, + { + name: "macOS defaults to bash", + filename: "../testdata/github-workflow-shells-default-macos.yaml", + expectedShells: []string{"bash"}, + }, + { + name: "ubuntu defaults to bash", + filename: "../testdata/github-workflow-shells-default-ubuntu.yaml", + expectedShells: []string{"bash"}, + }, + { + name: "windows defaults to pwsh", + filename: "../testdata/github-workflow-shells-default-windows.yaml", + expectedShells: []string{"pwsh"}, + }, + { + name: "windows specified in 'if'", + filename: "../testdata/github-workflow-shells-runner-windows-ubuntu.yaml", + expectedShells: append(repeatItem("pwsh", 7), repeatItem("bash", 4)...), + }, + { + name: "shell specified in job and step", + filename: "../testdata/github-workflow-shells-specified-job-step.yaml", + expectedShells: []string{"bash"}, + }, + { + name: "windows, shell specified in job", + filename: "../testdata/github-workflow-shells-specified-job-windows.yaml", + expectedShells: []string{"bash"}, + }, + { + name: "shell specified in job", + filename: "../testdata/github-workflow-shells-specified-job.yaml", + expectedShells: []string{"pwsh"}, + }, + { + name: "shell specified in step", + filename: "../testdata/github-workflow-shells-speficied-step.yaml", + expectedShells: []string{"pwsh"}, + }, + { + name: "different shells in each step", + filename: "../testdata/github-workflow-shells-two-shells.yaml", + expectedShells: []string{"bash", "pwsh"}, + }, + { + name: "windows step, bash specified", + filename: "../testdata/github-workflow-shells-windows-bash.yaml", + expectedShells: []string{"bash", "bash"}, + }, + } + for _, tt := range tests { + tt := tt // Re-initializing variable so it is not changed while executing the closure below + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + content, err := ioutil.ReadFile(tt.filename) + if err != nil { + t.Errorf("cannot read file: %v", err) + } + var workflow GitHubActionWorkflowConfig + err = yaml.Unmarshal(content, &workflow) + if err != nil { + t.Errorf("cannot unmarshal file: %v", err) + } + actualShells := make([]string, 0) + for _, job := range workflow.Jobs { + job := job + for _, step := range job.Steps { + step := step + shell, err := GetShellForStep(&step, &job) + if err != nil { + t.Errorf("error getting shell: %v", err) + } + actualShells = append(actualShells, shell) + } + } + if !cmp.DeepEqual(tt.expectedShells, actualShells)().Success() { + t.Errorf("%v: Got (%v) expected (%v)", tt.name, actualShells, tt.expectedShells) + } + }) + } +} diff --git a/checks/permissions.go b/checks/permissions.go index 8fcd8974..979c4c21 100644 --- a/checks/permissions.go +++ b/checks/permissions.go @@ -21,6 +21,7 @@ import ( "gopkg.in/yaml.v2" "github.com/ossf/scorecard/v3/checker" + "github.com/ossf/scorecard/v3/checks/fileparser" sce "github.com/ossf/scorecard/v3/errors" ) @@ -360,7 +361,7 @@ func testValidateGitHubActionTokenPermissions(pathfn string, // Check file content. func validateGitHubActionTokenPermissions(path string, content []byte, dl checker.DetailLogger, data FileCbData) (bool, error) { - if !isWorkflowFile(path) { + if !fileparser.IsWorkflowFile(path) { return true, nil } // Verify the type of the data. diff --git a/checks/pinned_dependencies.go b/checks/pinned_dependencies.go index b68768f8..1c0f265d 100644 --- a/checks/pinned_dependencies.go +++ b/checks/pinned_dependencies.go @@ -16,7 +16,6 @@ package checks import ( "fmt" - "path" "regexp" "strings" @@ -24,84 +23,13 @@ import ( "gopkg.in/yaml.v3" "github.com/ossf/scorecard/v3/checker" + "github.com/ossf/scorecard/v3/checks/fileparser" sce "github.com/ossf/scorecard/v3/errors" ) // CheckPinnedDependencies is the registered name for FrozenDeps. const CheckPinnedDependencies = "Pinned-Dependencies" -// defaultShellNonWindows is the default shell used for GitHub workflow actions for Linux and Mac. -const defaultShellNonWindows = "bash" - -// defaultShellWindows is the default shell used for GitHub workflow actions for Windows. -const defaultShellWindows = "pwsh" - -// Structure for workflow config. -// We only declare the fields we need. -// Github workflows format: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions -type gitHubActionWorkflowConfig struct { - Jobs map[string]gitHubActionWorkflowJob - Name string `yaml:"name"` -} - -// A Github Action Workflow Job. -// We only declare the fields we need. -// Github workflows format: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions -// nolint: govet -type gitHubActionWorkflowJob struct { - Name string `yaml:"name"` - Steps []gitHubActionWorkflowStep `yaml:"steps"` - Defaults struct { - Run struct { - Shell string `yaml:"shell"` - } `yaml:"run"` - } `yaml:"defaults"` - RunsOn stringOrSlice `yaml:"runs-on"` - Strategy struct { - // In most cases, the 'matrix' field will have a key of 'os' which is an array of strings, but there are - // some repos that have something like: 'matrix: ${{ fromJson(needs.matrix.outputs.latest) }}'. - Matrix interface{} `yaml:"matrix"` - } `yaml:"strategy"` -} - -// A Github Action Workflow Step. -// We only declare the fields we need. -// Github workflows format: https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions -type gitHubActionWorkflowStep struct { - Name string `yaml:"name"` - ID string `yaml:"id"` - Shell string `yaml:"shell"` - Run string `yaml:"run"` - If string `yaml:"if"` - Uses stringWithLine `yaml:"uses"` -} - -// stringOrSlice is for fields that can be a single string or a slice of strings. If the field is a single string, -// this value will be a slice with a single string item. -type stringOrSlice []string - -func (s *stringOrSlice) UnmarshalYAML(value *yaml.Node) error { - var stringSlice []string - err := value.Decode(&stringSlice) - if err == nil { - *s = stringSlice - return nil - } - var single string - err = value.Decode(&single) - if err != nil { - return sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("error decoding stringOrSlice Value: %v", err)) - } - *s = []string{single} - return nil -} - -// stringWithLine is for when you want to keep track of the line number that the string came from. -type stringWithLine struct { - Value string - Line int -} - // Structure to host information about pinned github // or third party dependencies. type worklowPinningResult struct { @@ -109,16 +37,6 @@ type worklowPinningResult struct { gitHubOwned pinnedResult } -func (ws *stringWithLine) UnmarshalYAML(value *yaml.Node) error { - err := value.Decode(&ws.Value) - if err != nil { - return sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("error decoding stringWithLine Value: %v", err)) - } - ws.Line = value.Line - - return nil -} - //nolint:gochecknoinits func init() { registerCheck(CheckPinnedDependencies, PinnedDependencies) @@ -522,7 +440,7 @@ func testValidateGitHubWorkflowScriptFreeOfInsecureDownloads(pathfn string, // Returns true if the check should continue executing after this file. func validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn string, content []byte, dl checker.DetailLogger, data FileCbData) (bool, error) { - if !isWorkflowFile(pathfn) { + if !fileparser.IsWorkflowFile(pathfn) { return true, nil } @@ -533,7 +451,7 @@ func validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn string, content []by return true, nil } - var workflow gitHubActionWorkflowConfig + var workflow fileparser.GitHubActionWorkflowConfig err := yaml.Unmarshal(content, &workflow) if err != nil { return false, sce.WithMessage(sce.ErrScorecardInternal, @@ -552,7 +470,7 @@ func validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn string, content []by } // https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepsrun. - shell, err := getShellForStep(&step, &job) + shell, err := fileparser.GetShellForStep(&step, &job) if err != nil { return false, err } @@ -579,99 +497,6 @@ func validateGitHubWorkflowIsFreeOfInsecureDownloads(pathfn string, content []by return true, nil } -// Returns the OSes this job runs on. -func getOSesForJob(job *gitHubActionWorkflowJob) ([]string, error) { - // The 'runs-on' field either lists the OS'es directly, or it can have an expression '${{ matrix.os }}' which - // is where the OS'es are actually listed. - getFromMatrix := len(job.RunsOn) == 1 && strings.Contains(job.RunsOn[0], "matrix.os") - if !getFromMatrix { - return job.RunsOn, nil - } - jobOSes := make([]string, 0) - // nolint: nestif - if m, ok := job.Strategy.Matrix.(map[string]interface{}); ok { - if osVal, ok := m["os"]; ok { - if oses, ok := osVal.([]interface{}); ok { - for _, os := range oses { - if strVal, ok := os.(string); ok { - jobOSes = append(jobOSes, strVal) - } - } - return jobOSes, nil - } - } - } - return jobOSes, sce.WithMessage(sce.ErrScorecardInternal, - fmt.Sprintf("unable to determine OS for job: %v", job.Name)) -} - -// The only OS that this job runs on is Windows. -func jobAlwaysRunsOnWindows(job *gitHubActionWorkflowJob) (bool, error) { - jobOSes, err := getOSesForJob(job) - if err != nil { - return false, err - } - for _, os := range jobOSes { - if !strings.HasPrefix(strings.ToLower(os), "windows") { - return false, nil - } - } - return true, nil -} - -// getShellForStep returns the shell that is used to run the given step. -func getShellForStep(step *gitHubActionWorkflowStep, job *gitHubActionWorkflowJob) (string, error) { - // https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#using-a-specific-shell. - if step.Shell != "" { - return step.Shell, nil - } - if job.Defaults.Run.Shell != "" { - return job.Defaults.Run.Shell, nil - } - - isStepWindows, err := isStepWindows(step) - if err != nil { - return "", err - } - if isStepWindows { - return defaultShellWindows, nil - } - - alwaysRunsOnWindows, err := jobAlwaysRunsOnWindows(job) - if err != nil { - return "", err - } - if alwaysRunsOnWindows { - return defaultShellWindows, nil - } - - return defaultShellNonWindows, nil -} - -// isStepWindows returns true if the step will be run on Windows. -func isStepWindows(step *gitHubActionWorkflowStep) (bool, error) { - windowsRegexes := []string{ - // Looking for "if: runner.os == 'Windows'" (and variants) - `(?i)runner\.os\s*==\s*['"]windows['"]`, - // Looking for "if: ${{ startsWith(runner.os, 'Windows') }}" (and variants) - `(?i)\$\{\{\s*startsWith\(runner\.os,\s*['"]windows['"]\)`, - // Looking for "if: matrix.os == 'windows-2019'" (and variants) - `(?i)matrix\.os\s*==\s*['"]windows-`, - } - - for _, windowsRegex := range windowsRegexes { - matches, err := regexp.MatchString(windowsRegex, step.If) - if err != nil { - return false, sce.WithMessage(sce.ErrScorecardInternal, fmt.Sprintf("error matching Windows regex: %v", err)) - } - if matches { - return true, nil - } - } - - return false, nil -} - // Check pinning of github actions in workflows. func isGitHubActionsWorkflowPinned(c *checker.CheckRequest) (int, error) { var r worklowPinningResult @@ -697,7 +522,7 @@ func testIsGitHubActionsWorkflowPinned(pathfn string, content []byte, dl checker // should continue executing after this file. func validateGitHubActionWorkflow(pathfn string, content []byte, dl checker.DetailLogger, data FileCbData) (bool, error) { - if !isWorkflowFile(pathfn) { + if !fileparser.IsWorkflowFile(pathfn) { return true, nil } @@ -709,7 +534,7 @@ func validateGitHubActionWorkflow(pathfn string, content []byte, return true, nil } - var workflow gitHubActionWorkflowConfig + var workflow fileparser.GitHubActionWorkflowConfig err := yaml.Unmarshal(content, &workflow) if err != nil { return false, sce.WithMessage(sce.ErrScorecardInternal, @@ -733,34 +558,14 @@ func validateGitHubActionWorkflow(pathfn string, content []byte, }) } - githubOwned := isGitHubOwnedAction(step.Uses.Value) - addWorkflowPinnedResult(pdata, match, githubOwned) - } + githubOwned := fileparser.IsGitHubOwnedAction(execAction.Uses.Value) + addWorkflowPinnedResult(pdata, match, githubOwned) } } return true, nil } -// isWorkflowFile returns true if this is a GitHub workflow file. -func isWorkflowFile(pathfn string) bool { - // From https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions: - // "Workflow files use YAML syntax, and must have either a .yml or .yaml file extension." - switch path.Ext(pathfn) { - case ".yml", ".yaml": - return true - default: - return false - } -} - -// isGitHubOwnedAction check github specific action. -func isGitHubOwnedAction(v string) bool { - a := strings.HasPrefix(v, "actions/") - c := strings.HasPrefix(v, "github/") - return a || c -} - func addWorkflowPinnedResult(w *worklowPinningResult, to, isGitHub bool) { if isGitHub { addPinnedResult(&w.gitHubOwned, to) diff --git a/checks/pinned_dependencies_test.go b/checks/pinned_dependencies_test.go index 2e439f6a..1d463628 100644 --- a/checks/pinned_dependencies_test.go +++ b/checks/pinned_dependencies_test.go @@ -19,9 +19,6 @@ import ( "strings" "testing" - "github.com/google/go-cmp/cmp" - "gopkg.in/yaml.v3" - "github.com/ossf/scorecard/v3/checker" scut "github.com/ossf/scorecard/v3/utests" ) @@ -968,118 +965,3 @@ func TestGitHubWorkflowUsesLineNumber(t *testing.T) { }) } } - -func TestGitHubWorkflowShell(t *testing.T) { - t.Parallel() - - repeatItem := func(item string, count int) []string { - ret := make([]string, 0, count) - for i := 0; i < count; i++ { - ret = append(ret, item) - } - return ret - } - - tests := []struct { - name string - filename string - // The shells used in each step, listed in order that the steps are listed in the file - expectedShells []string - }{ - { - name: "all windows, shell specified in step", - filename: "testdata/github-workflow-shells-all-windows-bash.yaml", - expectedShells: []string{"bash"}, - }, - { - name: "all windows, OSes listed in matrix.os", - filename: "testdata/github-workflow-shells-all-windows-matrix.yaml", - expectedShells: []string{"pwsh"}, - }, - { - name: "all windows", - filename: "testdata/github-workflow-shells-all-windows.yaml", - expectedShells: []string{"pwsh"}, - }, - { - name: "macOS defaults to bash", - filename: "testdata/github-workflow-shells-default-macos.yaml", - expectedShells: []string{"bash"}, - }, - { - name: "ubuntu defaults to bash", - filename: "testdata/github-workflow-shells-default-ubuntu.yaml", - expectedShells: []string{"bash"}, - }, - { - name: "windows defaults to pwsh", - filename: "testdata/github-workflow-shells-default-windows.yaml", - expectedShells: []string{"pwsh"}, - }, - { - name: "windows specified in 'if'", - filename: "testdata/github-workflow-shells-runner-windows-ubuntu.yaml", - expectedShells: append(repeatItem("pwsh", 7), repeatItem("bash", 4)...), - }, - { - name: "shell specified in job and step", - filename: "testdata/github-workflow-shells-specified-job-step.yaml", - expectedShells: []string{"bash"}, - }, - { - name: "windows, shell specified in job", - filename: "testdata/github-workflow-shells-specified-job-windows.yaml", - expectedShells: []string{"bash"}, - }, - { - name: "shell specified in job", - filename: "testdata/github-workflow-shells-specified-job.yaml", - expectedShells: []string{"pwsh"}, - }, - { - name: "shell specified in step", - filename: "testdata/github-workflow-shells-speficied-step.yaml", - expectedShells: []string{"pwsh"}, - }, - { - name: "different shells in each step", - filename: "testdata/github-workflow-shells-two-shells.yaml", - expectedShells: []string{"bash", "pwsh"}, - }, - { - name: "windows step, bash specified", - filename: "testdata/github-workflow-shells-windows-bash.yaml", - expectedShells: []string{"bash", "bash"}, - }, - } - for _, tt := range tests { - tt := tt // Re-initializing variable so it is not changed while executing the closure below - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - content, err := ioutil.ReadFile(tt.filename) - if err != nil { - t.Errorf("cannot read file: %v", err) - } - var workflow gitHubActionWorkflowConfig - err = yaml.Unmarshal(content, &workflow) - if err != nil { - t.Errorf("cannot unmarshal file: %v", err) - } - actualShells := make([]string, 0) - for _, job := range workflow.Jobs { - job := job - for _, step := range job.Steps { - step := step - shell, err := getShellForStep(&step, &job) - if err != nil { - t.Errorf("error getting shell: %v", err) - } - actualShells = append(actualShells, shell) - } - } - if !cmp.Equal(tt.expectedShells, actualShells) { - t.Errorf("%v: Got (%v) expected (%v)", tt.name, actualShells, tt.expectedShells) - } - }) - } -} diff --git a/go.mod b/go.mod index c7c8e8c5..7140fecd 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,8 @@ require ( mvdan.cc/sh/v3 v3.4.0 ) +require gotest.tools v2.2.0+incompatible + require ( cloud.google.com/go v0.94.1 // indirect cloud.google.com/go/storage v1.16.1 // indirect