Skip to content

precommit

A cheatsheet for various pre-commit hooks and things that help with linting, formatting, code scans and more. These all help "shift left" the review to eliminate more issues in the development workflow, rather than providing feedback only once the CI system is involved.

The Frameworks๐Ÿ”—

Precommit๐Ÿ”—

Install Precommit๐Ÿ”—

A bit more complicated, depending on the Docker image used and the python tooling installed. Assuming you have pip installed, then run pip install pre-commit --user

Here's some examples to get you started.

Skipping A Precommit Hook๐Ÿ”—

The pre-commit tasks can be overridden on a case by case basis.

The syntax for skipping is simple, just run the task with the name of the hook excluded like this:

  Don't commit to main.....................................................Passed
  check json5..........................................(no files to check)Skipped
  go-fmt...................................................................Passed
  golangci-lint...........................................................Skipped
  go-test-all..............................................................Failed
  - hook id: gotest ๐Ÿ‘ˆ๐Ÿ‘ˆ๐Ÿ‘ˆ๐Ÿ‘ˆ๐Ÿ‘ˆ๐Ÿ‘ˆ๐Ÿ‘ˆ๐Ÿ‘ˆ  # Use the hook id, not the text of the title
  - duration: 8.9s
  - exit code: 2
  • To skip the example above: SKIP='gotest' git commit -am"feat(fancy): my title" -m"- My Message Body" && git pull --rebase && git push.
  • To skip multiple: SKIP='gotest,go-fmt' git myaction.

Filtering & Triggering Tricks๐Ÿ”—

Let's say you have a document directory and want to trigger a report or doc generation if anything in that changes.

You can do this pretty elegantly with pre-commit.

For example, let's add a mage task to generate docs when something in the package directory for go is updated.

repos:
  # for specific updates that should result in an update to matched directories or files.
  - repo: local
    hooks:
      - id: docs:generate
        name: docs:generate
        entry: mage docs:generate
        language: system
        files: ^pkg/
        types: [file, go]

The types is pretty useful to not just try and match on file names.

Use identify-cli which is a python cli and package included when you install pre-commit.

Run it against a directory or file and you'll get the outputs that pre-commit will accept.

For example, against a markdown file: identify-cli README.md and you should get: ["file", "markdown", "non-executable", "text"]. Any of these (or all) can be put to filter when the hook runs.

Against a Go file: ["file", "go", "non-executable", "text"].

LeftHook

Using pre-commit framework heavily, and no longer relying on Lefthook.

Lefthook๐Ÿ”—

A great tool, but requires more work and not as fully featured as pre-commit. In most cases, I'd recommend pre-commit tooling over Lefthook.

If you are just starting out this requires more hands on, but can result in faster checks and commits.

My advice would be to maybe start with pre-commit if you want plug and play, and lefthook if you want to control the pre-commits explicitly and optimize for performance.

As long as you have the Go SDK installed, just run go install github.com/evilmartians/lefthook@master.

This framework is a little "rougher" and less supported than pre-commit framework, but for simple self-maintained hooks I've preferred this as it much faster, and so I end up using it more.

Other installation methods are located at the installation guide 1

Lefhook Tips๐Ÿ”—

  • Originally I broke out lefthook into multiple files, so I could drop them into a directory, but now I stick with one. Since it still requires editing the main file to extend and point to another file, I've found a single file simplier to maintain.
  • Disable parallel for anything formatting files or possible not thread safe. While parallel seems great, most of the pre-commit tasks should run quickly, and formatting and linting files at the same time could lead into conflicts or problems. Use parallel for seperate language test runs perhaps, like running Python tests and Go tests since those shouldn't conflict.
  • piped: true is useful but hides the underlying tasks in the summary, so I suggest avoid unless you have tasks that really should step by step feed into each other. In this case, maybe you should have this just be part of your task run, such as mage lint fmt rather than 2 seperate pre-commit hooks.

Using Lefthook๐Ÿ”—

Here's some updated configurations I've started using.

Output๐Ÿ”—

Reduce the noise:

skip_output:
  - meta
  - success
# - summary
skip:
  - merge
  - rebase

Pre-commit Checks๐Ÿ”—

These are basic quick checks for markdown (docs as code). This illustrates one of the challenges in pre-commit framework tooling.

Ideally, you want the pre-commit checks to only touch the files that changed to make things quick, but this requires some work-arounds, since not all tools support a comma delimited list of files passed in.

