diff --git a/docs/usage.md b/docs/usage.md index 5c1c995c5..76c8f4e91 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -42,6 +42,35 @@ processes. actionlint -shellcheck= -pyflakes= ``` +### Ignore error by inline comment + +To ignore an error by inline comment, use `# actionlint ignore=` or `# actionlint ignore=,,...` comment in the workflow file. +The comment should be placed at the before the line where the error is reported. Ignore pattern is the same as `-ignore` option. + +```yaml + # actionlint ignore=undefined variable + - run: echo '${{ foo }}' +``` + +```yaml + # actionlint ignore=undefined variable ".*" + - run: echo '${{ foo }}' +``` + +```yaml + - name: ignore shellcheck errors + # actionlint ignore=SC2016 + run: | + actionlint -format '{{range $err := .}}::error file={{$err.Filepath}},line={{$err.Line}},col={{$err.Column}}::{{$err.Message}}{{end}}' .github/workflows/*.yml +``` + +```yaml + # actionlint ignore=undefined variable, potentially untrusted + - run: | + echo '${{ foo }}' + echo '${{ github.event.head_commit.author.name }}' +``` + ### Format error messages diff --git a/linter.go b/linter.go index 9894a6dea..ae0bcf305 100644 --- a/linter.go +++ b/linter.go @@ -500,7 +500,7 @@ func (l *Linter) check( l.debug("No config was found") } - w, all := Parse(content) + w, i, all := Parse(content) if l.logLevel >= LogLevelVerbose { elapsed := time.Since(start) @@ -599,6 +599,24 @@ func (l *Linter) check( all = filtered } + // ignore inline ignore patterns + if len(i) > 0 { + filtered := make([]*Error, 0, len(all)) + Loop2: + for _, err := range all { + patterns, ok := i[err.Line] + if ok { + for _, p := range patterns { + if p.MatchString(err.Message) { + continue Loop2 + } + } + } + filtered = append(filtered, err) + } + all = filtered + } + for _, err := range all { err.Filepath = path // Populate filename in the error } diff --git a/parse.go b/parse.go index 989b02f0e..2ee9a2763 100644 --- a/parse.go +++ b/parse.go @@ -1402,14 +1402,41 @@ func handleYAMLError(err error) []*Error { return []*Error{yamlErr(err.Error())} } +var ignoreRe = regexp.MustCompile("(?:^|\n)\\s*#\\s*actionlint\\s+ignore=([^\n]+)\\s*$") + +func visitNodeForIgnores(n *yaml.Node, i *map[int][]regexp.Regexp) { + if n.HeadComment != "" { + line := posAt(n).Line + matches := ignoreRe.FindAllStringSubmatch(n.HeadComment, -1) + for _, m := range matches { + if len(m) > 1 { + patterns := strings.Split(m[1], ",") + (*i)[line] = make([]regexp.Regexp, 0, len(patterns)) + for _, p := range patterns { + r, err := regexp.Compile(p) + if err != nil { + continue + } + (*i)[line] = append((*i)[line], *r) + } + } + } + } + if n.Content != nil { + for _, c := range n.Content { + visitNodeForIgnores(c, i) + } + } +} + // Parse parses given source as byte sequence into workflow syntax tree. It returns all errors // detected while parsing the input. It means that detecting one error does not stop parsing. Even // if one or more errors are detected, parser will try to continue parsing and finding more errors. -func Parse(b []byte) (*Workflow, []*Error) { +func Parse(b []byte) (*Workflow, map[int][]regexp.Regexp, []*Error) { var n yaml.Node if err := yaml.Unmarshal(b, &n); err != nil { - return nil, handleYAMLError(err) + return nil, nil, handleYAMLError(err) } // Uncomment for checking YAML tree @@ -1417,6 +1444,8 @@ func Parse(b []byte) (*Workflow, []*Error) { p := &parser{} w := p.parse(&n) + i := map[int][]regexp.Regexp{} + visitNodeForIgnores(&n, &i) - return w, p.errors + return w, i, p.errors } diff --git a/testdata/err/ignore_not_works.out b/testdata/err/ignore_not_works.out new file mode 100644 index 000000000..d8bf49ae2 --- /dev/null +++ b/testdata/err/ignore_not_works.out @@ -0,0 +1,3 @@ +test.yaml:14:24: "github.event.head_commit.author.name" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions for more details [expression] +test.yaml:16:24: "github.event.head_commit.author.name" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions for more details [expression] +test.yaml:18:24: "github.event.head_commit.author.name" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions for more details [expression] diff --git a/testdata/err/ignore_not_works.yaml b/testdata/err/ignore_not_works.yaml new file mode 100644 index 000000000..f75361133 --- /dev/null +++ b/testdata/err/ignore_not_works.yaml @@ -0,0 +1,20 @@ +on: + push: + +jobs: + actionlint: + runs-on: ubuntu-latest + steps: + # OK + # actionlint ignore=potentially untrusted + - run: echo '${{ github.event.head_commit.author.name }}' + # ERROR: ignore should placed last line + # actionlint ignore=potentially untrusted + # dummy comment + - run: echo '${{ github.event.head_commit.author.name }}' + # ERROR: not ignored + - run: echo '${{ github.event.head_commit.author.name }}' # actionlint ignore=potentially untrusted + # ERROR: not ignored + - run: echo '${{ github.event.head_commit.author.name }}' + # actionlint ignore=potentially untrusted + diff --git a/testdata/examples/ignore.out b/testdata/examples/ignore.out new file mode 100644 index 000000000..d65ff54bf --- /dev/null +++ b/testdata/examples/ignore.out @@ -0,0 +1 @@ +test.yaml:11:24: "github.event.pull_request.title" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions for more details [expression] diff --git a/testdata/examples/ignore.yaml b/testdata/examples/ignore.yaml new file mode 100644 index 000000000..dbfc0ef07 --- /dev/null +++ b/testdata/examples/ignore.yaml @@ -0,0 +1,24 @@ +name: Lint GitHub Actions workflows +on: + push: + +jobs: + actionlint: + runs-on: ubuntu-latest + steps: + # ERROR: this step contains an actionlint error + - name: Print pull request title + run: echo '${{ github.event.pull_request.title }}' + # OK: ignore error (using environment variable is more better solution) + - name: Print pull request title + # actionlint ignore=potentially untrusted + run: echo '${{ github.event.pull_request.title }}' + # ERROR: this step contains a shellcheck error + - name: Run Actionlint + run: | + actionlint -format '{{range $err := .}}::error file={{$err.Filepath}},line={{$err.Line}},col={{$err.Column}}::{{$err.Message}}{{end}}' .github/workflows/*.yml + # OK: ignore error + - name: Run Actionlint + # actionlint ignore=SC2016 + run: | + actionlint -format '{{range $err := .}}::error file={{$err.Filepath}},line={{$err.Line}},col={{$err.Column}}::{{$err.Message}}{{end}}' .github/workflows/*.yml diff --git a/testdata/ok/ignore.yaml b/testdata/ok/ignore.yaml new file mode 100644 index 000000000..5b4087458 --- /dev/null +++ b/testdata/ok/ignore.yaml @@ -0,0 +1,45 @@ +on: + push: + +jobs: + actionlint: + runs-on: ubuntu-latest + steps: + # ignore one error + # actionlint ignore=undefined variable ".*" + - run: echo '${{ foo }}' + # multiple ignores + # actionlint ignore=undefined variable, potentially untrusted + - run: | + echo '${{ foo }}' + echo '${{ github.event.head_commit.author.name }}' + - name: ignore shellcheck errors + # actionlint ignore=SC2016 + run: | + actionlint -format '{{range $err := .}}::error file={{$err.Filepath}},line={{$err.Line}},col={{$err.Column}}::{{$err.Message}}{{end}}' .github/workflows/*.yml + - name: ignore actionlint errors + # actionlint ignore=potentially untrusted + run: echo '${{ github.event.head_commit.author.name }}' + - name: ignore errors by regexp + # actionlint ignore=po.*ly un[r-u]*ed + run: echo '${{ github.event.head_commit.author.name }}' + - name: multiline command + # actionlint ignore=potentially untrusted + run: | + echo "hello" + echo '${{ github.event.head_commit.author.name }}' + echo "hello" + - name: ignore shellcheck errors in multiline command + # actionlint ignore=SC2016 + run: | + echo "hello" + actionlint -format '{{range $err := .}}::error file={{$err.Filepath}},line={{$err.Line}},col={{$err.Column}}::{{$err.Message}}{{end}}' .github/workflows/*.yml + echo "hello" + # actionlint ignore=SC2016 + - run: | + actionlint -format '{{range $err := .}}::error file={{$err.Filepath}},line={{$err.Line}},col={{$err.Column}}::{{$err.Message}}{{end}}' .github/workflows/*.yml + # actionlint ignore=undefined variable + - run: | + echo "hello" + echo "${{foo}}" + echo "hello"