Skip to content

Raise the cognitive-complexity threshold to 30 and clear the rest #1078

Raise the cognitive-complexity threshold to 30 and clear the rest

Raise the cognitive-complexity threshold to 30 and clear the rest #1078

Workflow file for this run

name: Test
# Branches only — release tags are handled by release.yml. Running this on tag
# pushes fired the full matrix needlessly and broke `trunk check`, which diffs
# against `github.event.before` (the prior tag object, gone after a tag move).
on:
push:
branches: ["**"]
permissions: read-all
jobs:
trunk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Trunk Check
uses: trunk-io/trunk-action@v1
- name: Trunk must not own git hooks
run: |
# Git hooks belong to pre-commit (see .pre-commit-config.yaml); trunk's
# git-hook actions are disabled in .trunk/trunk.yaml so trunk never
# clobbers pre-commit's clang-format run. This can only be checked here:
# a pre-commit hook can't (if trunk owned core.hooksPath, pre-commit
# would never run), and a trunk linter can't (it runs in an isolated
# sandbox blind to the real git config).
# 1) Config invariant: no trunk git-hook action may be enabled.
enabled="$(yq -r '.actions.enabled // [] | .[]' .trunk/trunk.yaml)"
for action in trunk-announce trunk-check-pre-commit trunk-check-pre-push \
trunk-check-pre-push-always trunk-fmt-pre-commit trufflehog-pre-commit; do
if grep -qxF "${action}" <<<"${enabled}"; then
echo "::error::trunk git-hook action '${action}' is enabled in .trunk/trunk.yaml; disable it so pre-commit keeps ownership of git hooks"
exit 1
fi
done
# 2) Runtime truth (when trunk is available): sync hooks, then require
# core.hooksPath not point back into trunk's cache.
if command -v trunk >/dev/null 2>&1; then
trunk git-hooks sync 2>&1 || true
hooks_path="$(git config --get core.hooksPath || true)"
if [[ "${hooks_path}" == *trunk* ]]; then
echo "::error::core.hooksPath is trunk-managed (${hooks_path}); trunk grabbed the git hooks"
exit 1
fi
fi
echo "OK: pre-commit owns git hooks; trunk is not managing them"
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- uses: pre-commit/action@v3.0.1
env:
# The `trunk` job runs the full `trunk check`; do not also run trunk
# from pre-commit in CI (see the trunk-fmt hook). Likewise the
# `clang-tidy` job owns clang-tidy: it needs a compile_commands.json
# this job deliberately does not build. `clang-tidy` is `stages:
# [manual]` today, so it is already absent here; naming it keeps that
# true when the hook is promoted to an automatic gate.
SKIP: trunk-fmt,clang-tidy
- uses: pre-commit-ci/lite-action@v1.0.2
if: always()
clang-tidy:
needs: [trunk, pre-commit]
runs-on: ubuntu-latest
# Note there is deliberately no job-level `continue-on-error`: building the
# compile DB must gate, so a broken extractor/toolchain fails this job. Only
# the findings themselves are tolerated, at the step below.
steps:
- uses: actions/checkout@v6
with:
# Full history so the branch can be diffed against `main` to pick the
# changed sources below.
fetch-depth: 0
- uses: bazelbuild/setup-bazelisk@v3
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Select the scope to lint
# clang-tidy costs ~50s per translation unit here, so linting all 81
# sources takes ~24 minutes even though pre-commit already spreads them
# over every core. On a branch, lint only what the branch changed; `main`
# still gets the whole tree after merge.
# Known gap: editing only a header changes findings in the sources that
# include it, and none of those are in the branch's changed-file set. The
# post-merge run on `main` is what catches that.
run: |
set -euo pipefail
if [ "${GITHUB_REF}" = "refs/heads/main" ]; then
echo "CLANG_TIDY_SCOPE=--all-files" >>"${GITHUB_ENV}"
echo "Linting the whole tree (on main)."
else
git fetch --no-tags --quiet origin main
echo "CLANG_TIDY_SCOPE=--from-ref origin/main --to-ref HEAD" >>"${GITHUB_ENV}"
echo "Linting sources changed against origin/main:"
git diff --name-only origin/main...HEAD -- '*.cc' '*.cpp' '*.cxx' | sed 's/^/ /'
fi
- uses: actions/cache/restore@v5
id: cache_restore
with:
path: "~/.cache/bazel"
key: clang-tidy-${{github.ref}}-${{github.sha}}
restore-keys: |
clang-tidy-${{github.ref}}
clang-tidy-refs/heads/main
clang-tidy
- name: Build generated sources and headers
# Generated headers must exist on disk before the compile DB is
# extracted, or the extractor cannot resolve the sources that include
# them (`hash_mangle_seed_gen.h is missing`) and those translation units
# land in the DB degraded - which would later read as clang-tidy
# findings rather than as the build artifact gap it really is.
# Generated headers are the only build output a translation unit needs to
# parse, so this builds just those rather than all of `//...`; the query
# keeps it correct as generated files are added.
run: |
set -euo pipefail
# Read into an array without `mapfile`, so this snippet can also be run
# as-is on stock macOS bash 3.2 when reproducing a CI result locally.
GENERATED=()
while IFS= read -r target; do
GENERATED+=("${target}")
done < <(bazel query 'kind("generated file", //...:*)' |
grep -E '\.(h|hh|hpp|inc|ipp|cc|cpp|cxx)$')
if [ "${#GENERATED[@]}" -eq 0 ]; then
echo "::error::No generated sources/headers found. Either the query or the extension filter needs updating; without this the compile DB silently degrades."
exit 1
fi
printf 'Building %s generated file(s):\n' "${#GENERATED[@]}"
printf ' %s\n' "${GENERATED[@]}"
bazel build --config=clang "${GENERATED[@]}"
- name: Generate compile_commands.json
# The compile DB is a local artifact (gitignored), so it has to be built
# here. The script fetches the hermetic LLVM toolchain if needed and
# records ITS clang as the compiler, which is what makes the resulting
# commands parseable by the matching hermetic clang-tidy.
run: ./compile_commands-update.sh
- uses: actions/upload-artifact@v4
# Only from `main`, which is the run that lints the whole tree: that DB
# is the one worth keeping, to reproduce a finding locally against the
# exact same commands and to inspect flag/toolchain drift. A branch run
# lints only what the branch changed, so its DB describes a near-empty
# lint and is not worth storing.
id: upload_cdb
if: github.ref == 'refs/heads/main'
with:
name: compile-commands-json
path: compile_commands.json
retention-days: 14
if-no-files-found: error
- name: Summarize the compile DB
# Renders at the top of the run page, so the DB's shape and (on main) a
# direct link to it are visible without digging through step logs or
# scrolling to the artifacts panel.
run: |
set -euo pipefail
STATS="$(python3 -c '
import collections, json
entries = json.load(open("compile_commands.json"))
files = collections.Counter(e["file"] for e in entries)
print(len(entries), len(files), sum(n - 1 for n in files.values() if n > 1))
')"
read -r ENTRIES UNIQUE DUPES <<<"${STATS}"
{
echo "### clang-tidy compile database"
echo
echo "| | |"
echo "|---|---|"
echo "| Entries | ${ENTRIES} |"
echo "| Unique files | ${UNIQUE} |"
echo "| Duplicate entries | ${DUPES} |"
echo "| Lint scope | \`${CLANG_TIDY_SCOPE}\` |"
if [ -n "${CDB_ARTIFACT_URL}" ]; then
echo "| Artifact | [compile-commands-json](${CDB_ARTIFACT_URL}) |"
else
echo "| Artifact | not uploaded (branch run; only \`main\` publishes one) |"
fi
} >>"${GITHUB_STEP_SUMMARY}"
env:
CDB_ARTIFACT_URL: ${{steps.upload_cdb.outputs.artifact-url}}
- uses: pre-commit/action@v3.0.1
# TODO(helly25): drop this once the finding sweep lands, which is what
# turns this job into a real gate. `.clang-tidy` sets
# `WarningsAsErrors: '*'` and the tree is not clean yet, so findings are
# reported here rather than enforced. Scoped to this step so that a
# failure to produce the compile DB above still fails the job.
continue-on-error: true
with:
# Only the clang-tidy hook: every other hook already ran in the
# `pre-commit` job. `--hook-stage manual` is required while the hook
# is opt-in; it stays harmless once it becomes an automatic gate.
# Scope comes from the step above: changed sources on a branch, the
# whole tree on main.
extra_args: clang-tidy --hook-stage manual ${{env.CLANG_TIDY_SCOPE}}
- uses: actions/cache/save@v5
if: always() && steps.cache_restore.outputs.cache-hit != 'true'
with:
path: "~/.cache/bazel"
key: clang-tidy-${{github.ref}}-${{github.sha}}
test-gcc:
needs: [trunk, pre-commit]
secrets: inherit
strategy:
matrix:
os: [ubuntu-latest]
compiler: [gcc]
# gcc 14 gets the full config set (asan/cpp23/opt) here, early. gcc 13 is
# still built (opt) later by test-bcr's ubuntu+gcc rungs, so it need not
# repeat in this job.
gcc_version: [14]
bazel_config: [asan, cpp23, opt]
uses: ./.github/workflows/test.yml
with:
continue-on-error: false
os: ${{ matrix.os }}
compiler: ${{ matrix.compiler }}
gcc_version: ${{ matrix.gcc_version }}
bazel_config: ${{ matrix.bazel_config }}
test-bcr:
needs: [trunk, pre-commit, test-gcc]
secrets: inherit
strategy:
matrix:
os: [ubuntu-latest, macos-26]
compiler: [gcc, native, clang]
gcc_version: [13]
# The pinned default toolchain (see bazelmod/llvm.MODULE.bazel).
llvm_version: [22.1.8]
bazel_config: [opt]
# Bazel-version compatibility rungs: one per supported major (we test
# the last three, 7/8/9), crossed with the os/compiler combos below.
# 7.2.1 is the earliest 7.x that works: MODULE.bazel uses `include()`
# (added in 7.2.0, so 7.1.x fails with "name 'include' is not defined")
# and the dep `depend_on_what_you_use@0.16.0` declares
# `bazel_compatibility: [>=7.2.1]` (so 7.2.0 is rejected too). 8.7.0 is
# the latest 8.x; 9.1.1 is the latest 9.x and the checked-in default.
bazel_version: [7.2.1, 8.7.0, 9.1.1]
exclude:
- os: ubuntu-latest
compiler: native
- os: macos-26
compiler: gcc
# The 7.x/8.x rungs are a build-system-compat check (MODULE.bazel
# loading, rules resolution) - platform-agnostic, so run them on
# ubuntu only. macOS keeps the default 9.1.1 for platform coverage.
- os: macos-26
bazel_version: 7.2.1
- os: macos-26
bazel_version: 8.7.0
uses: ./.github/workflows/test.yml
with:
continue-on-error: false
os: ${{ matrix.os }}
compiler: ${{ matrix.compiler }}
gcc_version: ${{ matrix.gcc_version }}
llvm_version: ${{ matrix.llvm_version }}
bazel_config: ${{ matrix.bazel_config }}
bazel_version: ${{ matrix.bazel_version }}
test-clang:
needs: [trunk, pre-commit, test-gcc, test-bcr]
secrets: inherit
strategy:
matrix:
os: [ubuntu-latest, macos-26]
compiler: [clang]
# Hermetic toolchains_llvm clang (independent of the runner's Apple clang).
# Ladder: oldest supported (20.1.8) and the pinned default, also newest (22.1.8).
# TODO(llvm-23): add 23.x here once released and listed in toolchains_llvm.
llvm_version: [20.1.8, 22.1.8]
bazel_config: [asan, cpp23, opt]
exclude:
# asan runs only on the newest toolchain (both platforms); test-gcc
# already exercises asan early (gcc 14). macOS asan must be 22.1.8
# regardless - 20.1.8 hangs in compiler-rt FindDynamicShadowStart on
# macOS 26, while 22.1.8's sanitizer dylib uses toolchains_llvm's
# @loader_path rpath fix (helly25 fork; upstream PR #767) - so
# consolidating asan there also drops the redundant clang-20 asan.
- llvm_version: 20.1.8
bazel_config: asan
# C++23 is used on recent compilers, so exercise cpp23 on the newest
# clang only; the oldest rung just proves the minimum supported LLVM
# still builds (opt).
- llvm_version: 20.1.8
bazel_config: cpp23
uses: ./.github/workflows/test.yml
with:
continue-on-error: false
os: ${{ matrix.os }}
compiler: ${{ matrix.compiler }}
llvm_version: ${{ matrix.llvm_version }}
bazel_config: ${{ matrix.bazel_config }}
done:
needs: [trunk, pre-commit, clang-tidy, test-gcc, test-clang, test-bcr]
if: always()
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Ensure every job is wired into this gate
env:
NEEDS_JSON: ${{ toJSON(needs) }}
run: |
# Parse the workflow with a real YAML reader (yq) and fail if any
# declared job (other than this gate) is absent from `needs` above, so
# a newly added job cannot silently escape the required gate.
declared="$(yq '.jobs | keys | .[]' .github/workflows/main.yml |
grep -vx done | sort)"
wired="$(jq -r 'keys[]' <<<"${NEEDS_JSON}" | sort)"
missing="$(comm -23 <(printf '%s\n' "${declared}") <(printf '%s\n' "${wired}"))"
if [[ -n "${missing}" ]]; then
echo "Jobs declared in the workflow but missing from done.needs:"
while read -r job; do
echo " - ${job}"
echo "::error title=Gate is missing a job::${job} not in done.needs"
done <<<"${missing}"
exit 1
fi
echo "done covers all $(grep -c . <<<"${declared}") workflow jobs."
- name: Fail if any dependency did not succeed
if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')
run: exit 1