Skip to content

Commit f09672e

Browse files
committed
Merge branch 'issue-409' (fix #409)
2 parents 01d62a0 + f57173a commit f09672e

12 files changed

+403
-60
lines changed

rule_pyflakes.go

+17-13
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package actionlint
33
import (
44
"bytes"
55
"fmt"
6+
"strings"
67
"sync"
78
)
89

@@ -18,7 +19,7 @@ func getShellIsPythonKind(shell *String) shellIsPythonKind {
1819
if shell == nil {
1920
return shellIsPythonKindUnspecified
2021
}
21-
if shell.Value == "python" {
22+
if shell.Value == "python" || strings.HasPrefix(shell.Value, "python ") {
2223
return shellIsPythonKindPython
2324
}
2425
return shellIsPythonKindNotPython
@@ -34,15 +35,8 @@ type RulePyflakes struct {
3435
mu sync.Mutex
3536
}
3637

37-
// NewRulePyflakes creates new RulePyflakes instance. Parameter executable can be command name
38-
// or relative/absolute file path. When the given executable is not found in system, it returns
39-
// an error.
40-
func NewRulePyflakes(executable string, proc *concurrentProcess) (*RulePyflakes, error) {
41-
cmd, err := proc.newCommandRunner(executable)
42-
if err != nil {
43-
return nil, err
44-
}
45-
r := &RulePyflakes{
38+
func newRulePyflakes(cmd *externalCommand) *RulePyflakes {
39+
return &RulePyflakes{
4640
RuleBase: RuleBase{
4741
name: "pyflakes",
4842
desc: "Checks for Python script when \"shell: python\" is configured using Pyflakes",
@@ -51,7 +45,17 @@ func NewRulePyflakes(executable string, proc *concurrentProcess) (*RulePyflakes,
5145
workflowShellIsPython: shellIsPythonKindUnspecified,
5246
jobShellIsPython: shellIsPythonKindUnspecified,
5347
}
54-
return r, nil
48+
}
49+
50+
// NewRulePyflakes creates new RulePyflakes instance. Parameter executable can be command name
51+
// or relative/absolute file path. When the given executable is not found in system, it returns
52+
// an error.
53+
func NewRulePyflakes(executable string, proc *concurrentProcess) (*RulePyflakes, error) {
54+
cmd, err := proc.newCommandRunner(executable)
55+
if err != nil {
56+
return nil, err
57+
}
58+
return newRulePyflakes(cmd), nil
5559
}
5660

5761
// VisitJobPre is callback when visiting Job node before visiting its children.
@@ -98,8 +102,8 @@ func (rule *RulePyflakes) VisitStep(n *Step) error {
98102
}
99103

100104
func (rule *RulePyflakes) isPythonShell(r *ExecRun) bool {
101-
if r.Shell != nil {
102-
return r.Shell.Value == "python"
105+
if k := getShellIsPythonKind(r.Shell); k != shellIsPythonKindUnspecified {
106+
return k == shellIsPythonKindPython
103107
}
104108

105109
if rule.jobShellIsPython != shellIsPythonKindUnspecified {

rule_pyflakes_test.go

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package actionlint
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestRulePyflakesDetectPythonShell(t *testing.T) {
8+
tests := []struct {
9+
what string
10+
isPython bool
11+
workflow string // Shell name set at 'defaults' in Workflow node
12+
job string // Shell name set at 'defaults' in Job node
13+
step string // Shell name set at 'shell' in Step node
14+
}{
15+
{
16+
what: "no default shell",
17+
isPython: false,
18+
},
19+
{
20+
what: "workflow default",
21+
isPython: true,
22+
workflow: "python",
23+
},
24+
{
25+
what: "job default",
26+
isPython: true,
27+
job: "python",
28+
},
29+
{
30+
what: "step shell",
31+
isPython: true,
32+
step: "python",
33+
},
34+
{
35+
what: "custom shell",
36+
isPython: true,
37+
workflow: "python {0}",
38+
},
39+
{
40+
what: "other shell",
41+
isPython: false,
42+
workflow: "pwsh",
43+
},
44+
{
45+
what: "other custom shell",
46+
isPython: false,
47+
workflow: "bash -e {0}",
48+
},
49+
}
50+
51+
for _, tc := range tests {
52+
t.Run(tc.what, func(t *testing.T) {
53+
r := newRulePyflakes(&externalCommand{})
54+
55+
w := &Workflow{}
56+
if tc.workflow != "" {
57+
w.Defaults = &Defaults{
58+
Run: &DefaultsRun{
59+
Shell: &String{Value: tc.workflow},
60+
},
61+
}
62+
}
63+
r.VisitWorkflowPre(w)
64+
65+
j := &Job{}
66+
if tc.job != "" {
67+
j.Defaults = &Defaults{
68+
Run: &DefaultsRun{
69+
Shell: &String{Value: tc.job},
70+
},
71+
}
72+
}
73+
r.VisitJobPre(j)
74+
75+
e := &ExecRun{}
76+
if tc.step != "" {
77+
e.Shell = &String{Value: tc.step}
78+
}
79+
if have := r.isPythonShell(e); have != tc.isPython {
80+
t.Fatalf("Actual isPython=%v but wanted isPython=%v", have, tc.isPython)
81+
}
82+
})
83+
}
84+
}

rule_shellcheck.go

+47-32
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,32 @@ type RuleShellcheck struct {
2222
cmd *externalCommand
2323
workflowShell string
2424
jobShell string
25+
runnerShell string
2526
mu sync.Mutex
2627
}
2728

28-
// NewRuleShellcheck creates new RuleShellcheck instance. Parameter executable can be command name
29-
// or relative/absolute file path. When the given executable is not found in system, it returns an
30-
// error as 2nd return value.
31-
func NewRuleShellcheck(executable string, proc *concurrentProcess) (*RuleShellcheck, error) {
32-
cmd, err := proc.newCommandRunner(executable)
33-
if err != nil {
34-
return nil, err
35-
}
36-
r := &RuleShellcheck{
29+
func newRuleShellcheck(cmd *externalCommand) *RuleShellcheck {
30+
return &RuleShellcheck{
3731
RuleBase: RuleBase{
3832
name: "shellcheck",
3933
desc: "Checks for shell script sources in \"run:\" using shellcheck",
4034
},
4135
cmd: cmd,
4236
workflowShell: "",
4337
jobShell: "",
38+
runnerShell: "",
4439
}
45-
return r, nil
40+
}
41+
42+
// NewRuleShellcheck creates new RuleShellcheck instance. The executable argument can be command
43+
// name or relative/absolute file path. When the given executable is not found in system, it returns
44+
// an error as 2nd return value.
45+
func NewRuleShellcheck(executable string, proc *concurrentProcess) (*RuleShellcheck, error) {
46+
cmd, err := proc.newCommandRunner(executable)
47+
if err != nil {
48+
return nil, err
49+
}
50+
return newRuleShellcheck(cmd), nil
4651
}
4752

4853
// VisitStep is callback when visiting Step node.
@@ -52,44 +57,35 @@ func (rule *RuleShellcheck) VisitStep(n *Step) error {
5257
return nil
5358
}
5459

55-
name := rule.getShellName(run)
56-
if name != "bash" && name != "sh" {
57-
return nil
58-
}
59-
60-
rule.runShellcheck(run.Run.Value, name, run.RunPos)
60+
rule.runShellcheck(run.Run.Value, rule.getShellName(run), run.RunPos)
6161
return nil
6262
}
6363

6464
// VisitJobPre is callback when visiting Job node before visiting its children.
6565
func (rule *RuleShellcheck) VisitJobPre(n *Job) error {
6666
if n.Defaults != nil && n.Defaults.Run != nil && n.Defaults.Run.Shell != nil {
6767
rule.jobShell = n.Defaults.Run.Shell.Value
68-
return nil
6968
}
7069

71-
if n.RunsOn == nil {
72-
return nil
73-
}
74-
75-
for _, label := range n.RunsOn.Labels {
76-
l := strings.ToLower(label.Value)
77-
// Default shell on Windows is PowerShell.
78-
// https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#using-a-specific-shell
79-
if l == "windows" || strings.HasPrefix(l, "windows-") {
80-
return nil
70+
if n.RunsOn != nil {
71+
for _, label := range n.RunsOn.Labels {
72+
l := strings.ToLower(label.Value)
73+
// Default shell on Windows is PowerShell.
74+
// https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#using-a-specific-shell
75+
if l == "windows" || strings.HasPrefix(l, "windows-") {
76+
rule.runnerShell = "pwsh"
77+
break
78+
}
8179
}
8280
}
8381

84-
// TODO: When bash is not found, GitHub-hosted runner fallbacks to sh. What OSes require this behavior?
85-
rule.jobShell = "bash"
86-
8782
return nil
8883
}
8984

9085
// VisitJobPost is callback when visiting Job node after visiting its children.
9186
func (rule *RuleShellcheck) VisitJobPost(n *Job) error {
9287
rule.jobShell = ""
88+
rule.runnerShell = ""
9389
return nil
9490
}
9591

@@ -114,7 +110,15 @@ func (rule *RuleShellcheck) getShellName(exec *ExecRun) string {
114110
if rule.jobShell != "" {
115111
return rule.jobShell
116112
}
117-
return rule.workflowShell
113+
if rule.workflowShell != "" {
114+
return rule.workflowShell
115+
}
116+
if rule.runnerShell != "" {
117+
return rule.runnerShell
118+
}
119+
// Note: Default shell on Windows is pwsh so this value is not always correct.
120+
// Note: When bash is not found, GitHub-hosted runner fallbacks to sh.
121+
return "bash"
118122
}
119123

120124
// Replace ${{ ... }} with underscores like __________
@@ -156,7 +160,18 @@ func sanitizeExpressionsInScript(src string) string {
156160
}
157161
}
158162

159-
func (rule *RuleShellcheck) runShellcheck(src, sh string, pos *Pos) {
163+
func (rule *RuleShellcheck) runShellcheck(src, shell string, pos *Pos) {
164+
var sh string
165+
if shell == "bash" || shell == "sh" {
166+
sh = shell
167+
} else if strings.HasPrefix(shell, "bash ") {
168+
sh = "bash"
169+
} else if strings.HasPrefix(shell, "sh ") {
170+
sh = "sh"
171+
} else {
172+
return // Skip checking this shell script since shellcheck doesn't support it
173+
}
174+
160175
src = sanitizeExpressionsInScript(src)
161176
rule.Debug("%s: Run shellcheck for %s script:\n%s", pos, sh, src)
162177

0 commit comments

Comments
 (0)