Moving github worflow parsing to its own file

This commit is contained in:
Chris McGehee 2021-11-08 10:26:59 -08:00 committed by Naveen
parent b3ac52a06b
commit f319aca82d
6 changed files with 370 additions and 322 deletions

View File

@ -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
}

View File

@ -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)
}
})
}
}

View File

@ -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.

View File

@ -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)

View File

@ -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)
}
})
}
}

2
go.mod
View File

@ -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