One big improvement to lefthook, would be supporting for_each operator, so that cross-platform looping on matched files could be run, instead of having to parse inside the script here. I'm pretty sure that this would be more compatible with various platforms as well, since this I believe uses your native shell, so you'd have to be in WSL2 in Windows, for example, for the bash-like syntax to work.

See ci-configuration-files for markdown lint config examples.

Install gojq or replace with jq if you have it.

pre-commit:
  tags: markdown fmt
  parallel: false
  commands:
    markdownlintfix:
      files: git diff-index --name-only HEAD
      exclude: '.licenses/*'
      glob: '*{.md}'
      run: |
        echo "โšก markdownlint on: {files}"
        for file in {files}
        do
          echo "๐Ÿ”จ markdownlint: $file"
          docker run --rm -v ${PWD}:/workdir --entrypoint="markdownlint-cli2-fix" davidanson/markdownlint-cli2:latest "$file"
        done
    markdownlintcheck:
      files: git diff-index --name-only HEAD
      exclude: '_licenses/*'
      # exclude: '_licenses/*'
      # files: git diff-index --name-only HEAD #git ls-files **/*.md -m #git diff-index --name-only HEAD #git ls-files **/*/*.md  -m
      glob: '*{.md}'
      run: |
        echo "โšก markdownlint on: {files}"
        for file in {files}
        do
          echo "๐Ÿ”จ markdownlint: $file"
          docker run --rm -v ${PWD}:/workdir --entrypoint="markdownlint-cli2" davidanson/markdownlint-cli2:latest "$file"
        done
    # REQUIREMENTS: go install github.com/itchyny/gojq/cmd/gojq@latest # cross platform alternative to jq
    shellcheck:
      tags: gotool gojq
      name: shellcheck
      files: git diff-index --name-only HEAD
      exclude: '.licenses/*'
      glob: '*.sh'
      run: docker run --rm -v ${PWD}:/mnt koalaman/shellcheck:stable --format=json {files}  | gojq
    # REQUIREMENTS: npm install --global prettier
    yamlfmt:
      files: git diff-index --name-only HEAD
      glob: '*.yaml|*.yml'
      exclude: '.licenses/*'
      skip_empty: false
      run: prettier --loglevel warn --no-error-on-unmatched-pattern --write "{.yaml,.yml}"
    # REQUIREMENTS: go install go.atrox.dev/sync-dotenv@latest
    # used to sync default dotenv files to an example file to avoid commits on main .env
    envfile:
      name: update env.example file
      files: '*.env'
      exclude: '.licenses/*'
      run: |
        cd env
        touch .env
        sync-dotenv
    # REQUIREMENTS: Mage Tasks Built (See github.com/sheldonhull/magetools)
    # CI=1 helps reduce formatting output to minimal
    # MAGEFILE_HASHFAST improves speed of calling mage by assuming your tasks haven't changed
    go:
      piped: true
      tags: go lint fmt
      files: git diff-index --name-only HEAD
      exclude: '.licenses/*'
      glob: '*.{go,mod,sum}' #'*.go|*.mod|*.sum'
      commands:
        fmt:
          run: CI=1 MAGEFILE_HASHFAST=1 mage fmt
        lint:
          run: CI=1 MAGEFILE_HASHFAST=1 mage lint

Pre-Push Checks๐Ÿ”—

Most of these Mage oriented tasks from my magetools repo.

Note that while they filter based on the files being Go related, they run against the entire repo.

pre-push:
  parallel: false
  commands:
    fmt:
      files: git diff-index --name-only HEAD
      exclude: '.licenses/*'
      glob: '*.{go,mod,sum}'
      # run: CI=1 MAGEFILE_HASHFAST=1 mage fmt      # alias for task that can contain formatting for all fmt tasks if you wish
      # run: CI=1 MAGEFILE_HASHFAST=1 mage go:fmt   # gofumpt based formatting
      run: CI=1 MAGEFILE_HASHFAST=1 mage go:wrap  # golines based formatting
    lint:
      files: git diff-index --name-only HEAD
      exclude: '.licenses/*'
      glob: '*.{go,mod,sum}'
      run: CI=1 MAGEFILE_HASHFAST=1 mage lint
    test:
      files: git diff-index --name-only HEAD
      exclude: '.licenses/*'
      glob: '*.{go,mod,sum}'
      run: CI=1 MAGEFILE_HASHFAST=1 GOTEST_FLAGS='-tags integration' mage go:test
    gitleaks:
      tags: security gotool linux macos nowindows
      run: CI=1 MAGEFILE_HASHFAST=1 mage secrets:check