Running pre-commit in a reusable GitHub Actions workflow with caching

Kilian KlugeKilian Kluge
2 min read

The pre-commit framework is an awesome utility to enforce coding standards and run sanity checks, such as preventing large files from being committed.

It's not just convenient to run locally on the developers' machines. By using pre-commit to run checks, linters, and formatters in our CI/CD workflows, it's straightfoward to ensure consistency, with our .pre-commit-config.yaml becoming the single source of truth.

The official GitHub action for pre-commit is no longer actively maintained, and I often find that it is not versatile enough. Particularly in a repository that contains several components, each covered by a dedicated workflow, I'd like to

  • define the pre-commit setup and configuration in a single place,

  • cache dependencies and environments to the maximum degree possible,

  • and flexibly determine which files are processed by a given workflow run.

Here's a GitHub Actions workflow that meets these three requirements:

name: Lint

on:
  push:
    # exclude files covered by dedicated workflows
    paths-ignore:
      - 'component/**'
  workflow_call:
    inputs:
      working-directory:
        type: string
        required: false
        default: '.'

jobs:
  pre-commit:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python
        id: setup-python
        uses: actions/setup-python@v5
        with:
          python-version: '3.10'
          cache: 'pip'

      - uses: actions/cache@v4
        with:
          path: ~/.cache/pre-commit
          key: >
            ${{ format('pre-commit-{0}-{1}',
            steps.setup-python.outputs.python-version,
            hashFiles('.pre-commit-config.yaml')
            ) }}

      - name: Install pre-commit
        run: |
          pip install --upgrade pip
          pip install pre-commit
          pre-commit install

      - name: Run pre-commit hooks
        working-directory: ${{ inputs.working-directory }}
        run: |
          git ls-files | xargs pre-commit run \
                         --show-diff-on-failure \
                         --color=always \
                         --files

This runs as a standalone workflow but can also be called by other workflows. If the main pre-commit workflow is stored as .github/workflows/lint.yml, you can integrate it into other workflows as follows:

name: Component

on:
  push:
    paths:
      - 'component/**'

jobs:
  lint:
    uses: ./.github/workflows/lint.yml
    with:
      working-directory: component
0
Subscribe to my newsletter

Read articles from Kilian Kluge directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Kilian Kluge
Kilian Kluge

My journey into software and infrastructure engineering started in a physics research lab, where I discovered the merits of loose coupling and adherence to standards the hard way. I like automated testing, concise documentation, and hunting complex bugs.