ZedThree / clang-tidy-review

Create a pull request review based on clang-tidy warnings
MIT License
90 stars 45 forks source link
actions clang-tidy code-review

Clang-Tidy Review

Create a pull-request review based on the warnings from clang-tidy.

Inspired by clang-tidy-diff, Clang-Tidy Review only runs on the changes in the pull request. This makes it nice and speedy, as well as being useful for projects that aren't completely clang-tidy clean yet.

Where possible, makes the warnings into suggestions so you can apply them immediately.

Returns the number of comments, so you can decide whether the warnings act as suggestions, or check failure.

Doesn't spam by repeating identical warnings for the same line.

Can use compile_commands.json, so you can optionally configure the build how you like first.

Example review

Example usage:

name: clang-tidy-review

# You can be more specific, but it currently only works on pull requests
on: [pull_request]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4

    # Optionally generate compile_commands.json

    - uses: ZedThree/clang-tidy-review@v0.14.0
      id: review

    # Uploads an artefact containing clang_fixes.json
    - uses: ZedThree/clang-tidy-review/upload@v0.14.0
      id: upload-review

    # If there are any comments, fail the check
    - if: steps.review.outputs.total_comments > 0
      run: exit 1

The ZedThree/clang-tidy-review/upload Action is optional (unless using the split workflow, see below), and will upload some of the output files as workflow artefacts. These are useful when there are more comments than can be posted, as well as for applying fixes locally.

Limitations

This is a Docker container-based Action because it needs to install some system packages (the different clang-tidy versions) as well as some Python packages. This that means that there's a two-three minutes start-up in order to build the Docker container. If you need to install some additional packages you can pass them via the apt_packages argument.

Except for very simple projects, a compile_commands.json file is necessary for clang-tidy to find headers, set preprocessor macros, and so on. You can generate one as part of this Action by setting cmake_command to something like cmake . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=on.

GitHub only mounts the GITHUB_WORKSPACE directory (that is, the default place where it clones your repository) on the container. If you install additional libraries/packages yourself, you'll need to make sure they are in this directory, otherwise they won't be accessible from inside this container.

It seems the GitHub API might only accept a limited number of comments at once, so clang-tidy-review will only attempt to post the first max_comments of them (default 25, as this has worked for me).

Inputs

Outputs

Generating compile_commands.json

Very simple projects can get away without a compile_commands.json file, but for most projects clang-tidy needs this file in order to find include paths and macro definitions.

If you use the GitHub ubuntu-latest image as your normal runs-on container, you only install packages from the system package manager, and don't need to build or install other tools yourself, then you can generate compile_commands.json as part of the clang-tidy-review action:

name: clang-tidy-review
on: [pull_request]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4

    - uses: ZedThree/clang-tidy-review@v0.14.0
      id: review
      with:
        # List of packages to install
        apt_packages: liblapack-dev
        # CMake command to run in order to generate compile_commands.json
        cmake_command: cmake . -DCMAKE_EXPORT_COMPILE_COMMANDS=on

If you don't use CMake, this may still work for you if you can use a tool like bear for example.

You can also generate this file outside the container, e.g. by adding -DCMAKE_EXPORT_COMPILE_COMMANDS=ON to a cmake command in an earlier action and omitting the cmake_command paramter.

Use in a non-default location

If you're using the container argument in your GitHub workflow, downloading/building other tools manually, or not using CMake, you will need to generate compile_commands.json before the clang-tidy-review action. However, the Action is run inside another container, and due to the way GitHub Actions work, clang-tidy-review ends up running with a different absolute path.

What this means is that if compile_commands.json contains absolute paths, clang-tidy-review needs to adjust them to where it is being run instead. By default, it replaces absolute paths that start with the value of ${GITHUB_WORKSPACE} with the new working directory.

If you're running in a container other than a default GitHub container, then you may need to pass the working directory to base_dir. Unfortunately there's not an easy way for clang-tidy-review to auto-detect this, so in order to pass the current directory you will need to do something like the following:

name: clang-tidy-review
on: [pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    # Using another container changes the
    # working directory from GITHUB_WORKSPACE
    container:
      image: my-container

    steps:
    - uses: actions/checkout@v4

    # Get the current working directory and set it
    # as an environment variable
    - name: Set base_dir
      run: echo "base_dir=$(pwd)" >> $GITHUB_ENV

    - uses: ZedThree/clang-tidy-review@v0.14.0
      id: review
      with:
        # Tell clang-tidy-review the base directory.
        # This will get replaced by the new working
        # directory inside the action
        base_dir: ${{ env.base_dir }}

Usage in fork environments (Split workflow)

Actions from forks are limited in their permissions for your security. To support this use case, you can use the split workflow described below.

Example review workflow:

name: clang-tidy-review

# You can be more specific, but it currently only works on pull requests
on: [pull_request]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4

    # Optionally generate compile_commands.json

    - uses: ZedThree/clang-tidy-review@v0.14.0
      with:
        split_workflow: true

    - uses: ZedThree/clang-tidy-review/upload@v0.14.0

The clang-tidy-review/upload Action will automatically upload the following files as workflow artefacts:

Example post comments workflow:

name: Post clang-tidy review comments

on:
  workflow_run:
    # The name field of the lint action
    workflows: ["clang-tidy-review"]
    types:
      - completed

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: ZedThree/clang-tidy-review/post@v0.14.0
        # lgtm_comment_body, max_comments, and annotations need to be set on the posting workflow in a split setup
        with:
          # adjust options as necessary
          lgtm_comment_body: ''
          annotations: false
          max_comments: 10

This Action will try to automatically download clang-tidy-review-{output,metadata}.json from the workflow that triggered it.

The review workflow runs with limited permissions and no access to repo/organisation secrets, while the post comments workflow has the required permissions because it's triggered by the workflow_run event and always uses the version of the workflow in the original repo.

Read more about workflow security limitations here.

Ensure that your workflow name doesn't contain any special characters as Github does not treat on.workflow_run.workflows literally.

Project layout

This project is laid out as follows:

.
├── action.yml       # The `review` Action
├── Dockerfile
└── post
    ├── action.yml   # The `post` Action
    ├── Dockerfile
    └── clang_tidy_review      # Common python package
        └── clang_tidy_review
            ├── __init__.py
            ├── post.py        # Entry point for `post`
            └── review.py      # Entry point for `review`

In order to accommodate the split workflow, the review and post actions must have their own Action metadata files. GitHub requires this file to be named exactly action.yml, so they have to be in separate directories. The associated Dockerfiles must also be named exactly Dockerfile, so they also have to be separate directories.

Lastly, we want to be able to reuse the python package between the two Actions, which means it must be in a subdirectory of both Dockerfiles because they can't see parent directories.

Which is why we've ended up with this slightly strange structure! This way, we can COPY the python package into both Docker images.

Real world project samples

Project Workflow
BOUT++ CMake
Mudlet CMake + Qt