/differential-shellcheck

šŸš GitHub Action for running ShellCheck differentially

Primary LanguageShellGNU General Public License v3.0GPL-3.0

Differential ShellCheck

Differential ShellCheck

GitHub Marketplace Lint Code Base Unit Tests

OSSF-Scorecard Score OpenSSF Best Practices codecov

This repository hosts code for running Differential ShellCheck in GitHub Actions. Idea of having something like a Differential ShellCheck was first introduced in @fedora-sysv/initscripts. Initscripts needed some way to verify incoming Pull Requests without getting warnings and errors about already merged and for years working code. Therefore, Differential ShellCheck was born.

How does it work

First Differential ShellCheck gets a list of changed shell scripts based on file extensions, shebangs and script list, if provided. Then it calls @koalaman/shellcheck on those scripts where it stores ShellCheck output for later use. Then it switches from HEAD to provided BASE and runs ShellCheck on the same files as before and stores output to a separate file.

To evaluate results, Differential ShellCheck uses utilities csdiff and csgrep from @csutils/csdiff. First csdiff is used to get a list/number of fixed and added errors. And then csgrep is used to output the results in a nice colorized way to console and optionally into GitHub GUI as a security alert.

Features

  • Shell scripts auto-detection based on shebangs, ShellCheck directives, file extensions and more
    • supported shell interpreters are: sh, ash, bash, dash, ksh and bats
    • supported shebangs are: #!/bin/, #!/usr/bin/, #!/usr/local/bin/, #!/bin/envā£, #!/usr/bin/envā£ and #!/usr/local/bin/envā£ ; e.g. #!/bin/envā£bash
    • support for ShellCheck directives ; e.g. # shellcheck shell=bash
    • support for emacs modes specifications ; e.g. # -*- sh -*-
    • support for vi/vim modeline specifications ; e.g. # vi: set filetype=sh, # vim: ft=sh
  • Ability to allowlist specific error codes
  • Statistics about fixed and added defects and their severity
  • Colored console output with emojis
  • SARIF support - warnings are visible in the Changed files tab of the Pull-Request and as comment alerts on Pull-Requests
  • Ability to run in a verbose mode when run with debug option
  • Results displayed as Job Summaries - example
  • Ability to configure Differential ShellCheck using .shellcheckrc

Usage

Example of running Differential ShellCheck:

name: Differential ShellCheck
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

permissions:
  contents: read

jobs:
  lint:
    runs-on: ubuntu-latest

    permissions:
      # required for all workflows
      security-events: write

      # only required for workflows in private repositories
      actions: read
      contents: read

    steps:
      - name: Repository checkout
        uses: actions/checkout@v4
        with:
          # Differential ShellCheck requires full git history
          fetch-depth: 0

      - id: ShellCheck
        name: Differential ShellCheck
        uses: redhat-plumbers-in-action/differential-shellcheck@v5
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

      - if: always()
        name: Upload artifact with ShellCheck defects in SARIF format
        uses: actions/upload-artifact@v4
        with:
          name: Differential ShellCheck SARIF
          path: ${{ steps.ShellCheck.outputs.sarif }}

Important

fetch-depth: 0 is required to run differential-shellcheck successfully. It fetches all git history.

Console output example

Console output example

Example of Job Summary

Example of Job Summary

Example of output in Changed files tab

Example of output in Changed files tab

Example of @github-code-scanning bot review comment

Example of @github-code-scanning bot review comment

Real life examples of usage

Configuration options

Action currently accepts following options:

# ...

- name: Differential ShellCheck
  uses: redhat-plumbers-in-action/differential-shellcheck@v5
  with:
    triggering-event: <name of triggering event>
    base: <sha1>
    head: <sha1>
    pull-request-base: <sha1>
    pull-request-head: <sha1>
    push-event-base: <sha1>
    push-event-head: <sha1>
    diff-scan: <true or false>
    strict-check-on-push: <true or false>
    external-sources: <true or false>
    severity: <minimal severity level>
    scan-directory: <list of paths>
    exclude-path: <list of paths>
    include-path: <list of paths>
    token: <GitHub token>

# ...

triggering-event

The name of the event that triggered the workflow run. Supported values are: merge_group, pull_request, push and manual.

  • default value: ${{ github.event_name }}
  • requirements: optional

base

SHA1 of the commit which will be used as the base when performing differential ShellCheck. Input is used only when triggering-event is set to manual.

  • default value: undefined
  • requirements: optional

head

SHA1 of the commit which refers to the HEAD of changes. Input is used only when triggering-event is set to manual.

  • default value: undefined
  • requirements: optional

merge-group-base

SHA1 of the merge group's parent commit. Input is used when triggering-event is set to merge_group.

  • default value: ${{ github.event.merge_group.base_sha }}
  • requirements: optional

merge-group-head

SHA1 of the merge group commit. Input is used when triggering-event is set to merge_group.

  • default value: ${{ github.event.merge_group.head_sha }}
  • requirements: optional

pull-request-base

SHA1 of the top commit on the base branch. Input is used when triggering-event is set to pull_request.

  • default value: ${{ github.event.pull_request.base.sha }}
  • requirements: optional

pull-request-head

SHA1 of the latest commit in Pull Request. Input is used when triggering-event is set to pull_request.

  • default value: ${{ github.event.pull_request.head.sha }}
  • requirements: optional

