From 693bf8bf0b1c2c7a40d08df58e065aef23b48af4 Mon Sep 17 00:00:00 2001 From: Xianpeng Shen <3353385+shenxianpeng@users.noreply.github.com> Date: Tue, 10 Nov 2026 07:30:58 +0000 Subject: [PATCH 1/2] docs: add a page for other CI systems and fix stale facts - New page: run cpp-linter in GitLab CI, Jenkins, Azure Pipelines and Bitbucket Pipelines, with one script and a job definition for each. - cpp-linter-hooks examples use v1.9.0, and a daily workflow opens a pull request when a newer release exists. - The Docker images for LLVM 12 to 23 are built from the static binaries, not from Ubuntu's packages. - clang-include-cleaner wheels exist for LLVM 22 and 23. --- .github/workflows/update-versions.yml | 43 +++++ README.md | 7 + docs/ai-coding-agents.md | 2 +- ...2026-09-12-one-clang-version-everywhere.md | 4 +- docs/getting-started.md | 12 +- docs/other-ci.md | 171 ++++++++++++++++++ docs/why-cpp-linter.md | 2 +- mkdocs.yml | 1 + scripts/update-versions.py | 52 ++++++ 9 files changed, 286 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/update-versions.yml create mode 100644 docs/other-ci.md create mode 100644 scripts/update-versions.py diff --git a/.github/workflows/update-versions.yml b/.github/workflows/update-versions.yml new file mode 100644 index 0000000..a833144 --- /dev/null +++ b/.github/workflows/update-versions.yml @@ -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 }} diff --git a/README.md b/README.md index 2f969e1..31c473d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/ai-coding-agents.md b/docs/ai-coding-agents.md index 7244807..e5848d7 100644 --- a/docs/ai-coding-agents.md +++ b/docs/ai-coding-agents.md @@ -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] diff --git a/docs/blog/posts/2026-09-12-one-clang-version-everywhere.md b/docs/blog/posts/2026-09-12-one-clang-version-everywhere.md index 56f2aa7..a63f80b 100644 --- a/docs/blog/posts/2026-09-12-one-clang-version-everywhere.md +++ b/docs/blog/posts/2026-09-12-one-clang-version-everywhere.md @@ -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 diff --git a/docs/getting-started.md b/docs/getting-started.md index 78f7458..f9b35f1 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -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] @@ -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. @@ -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). diff --git a/docs/other-ci.md b/docs/other-ci.md new file mode 100644 index 0000000..6aa27c9 --- /dev/null +++ b/docs/other-ci.md @@ -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 +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 +``` diff --git a/docs/why-cpp-linter.md b/docs/why-cpp-linter.md index 6dbc652..8f876f9 100644 --- a/docs/why-cpp-linter.md +++ b/docs/why-cpp-linter.md @@ -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] diff --git a/mkdocs.yml b/mkdocs.yml index ad5115d..66d851a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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 diff --git a/scripts/update-versions.py b/scripts/update-versions.py new file mode 100644 index 0000000..f6c56c2 --- /dev/null +++ b/scripts/update-versions.py @@ -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() From 2412831acb356634bf9d23b07de8f3cfd9254b7a Mon Sep 17 00:00:00 2001 From: Xianpeng Shen <3353385+shenxianpeng@users.noreply.github.com> Date: Tue, 10 Nov 2026 13:05:51 +0000 Subject: [PATCH 2/2] style: make scripts/update-versions.py executable --- scripts/update-versions.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/update-versions.py diff --git a/scripts/update-versions.py b/scripts/update-versions.py old mode 100644 new mode 100755