diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index 0c082e4..82f7531 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -16,10 +16,18 @@ jobs: with: fetch-depth: 0 - # Rules come from the committed .commitlintrc.json — the same file the local - # pre-commit commit-msg hook reads — so local and CI validation cannot drift. - - name: Install commitlint - run: npm install -g @commitlint/{cli,config-conventional} + # Rules come from the committed .commitlintrc.json, and the commitlint, preset and + # Node versions from the `commitlint` hook in .pre-commit-config.yaml. CI runs that + # same hook through pre-commit rather than its own npm install, so local and CI + # validation cannot drift. + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Cache the pre-commit hook environment + uses: actions/cache@v6 + with: + path: ~/.cache/pre-commit + key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }} - name: Validate commits run: | @@ -37,9 +45,12 @@ jobs: echo "No PR-unique commits to lint (mergeback/back-merge or merge-only PR)." exit 0 fi + msg="${RUNNER_TEMP}/COMMIT_EDITMSG" rc=0 while read -r sha; do echo "── ${sha} ──" - git log -1 --format=%B "${sha}" | npx commitlint --verbose || rc=1 + git log -1 --format=%B "${sha}" > "${msg}" + uvx pre-commit run commitlint --verbose \ + --hook-stage commit-msg --commit-msg-filename "${msg}" || rc=1 done <<< "${commits}" exit "${rc}" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 30ccdac..56fced2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,10 +33,21 @@ repos: files: ^src/swgoh_comlink/ types: [python] - # ── Commitlint (mirrors CI commitlint job) ──────────────────────────── - - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook - rev: v9.21.0 + # ── Commitlint (CI's Commit Lint job runs this exact hook) ──────────── + # Every version is pinned here and nowhere else: .github/workflows/commitlint.yml + # invokes this hook through pre-commit, so local and CI linting cannot drift. A + # third-party hook rev would only pin a caret range (its lockfile is not honoured), + # and commitlint changes rule behaviour across majors. Bump cli and + # config-conventional together; language_version makes pre-commit fetch its own Node + # (commitlint 21 needs >= 22.12) instead of depending on whatever is installed. + - repo: local hooks: - id: commitlint + name: commitlint + entry: commitlint --edit + language: node + language_version: "24.21.0" stages: [commit-msg] - additional_dependencies: ["@commitlint/config-conventional"] + additional_dependencies: + - "@commitlint/cli@21.2.3" + - "@commitlint/config-conventional@21.2.3" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d43771b..79206a7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -133,7 +133,7 @@ comlink-python/ │ ├── workflows/ │ │ ├── backmerge.yml # Stages the main → develop back-merge after a release │ │ ├── ci.yml # CI pipeline (lint, type-check, test, docs, build) -│ │ ├── commitlint.yml # Commit message validation (npm-based) +│ │ ├── commitlint.yml # Commit message validation (runs the pre-commit hook) │ │ ├── docs.yml # Publishes the docs site to GitHub Pages from main │ │ ├── integration.yml # Integration tests against live comlink services │ │ ├── labeler.yml # Auto-label PRs by file path @@ -370,6 +370,9 @@ Other accepted types — `build`, `deps`, `chore`, `test`, `style`, `ci` — are Types and scopes are defined in [`.commitlintrc.json`](.commitlintrc.json), the single config used by both the pre-commit `commit-msg` hook and the CI **Commit Lint** check. Unknown scopes produce a warning, not a failure. +The commitlint, preset and Node versions are pinned once, on the `commitlint` hook in +[`.pre-commit-config.yaml`](.pre-commit-config.yaml); CI runs that same hook, so a message that passes locally +passes in CI. **Examples:**