push-event-base

SHA1 of the last commit before the push. Input is used when triggering-event is set to push.

  • default value: ${{ github.event.before }}
  • requirements: optional

push-event-head

SHA1 of the last commit after push. Input is used when triggering-event is set to push.

  • default value: ${{ github.event.after }}
  • requirements: optional

diff-scan

Input allows requesting a specific type of scan. Input is considered only if triggering-event is set to manual.

Default types of scans based on triggering-event input:

triggering-event type of scan
merge_group differential
pull_request differential
push full
manual based on diff-scan input
  • default value: true
  • requirements: optional

strict-check-on-push

Differential ShellCheck performs full scans when running on a push event, but the Action fails only when new defects are added. This option allows overwriting this behavior. Hence when strict-check-on-push is set to true it will fail when any defect is discovered.

  • default value: false
  • requirements: optional

external-sources

Enable following of source statements even when the file is not specified as input. By default, ShellCheck will only follow files specified on the command-line (plus /dev/null). This option allows following any file the script may source. This option may also be enabled using external-sources=true in .shellcheckrc.

  • default value: true
  • requirements: optional

severity

Minimal severity level of detected errors that will be reported. Valid values in order of severity are error, warning, info and style.

  • default value: style
  • requirements: optional

scan-directory

List of relative paths to directories that will be scanned for shell scripts. Globbing is supported. The list is a multi-line string, not a YAML list.

By default the whole repository is scanned. This feature is useful when you want to scan only a subset of the repository.

This feature is fully compatible with exclude-path and include-path options.

  • requirements: optional

  • example: "build/**"

  • example for multiple values:

    scan-directory: |
      build/**
      testing

exclude-path

List of relative paths excluded from ShellCheck scanning. Globbing is supported. The list is a multi-line string, not a YAML list.

  • requirements: optional
  • example: "test/{bats,beakerlib}/**"

include-path

List of file paths that will be scanned by ShellCheck. Globbing is supported. The list is a multi-line string, not a YAML list.

  • requirements: optional
  • example: "src/**.{shell,custom}"

display-engine

Tool used to display the defects and fixes in the console output. Currently supported tools are csgrep and sarif-fmt.

csgrep output example

`display-engine: csgrep`

sarif-fmt output example

`display-engine: sarif-fmt`

  • requirements: optional
  • default value: csgrep

token

The token is used to upload findings in SARIF format to GitHub.

  • default value: undefined
  • requirements: optional

The token needs to have the following permissions:

  • security_events: write - required for all repositories.
  • actions: read and contents: read - required only for private repositories.

Tip

When the token isn't passed, the SARIF file won't be uploaded (the GitHub Security Dashboard won't be updated), but the Action will work as expected. SARIF file can also be uploaded manually using sarif from outputs and github/codeql-action/upload-sarif GitHub Action.

Outputs

Differential ShellCheck exposes following outputs.

sarif

Relative path to the SARIF file containing detected defects. Example of how to use sarif output within the workflow:

- id: ShellCheck
  name: Differential ShellCheck
  uses: redhat-plumbers-in-action/differential-shellcheck@v5

- if: always()
  name: Upload artifact with ShellCheck defects in SARIF format
  uses: actions/upload-artifact@v4
  with:
    name: Differential ShellCheck SARIF
    path: ${{ steps.ShellCheck.outputs.sarif }}

- if: always()
  name: Upload SARIF to GitHub using github/codeql-action/upload-sarif
  uses: github/codeql-action/upload-sarif@v2
  with:
    sarif_file: ${{ steps.ShellCheck.outputs.sarif }}

Tip

sarif output can be used together with tools like microsoft/sarif-tools to convert SARIF to other formats like codeclimate, csv, docx and more. Example of use.

html

Relative path to the HTML file containing detected defects. Example of how to use html output within the workflow:

- id: ShellCheck
  name: Differential ShellCheck
  uses: redhat-plumbers-in-action/differential-shellcheck@v5

- if: always()
  name: Upload artifact with ShellCheck defects in HTML format
  uses: actions/upload-artifact@v4
  with:
    name: Differential ShellCheck HTML
    path: ${{ steps.ShellCheck.outputs.html }}

Example of HTML output:

HTML output example

Using with Private repositories

Differential ShellCheck GitHub Action could be used in private repositories by any user. But code scanning-related features are available only for GitHub Enterprise users, as mentioned in GitHub Documentation:

Code scanning is available for all public repositories on GitHub.com. Code scanning is also available for private repositories owned by organizations that use GitHub Enterprise Cloud and have a license for GitHub Advanced Security. For more information, see "About GitHub Advanced Security".

Using with Visual Studio Code

Differential ShellCheck doesn't have a Visual Studio Code plugin, but results can be accessed by using SARIF Viewer Visual Studio Code extension provided by Microsoft. Once installed, you have to connect your GitHub account with Visual Studio Code. Then, if you open a repository that uses Differential ShellCheck, you will see reported defects directly in your Visual Studio Code IDE.

Visual Studio Code SARIF connect

Visual Studio Code SARIF results

Limitations

  • differential-shellcheck Action doesn't run correctly when overwriting commits using --force and when the triggering event is push.

Useful documents: CHANGELOG | ARCHITECTURE | CONTRIBUTING | CODE_OF_CONDUCT | SECURITY | LICENSE