Skip to content

Commit

Permalink
implement paths configuration in actionlint.yml
Browse files Browse the repository at this point in the history
for ignoring specific errors per file path patterns
  • Loading branch information
rhysd committed Oct 27, 2024
1 parent 9fe617f commit 20860bb
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 22 deletions.
103 changes: 103 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,90 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"

"github.com/gobwas/glob"
"gopkg.in/yaml.v3"
)

// PathConfig is a configuration for specific file path pattern. This is for values of the "paths" mapping
// in the configuration file.
type PathConfig struct {
glob glob.Glob
// Ignore is a list of patterns. They are used for ignoring errors by matching to the error messages.
// These are similar to the "-ignore" command line option.
Ignore []*regexp.Regexp
}

func newPathConfig(pat string, cfg *yaml.Node) (*PathConfig, error) {
g, err := glob.Compile(pat)
if err != nil {
return nil, fmt.Errorf("error while processing glob pattern %q in \"paths\" config: %w", pat, err)
}

ret := &PathConfig{glob: g}

for i := 0; i < len(cfg.Content); i += 2 {
k, v := cfg.Content[i].Value, cfg.Content[i+1]
switch k {
case "ignore":
if v.Kind != yaml.SequenceNode {
return nil, fmt.Errorf("yaml: \"paths.ignore\" config for %q must be a sequence node", pat)
}
ignore := make([]*regexp.Regexp, 0, len(v.Content))
for _, p := range v.Content {
r, err := regexp.Compile(p.Value)
if err != nil {
return nil, fmt.Errorf("invalid regular expression %q at \"paths.ignore\" config for %q", p.Value, pat)
}
ignore = append(ignore, r)
}
ret.Ignore = ignore
}
}

return ret, nil
}

// Matches returns whether this config is for the given path.
func (cfg *PathConfig) Matches(path string) bool {
return cfg.glob.Match(path)
}

// Ignores returns whether the given error should be ignored due to the "ignore" configuration.
func (cfg *PathConfig) Ignores(err *Error) bool {
for _, r := range cfg.Ignore {
if r.MatchString(err.Message) {
return true
}
}
return false
}

// PathConfigs is a "paths" mapping in the configuration file. The keys are glob patterns matching to
// file paths. And the values are corresponding configurations.
type PathConfigs map[string]*PathConfig

func (cfgs *PathConfigs) UnmarshalYAML(n *yaml.Node) error {
if n.Kind != yaml.MappingNode {
return expectedMapping("paths", n)
}

ret := make(PathConfigs, len(n.Content)/2)
for i := 0; i < len(n.Content); i += 2 {
pat, child := n.Content[i].Value, n.Content[i+1]
cfg, err := newPathConfig(pat, child)
if err != nil {
return err
}
ret[pat] = cfg
}
*cfgs = ret

return nil
}

