diff --git a/example_your_won_rule_test.go b/example_your_won_rule_test.go new file mode 100644 index 000000000..59a6aacdb --- /dev/null +++ b/example_your_won_rule_test.go @@ -0,0 +1,65 @@ +package actionlint_test + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/rhysd/actionlint" +) + +// A rule type to check every steps have their names. +type RuleStepName struct { + // Embedding RuleBase struct implements the minimal Rule interface. + actionlint.RuleBase +} + +// Reimplement methods in RuleBase. Visit* methods are called on checking workflows. +func (r *RuleStepName) VisitStep(n *actionlint.Step) error { + // Implement your own check + if n.Name == nil { + // RuleBase provides methods to report errors. See RuleBase.Error and RuleBase.Errorf. + r.Error(n.Pos, "every step must have its name") + } + return nil +} + +func NewRuleStepName() *RuleStepName { + return &RuleStepName{ + RuleBase: actionlint.NewRuleBase("step-name", "Checks every step has their own name"), + } +} + +func ExampleLinter_yourOwnRule() { + // The function set at OnRulesCreated is called after rule instances are created. You can + // add/remove some rules and return the modified slice. This function is called on linting + // each workflow files. + o := &actionlint.LinterOptions{ + OnRulesCreated: func(rules []actionlint.Rule) []actionlint.Rule { + rules = append(rules, NewRuleStepName()) + return rules + }, + } + + l, err := actionlint.NewLinter(os.Stdout, o) + if err != nil { + panic(err) + } + + f := filepath.Join("testdata", "ok", "minimal.yaml") + + // First return value is an array of lint errors found in the workflow file. + errs, err := l.LintFile(f, nil) + if err != nil { + panic(err) + } + + fmt.Println(len(errs), "lint errors found by actionlint") + + // Output: + // testdata/ok/minimal.yaml:6:9: every step must have its name [step-name] + // | + // 6 | - run: echo + // | ^~~~ + // 1 lint errors found by actionlint +} diff --git a/linter.go b/linter.go index 91ad79ab8..df973d636 100644 --- a/linter.go +++ b/linter.go @@ -85,22 +85,27 @@ type LinterOptions struct { // WorkingDir is a file path to the current working directory. When this value is empty, os.Getwd // will be used to get a working directory. WorkingDir string + // OnRulesCreated is a hook to add or remove the check rules. This function is called on checking + // every workflow files. Rules created by Linter instance is passed to the argument and the function + // should return the modified rules slice. + OnRulesCreated func([]Rule) []Rule // More options will come here } // Linter is struct to lint workflow files. type Linter struct { - projects *Projects - out io.Writer - logOut io.Writer - logLevel LogLevel - oneline bool - shellcheck string - pyflakes string - ignorePats []*regexp.Regexp - defaultConfig *Config - errFmt *ErrorFormatter - cwd string + projects *Projects + out io.Writer + logOut io.Writer + logLevel LogLevel + oneline bool + shellcheck string + pyflakes string + ignorePats []*regexp.Regexp + defaultConfig *Config + errFmt *ErrorFormatter + cwd string + onRulesCreated func([]Rule) []Rule } // NewLinter creates a new Linter instance. @@ -178,6 +183,7 @@ func NewLinter(out io.Writer, opts *LinterOptions) (*Linter, error) { cfg, formatter, cwd, + opts.OnRulesCreated, }, nil } @@ -525,6 +531,9 @@ func (l *Linter) check( } else { l.log("Rule \"pyflakes\" was disabled since pyflakes command name was empty") } + if l.onRulesCreated != nil { + rules = l.onRulesCreated(rules) + } v := NewVisitor() for _, rule := range rules { diff --git a/linter_test.go b/linter_test.go index c0c95392b..007b4a72c 100644 --- a/linter_test.go +++ b/linter_test.go @@ -458,6 +458,33 @@ func TestLinterPathsNotFound(t *testing.T) { } } +func TestLinterRemoveRuleOnRulesCreatedHook(t *testing.T) { + o := &LinterOptions{ + OnRulesCreated: func(rules []Rule) []Rule { + for i, r := range rules { + if r.Name() == "runner-label" { + rules = append(rules[:i], rules[i+1:]...) + } + } + return rules + }, + } + + l, err := NewLinter(io.Discard, o) + if err != nil { + t.Fatal(err) + } + + f := filepath.Join("testdata", "err", "invalid_runner_labels.yaml") + errs, err := l.LintFile(f, nil) + if err != nil { + t.Fatal(err) + } + if len(errs) != 0 { + t.Fatal("no error was expected because runner-label rule was removed but got:", errs) + } +} + func BenchmarkLintWorkflowFiles(b *testing.B) { large := filepath.Join("testdata", "bench", "many_scripts.yaml") small := filepath.Join("testdata", "bench", "small.yaml") diff --git a/rule.go b/rule.go index 75a0d8464..75ffa5f25 100644 --- a/rule.go +++ b/rule.go @@ -17,8 +17,8 @@ type RuleBase struct { // NewRuleBase creates a new RuleBase instance. It should be embedded to your own // rule instance. -func NewRuleBase(name string, desc string) *RuleBase { - return &RuleBase{ +func NewRuleBase(name string, desc string) RuleBase { + return RuleBase{ name: name, desc: desc, } diff --git a/testdata/err/invalid_runner_labels.out b/testdata/err/invalid_runner_labels.out new file mode 100644 index 000000000..3280d6b8a --- /dev/null +++ b/testdata/err/invalid_runner_labels.out @@ -0,0 +1,3 @@ +/test\.yaml:4:14: label "ubuntu-oldest" is unknown\. available labels are .+\. if it is a custom label for self-hosted runner, set list of labels in actionlint.yaml config file \[runner-label\]/ +test.yaml:8:30: label "windows-latest" conflicts with label "ubuntu-latest" defined at line:8,col:15. note: to run your job on each workers, use matrix [runner-label] +test.yaml:8:46: label "macos-latest" conflicts with label "ubuntu-latest" defined at line:8,col:15. note: to run your job on each workers, use matrix [runner-label] diff --git a/testdata/err/invalid_runner_labels.yaml b/testdata/err/invalid_runner_labels.yaml new file mode 100644 index 000000000..6c307eacb --- /dev/null +++ b/testdata/err/invalid_runner_labels.yaml @@ -0,0 +1,10 @@ +on: push +jobs: + test1: + runs-on: ubuntu-oldest + steps: + - run: echo + test2: + runs-on: [ubuntu-latest, windows-latest, macos-latest] + steps: + - run: echo diff --git a/testdata/ok/minimal.yaml b/testdata/ok/minimal.yaml new file mode 100644 index 000000000..f1787617a --- /dev/null +++ b/testdata/ok/minimal.yaml @@ -0,0 +1,6 @@ +on: push +jobs: + test: + runs-on: ubuntu-latest + steps: + - run: echo