Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/update-versions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Update versions

on:
schedule:
# Daily check for a new cpp-linter-hooks release
- cron: '20 6 * * *'
workflow_dispatch:

permissions: {}

jobs:
update:
runs-on: ubuntu-latest
permissions:
# push the update branch and open the pull request
contents: write
pull-requests: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set the cpp-linter-hooks rev in the examples
run: python3 scripts/update-versions.py
env:
GITHUB_TOKEN: ${{ github.token }}

- name: Create Pull Request
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
sign-commits: true
commit-message: "docs: update the cpp-linter-hooks version in the examples"
branch: auto-update-versions
delete-branch: true
title: "docs: update the cpp-linter-hooks version in the examples"
body: |
Automated update: the `rev:` of the cpp-linter-hooks examples now names the
latest [cpp-linter-hooks release](https://github.com/cpp-linter/cpp-linter-hooks/releases/latest).
labels: |
documentation
assignees: |
${{ github.actor }}
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ pipx run nox -s docs # build into site/
pipx run nox -s docs-live # preview with live reload
```

## Versions in the examples

The [Update versions](https://github.com/cpp-linter/cpp-linter.github.io/actions/workflows/update-versions.yml)
workflow runs daily and opens a pull request when cpp-linter-hooks has a release newer than the
`rev:` in the pages' examples. To do it by hand, run `python3 scripts/update-versions.py`. Blog
posts keep the version of the day they were written.

## Files other projects use

Other repositories load these files from the published site, so do not rename or remove them
Expand Down
2 changes: 1 addition & 1 deletion docs/ai-coding-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ checks on the staged files at each commit, on the agent's machine and on yours:
```yaml title=".pre-commit-config.yaml"
repos:
- repo: https://github.com/cpp-linter/cpp-linter-hooks
rev: v1.8.1
rev: v1.9.0
hooks:
- id: clang-format
args: [--style=file, --version=21]
Expand Down
4 changes: 2 additions & 2 deletions docs/blog/posts/2026-09-12-one-clang-version-everywhere.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,8 @@ same patch release.

## Containers and other package managers

[Docker images](https://github.com/cpp-linter/clang-tools-docker) carry Ubuntu's clang-format and
clang-tidy packages, tagged by major version up to 22:
[Docker images](https://github.com/cpp-linter/clang-tools-docker) are built from the same static
binaries for LLVM 12 to 23, tagged by major version:

```bash
docker run --rm -v "$PWD":/src xianpengshen/clang-tools:21 clang-format --dry-run --Werror /src/main.cpp
Expand Down
12 changes: 8 additions & 4 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ pip-install the clang-format and clang-tidy version you pin:
```yaml title=".pre-commit-config.yaml"
repos:
- repo: https://github.com/cpp-linter/cpp-linter-hooks
rev: v1.8.1
rev: v1.9.0
hooks:
- id: clang-format
args: [--style=file, --version=21]
Expand Down Expand Up @@ -100,6 +100,8 @@ cpp-linter --version=21 --style=file --tidy-checks=''
- Locally and in other CI, `--files-changed-only` and `--lines-changed-only` read `git diff`;
`--diff-base` sets the commit to compare against. Only on GitHub Actions, which sets
`GITHUB_ACTIONS=true`, do they ask the GitHub API for the changed files.
- [Other CI systems](other-ci.md) has a script and the job definitions for GitLab CI, Jenkins,
Azure Pipelines and Bitbucket Pipelines.

cpp-linter v2, a rewrite in Rust ([cpp-linter-rs](https://github.com/cpp-linter/cpp-linter-rs)),
is in release candidates. Use the Python package until 2.0 is released.
Expand All @@ -123,10 +125,12 @@ have, and explains how to check a download.

The static binaries include clang-format, clang-tidy, clang-query, clang-apply-replacements,
clang-include-cleaner (LLVM 18 and later), clang-scan-deps, llvm-cov, llvm-profdata and
llvm-symbolizer. The Docker images carry Ubuntu's clang-format and clang-tidy packages (Debian
unstable's for LLVM 23). The same images are on Docker Hub as `xianpengshen/clang-tools`.
llvm-symbolizer. The Docker images for LLVM 12 to 23 are built from these static binaries, with
clang's builtin headers and the C and C++ standard library headers; the images for 7 to 11 carry
Ubuntu's packages and are no longer rebuilt. The same images are on Docker Hub as
`xianpengshen/clang-tools`.

Single tools are also on PyPI as Python wheels: `clang-format` and `clang-tidy` (the wheels
cpp-linter-hooks installs), [clang-apply-replacements](https://pypi.org/project/clang-apply-replacements/)
(LLVM 16 to 23) and [clang-include-cleaner](https://pypi.org/project/clang-include-cleaner/)
(LLVM 22).
(LLVM 22 and 23).
171 changes: 171 additions & 0 deletions docs/other-ci.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
---
title: Other CI systems
description: Run clang-format and clang-tidy on the files a branch changes in GitLab CI, Jenkins, Azure Pipelines and Bitbucket Pipelines, with the cpp-linter command.
hide:
- navigation
---

# Other CI systems

[cpp-linter-action](getting-started.md#on-every-pull-request) is for GitHub Actions. On any other
CI system, run the `cpp-linter` command: it reads `git diff`, checks the C and C++ files your
branch changes with the LLVM version you pin, and writes a report.

What you get outside GitHub:

- The findings in the job log, and a Markdown report in `cpp-linter.md` that the job keeps.
- A failed job when there are findings.
- No comments or reviews on the merge request or pull request. cpp-linter posts those on GitHub
only.

## The script

Save this as `ci/cpp-linter.sh`. It is the same for every CI system; only the job definition
around it differs.

```bash title="ci/cpp-linter.sh"
#!/usr/bin/env bash
# Checks the files this branch changes with clang-format and clang-tidy.
# Usage: ci/cpp-linter.sh <target branch>
set -euo pipefail

target="$1"
llvm=21

python3 -m venv .venv
. .venv/bin/activate
pip install --quiet 'cpp-linter<2' clang-tools
clang-tools install clang-format clang-tidy --version "$llvm" --directory .venv/bin --no-progress-bar

git fetch --quiet origin "+refs/heads/$target:refs/remotes/origin/$target"
base="$(git merge-base "origin/$target" HEAD)"

cpp-linter --version="$llvm" --style=file --tidy-checks='' \
--files-changed-only=true --diff-base="$base" \
--summary-output-file=cpp-linter.md 2>&1 | tee cpp-linter.log

# cpp-linter exits with 0 when it finds problems, so fail the job here.
grep -q ':0 checks-failed$' cpp-linter.log
```

- The script needs Python 3.11 or later, git and a full clone: `git merge-base` cannot find the
point where your branch started in a shallow clone. Each job below turns shallow cloning off.
- `clang-tools` downloads static clang-format and clang-tidy binaries into the virtual
environment, with clang's builtin headers. Nothing is installed outside `.venv`.
- `--style=file` and `--tidy-checks=''` use your `.clang-format` and `.clang-tidy`.
- `--files-changed-only=true` checks every line of the files the branch changes. Add
`--lines-changed-only=true` to report only on the changed lines.
- clang-tidy needs a `compile_commands.json` to find your include paths and flags. Build it
before the script runs and add `--database=build` (the directory that holds it).
- `'cpp-linter<2'` keeps the Python package. Version 2, a rewrite in Rust, is in release
candidates and changes some options.

Run it on your machine first, from a branch of your repository:

```bash
bash ci/cpp-linter.sh main
```

## GitLab CI

```yaml title=".gitlab-ci.yml"
cpp-linter:
image: python:3.13
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
variables:
GIT_DEPTH: 0
script:
- bash ci/cpp-linter.sh "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
artifacts:
when: always
paths:
- cpp-linter.md
```

## Jenkins

For a multibranch pipeline, where Jenkins sets `CHANGE_TARGET` on pull request builds:

```groovy title="Jenkinsfile"
pipeline {
agent { docker { image 'python:3.13' } }
stages {
stage('cpp-linter') {
when { changeRequest() }
steps {
sh 'bash ci/cpp-linter.sh "$CHANGE_TARGET"'
}
}
}
post {
always {
archiveArtifacts artifacts: 'cpp-linter.md', allowEmptyArchive: true
}
}
}
```

## Azure Pipelines

```yaml title="azure-pipelines.yml"
pr:
- main

pool:
vmImage: ubuntu-latest

steps:
- checkout: self
fetchDepth: 0
- task: UsePythonVersion@0
inputs:
versionSpec: '3.13'
- script: bash ci/cpp-linter.sh "$(System.PullRequest.targetBranchName)"
displayName: cpp-linter
condition: eq(variables['Build.Reason'], 'PullRequest')
- publish: cpp-linter.md
artifact: cpp-linter
condition: and(always(), eq(variables['Build.Reason'], 'PullRequest'))
```

The `pr` trigger works for repositories on GitHub and Bitbucket Cloud. For a repository in Azure
Repos, start the pipeline from a branch policy's build validation instead.

## Bitbucket Pipelines

```yaml title="bitbucket-pipelines.yml"
image: python:3.13

clone:
depth: full

pipelines:
pull-requests:
'**':
- step:
name: cpp-linter
script:
- bash ci/cpp-linter.sh "$BITBUCKET_PR_DESTINATION_BRANCH"
artifacts:
- cpp-linter.md
```

## What was tested

The script was run as written, on Linux, with cpp-linter 1.16.0 and LLVM 21: it fails on a branch
with findings and passes on a clean one. The four job definitions use each system's documented
variables for the target branch, but were not run on those systems. If one does not work for
you, [open an issue](https://github.com/cpp-linter/cpp-linter.github.io/issues).

## Only clang-format, without Python

The [Docker images](https://github.com/cpp-linter/clang-tools-docker) hold the clang tools and
nothing else to install. This GitLab job checks the formatting of every source file under `src`:

```yaml title=".gitlab-ci.yml"
clang-format:
image: ghcr.io/cpp-linter/clang-tools:21
script:
- find src -name '*.cpp' -o -name '*.h' | xargs -r clang-format --dry-run --Werror
```
2 changes: 1 addition & 1 deletion docs/why-cpp-linter.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ annotations only; if most of yours come from forks, stay with the split workflow

```yaml title="After"
- repo: https://github.com/cpp-linter/cpp-linter-hooks
rev: v1.8.1
rev: v1.9.0
hooks:
- id: clang-format
args: [--style=file, --version=21]
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ exclude_docs: |
nav:
- Home: index.md
- Get started: getting-started.md
- Other CI: other-ci.md
- AI coding agents: ai-coding-agents.md
- Why cpp-linter: why-cpp-linter.md
- Showcase: showcase.md
Expand Down
52 changes: 52 additions & 0 deletions scripts/update-versions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
"""Set the cpp-linter-hooks `rev:` in the pages' examples to the latest release.

Usage:
python3 scripts/update-versions.py [tag]

Without a tag, the script asks GitHub for the latest cpp-linter-hooks release.
Blog posts are left alone: each one shows the version of the day it was written.
"""

from __future__ import annotations

import json
import os
import re
import sys
import urllib.request
from pathlib import Path

HOOKS_REPO = "cpp-linter/cpp-linter-hooks"
DOCS = Path(__file__).resolve().parent.parent / "docs"
REV = re.compile(
rf"(repo: https://github\.com/{re.escape(HOOKS_REPO)}\s*\n\s*rev: )v[0-9][0-9.]*"
)


def latest_tag(repo: str) -> str:
request = urllib.request.Request(
f"https://api.github.com/repos/{repo}/releases/latest",
headers={"Accept": "application/vnd.github+json"},
)
token = os.environ.get("GITHUB_TOKEN")
if token:
request.add_header("Authorization", f"Bearer {token}")
with urllib.request.urlopen(request, timeout=30) as response:
return json.load(response)["tag_name"]


def main() -> None:
tag = sys.argv[1] if len(sys.argv) > 1 else latest_tag(HOOKS_REPO)
if not re.fullmatch(r"v[0-9]+\.[0-9]+\.[0-9]+", tag):
sys.exit(f"Unexpected {HOOKS_REPO} tag: {tag!r}")
for page in sorted(DOCS.glob("*.md")):
text = page.read_text(encoding="utf-8")
updated = REV.sub(rf"\g<1>{tag}", text)
if updated != text:
page.write_text(updated, encoding="utf-8")
print(f"{page.name}: rev set to {tag}")


if __name__ == "__main__":
main()
Loading