// Config is configuration of actionlint. This struct instance is parsed from "actionlint.yaml"
// file usually put in ".github" directory.
type Config struct {
Expand All @@ -22,6 +101,21 @@ type Config struct {
// listed here as undefined config variables.
// https://docs.github.com/en/actions/learn-github-actions/variables
ConfigVariables []string `yaml:"config-variables"`
// Paths is a "paths" mapping in the configuration file. See the document for PathConfigs for more details.
Paths PathConfigs `yaml:"paths"`
}

// PathConfigsFor returns a list of all PathConfig values matching to the given path.
func (cfg *Config) PathConfigsFor(path string) []*PathConfig {
var ret []*PathConfig
if cfg != nil {
for _, c := range cfg.Paths {
if c.Matches(path) {
ret = append(ret, c)
}
}
}
return ret
}

func parseConfig(b []byte, path string) (*Config, error) {
Expand Down Expand Up @@ -68,6 +162,15 @@ func writeDefaultConfigFile(path string) error {
# organization. ` + "`null`" + ` means disabling configuration variables check.
# Empty array means no configuration variable is allowed.
config-variables: null
# Configuration for file paths. The keys are glob patterns to match to file
# paths. The values are the configurations for the file paths. Currently only
# "ignore" is available.
# "ignore" is an array of regular expression patterns. Matched errors are
# ignored. This is similar to "-ignore" command line option.
paths:
# .github/workflows/**/*.yml:
# ignore: []
`)
if err := os.WriteFile(path, b, 0644); err != nil {
return fmt.Errorf("could not write default configuration file at %q: %w", path, err)
Expand Down
12 changes: 4 additions & 8 deletions config_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package actionlint

import (
"os"
"path/filepath"
"strings"
"testing"
Expand Down Expand Up @@ -105,13 +104,7 @@ func TestConfigReadFileParseError(t *testing.T) {
}

func TestConfigGenerateDefaultConfigFileOK(t *testing.T) {
dir, err := os.MkdirTemp(filepath.Join("testdata", "config"), "generate")
if err != nil {
panic(err)
}
defer os.RemoveAll(dir)

f := filepath.Join(dir, "test.yml")
f := filepath.Join(t.TempDir(), "default-config-for-test.yml")
if err := writeDefaultConfigFile(f); err != nil {
t.Fatal(err)
}
Expand All @@ -125,6 +118,9 @@ func TestConfigGenerateDefaultConfigFileOK(t *testing.T) {
if c.ConfigVariables != nil {
t.Fatal(c.SelfHostedRunner.Labels)
}
if len(c.Paths) != 0 {
t.Fatal(c.Paths)
}
}

func TestConfigGenerateDefaultConfigFileError(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.18

require (
github.com/fatih/color v1.17.0
github.com/gobwas/glob v0.2.3
github.com/google/go-cmp v0.6.0
github.com/mattn/go-colorable v0.1.13
github.com/mattn/go-runewidth v0.0.16
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
Expand Down
44 changes: 30 additions & 14 deletions linter.go
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ func (l *Linter) check(
}

if err := v.Visit(w); err != nil {
l.debug("error occurred while visiting workflow syntax tree: %v", err)
l.debug("Error occurred while visiting workflow syntax tree: %v", err)
return nil, err
}

Expand All @@ -611,19 +611,7 @@ func (l *Linter) check(
}
}

if len(l.ignorePats) > 0 {
filtered := make([]*Error, 0, len(all))
Loop:
for _, err := range all {
for _, pat := range l.ignorePats {
if pat.MatchString(err.Message) {
continue Loop
}
}
filtered = append(filtered, err)
}
all = filtered
}
all = l.filterErrors(all, cfg.PathConfigsFor(path))

for _, err := range all {
err.Filepath = path // Populate filename in the error
Expand All @@ -639,6 +627,34 @@ func (l *Linter) check(
return all, nil
}

func (l *Linter) filterErrors(errs []*Error, cfgs []*PathConfig) []*Error {
if len(l.ignorePats) == 0 && len(cfgs) == 0 {
return errs
}

filtered := make([]*Error, 0, len(errs))
Loop:
for _, err := range errs {
for _, pat := range l.ignorePats {
if pat.MatchString(err.Message) {
l.debug("Error %q is ignored due to -ignore pattern %q", err.Message, pat.String())
continue Loop
}
}
for _, c := range cfgs {
if c.Ignores(err) {
l.debug("Error %q is ignored due to the \"ignore\" config in the config file", err.Message)
continue Loop
}
}
filtered = append(filtered, err)
}
if len(filtered) != len(errs) {
l.log("Filtered", len(errs)-len(filtered), "error(s) due to \"-ignore\" command line option and \"ignore\" configuration")
}
return filtered
}

func (l *Linter) printErrors(errs []*Error, src []byte) {
if l.oneline {
src = nil
Expand Down

0 comments on commit 20860bb

Please sign in to comment.