Skip to content

Commit 0223c49

Browse files
build(lint): consolidate linters under pre-commit
Migrate all linting tools to pre-commit for unified local and CI execution. - Add .pre-commit-config.yaml with hooks for file checks, Python version consistency, EditorConfig, markdownlint-cli2, Ruff, mypy, and Mergify - Add conditional_hook.py wrapper for a graceful skip (with a warning) when requirements for linters (like Node.js for markdownlint-cli2) are not installed locally - Update Hatch lint environment with pre-commit integration - Rename .markdownlint-config.yaml to .markdownlint.yaml for markdownlint-cli2 compatibility - Update AGENTS.md and docs/develop.md with pre-commit documentation
1 parent bfdda7e commit 0223c49

File tree

12 files changed

+317
-115
lines changed

12 files changed

+317
-115
lines changed

.github/workflows/check.yaml

Lines changed: 5 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -20,53 +20,16 @@ jobs:
2020
with:
2121
python-version: "3.11" # minimum supported lang version
2222

23-
- name: Install dependencies
24-
run: python -m pip install hatch 'click!=8.3.0'
25-
26-
- name: Run pre-commit hooks
27-
run: hatch run lint:install-hooks && hatch run lint:precommit
28-
29-
linter:
30-
name: linter
31-
runs-on: ubuntu-latest
32-
if: ${{ !startsWith(github.ref, 'refs/tags') }}
33-
34-
steps:
35-
- uses: actions/checkout@v6
23+
- name: Set up Node.js (for markdownlint)
24+
uses: actions/setup-node@v4
3625
with:
37-
fetch-depth: 0
38-
39-
- name: Set up Python
40-
uses: actions/setup-python@v6
41-
with:
42-
python-version: "3.11" # minimum supported lang version
26+
node-version: "20"
4327

4428
- name: Install dependencies
4529
run: python -m pip install hatch 'click!=8.3.0'
4630

47-
- name: Run
48-
run: hatch run lint:check
49-
50-
mypy:
51-
name: mypy
52-
runs-on: ubuntu-latest
53-
if: ${{ !startsWith(github.ref, 'refs/tags') }}
54-
55-
steps:
56-
- uses: actions/checkout@v6
57-
with:
58-
fetch-depth: 0
59-
60-
- name: Set up Python
61-
uses: actions/setup-python@v6
62-
with:
63-
python-version: "3.11" # minimum supported lang version
64-
65-
- name: Install dependencies
66-
run: python -m pip install hatch 'click!=8.3.0'
67-
68-
- name: Check MyPy
69-
run: hatch run mypy:check
31+
- name: Run pre-commit hooks
32+
run: hatch run lint:install-hooks && hatch run lint:precommit
7033

7134
pkglint:
7235
name: pkglint
@@ -89,22 +52,6 @@ jobs:
8952
- name: Run
9053
run: hatch run lint:pkglint
9154

92-
markdownlint:
93-
# https://github.com/marketplace/actions/markdown-lint
94-
name: markdownlint
95-
runs-on: ubuntu-latest
96-
if: ${{ !startsWith(github.ref, 'refs/tags') }}
97-
steps:
98-
- uses: actions/checkout@v6
99-
with:
100-
fetch-depth: 0
101-
- uses: articulate/[email protected]
102-
with:
103-
config: .markdownlint-config.yaml
104-
# files: 'docs/**/*.md'
105-
# ignore: node_modules
106-
# version: 0.28.1
107-
10855
docs:
10956
name: readthedocs
11057
runs-on: ubuntu-latest
@@ -125,23 +72,3 @@ jobs:
12572

12673
- name: Run
12774
run: hatch run docs:build
128-
129-
super-linter:
130-
name: super-linter
131-
runs-on: ubuntu-latest
132-
if: ${{ !startsWith(github.ref, 'refs/tags') }}
133-
134-
steps:
135-
- uses: actions/checkout@v6
136-
with:
137-
fetch-depth: 0
138-
- name: Super-Linter
139-
uses: super-linter/[email protected] # x-release-please-version
140-
env:
141-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
142-
# To reuse the same Super-linter configuration that you use in the
143-
# lint job without duplicating it, see
144-
# https://github.com/super-linter/super-linter/blob/main/docs/run-linter-locally.md#share-environment-variables-between-environments
145-
VALIDATE_ALL_CODEBASE: false
146-
VALIDATE_MARKDOWN: true
147-
VALIDATE_EDITORCONFIG: true

.pre-commit-config.yaml

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1+
# Pre-commit configuration
2+
# Run locally: pre-commit run --all-files
3+
# CI runs via: hatch run lint:precommit
4+
15
repos:
6+
# ---------------------------------------------------------------------------
7+
# 1. Standard file checks (fastest, run first)
8+
# ---------------------------------------------------------------------------
29
- repo: https://github.com/pre-commit/pre-commit-hooks
310
rev: v4.5.0
411
hooks:
@@ -9,18 +16,76 @@ repos:
916
- id: check-merge-conflict # Prevents merge conflict markers
1017
- id: check-toml # Validates TOML syntax
1118

19+
# ---------------------------------------------------------------------------
20+
# 2. Python version consistency check
21+
# ---------------------------------------------------------------------------
22+
- repo: https://github.com/mgedmin/check-python-versions
23+
rev: "0.24.0"
24+
hooks:
25+
- id: check-python-versions
26+
args: ["--only", "pyproject.toml,.github/workflows/test.yaml"]
27+
28+
# ---------------------------------------------------------------------------
29+
# 3. EditorConfig validation (polyglot formatting)
30+
# ---------------------------------------------------------------------------
31+
- repo: https://github.com/editorconfig-checker/editorconfig-checker.python
32+
rev: 3.2.1
33+
hooks:
34+
- id: editorconfig-checker
35+
alias: ec
36+
# Exclude files that are auto-generated or strictly formatted elsewhere
37+
exclude: |
38+
(?x)^(
39+
LICENSE|
40+
.*\.lock|
41+
dist/.*
42+
)$
43+
44+
# ---------------------------------------------------------------------------
45+
# 4. Markdown linting
46+
# ---------------------------------------------------------------------------
1247
- repo: local
1348
hooks:
14-
- id: hatch-lint
15-
name: hatch lint check
16-
entry: hatch run lint:check
49+
- id: markdownlint
50+
name: markdownlint
51+
entry: python scripts/conditional_hook.py markdownlint-cli2
52+
args: ["--fix"]
1753
language: system
18-
types: [python]
19-
pass_filenames: false
54+
types: [markdown]
55+
require_serial: true
56+
verbose: true
2057

58+
# ---------------------------------------------------------------------------
59+
# 5. Python linting & formatting (Ruff - official hooks)
60+
# ---------------------------------------------------------------------------
61+
- repo: https://github.com/astral-sh/ruff-pre-commit
62+
rev: v0.11.11
63+
hooks:
64+
# Linter: Runs BEFORE formatter
65+
# Rationale: Fixes (like removing imports) change line lengths
66+
- id: ruff-check
67+
args: [--fix]
68+
types_or: [python, pyi]
69+
70+
# Formatter: Runs AFTER linter to clean up any changes
71+
- id: ruff-format
72+
types_or: [python, pyi]
73+
74+
# ---------------------------------------------------------------------------
75+
# 6. Local hooks (tools without official pre-commit hooks)
76+
# ---------------------------------------------------------------------------
77+
- repo: local
78+
hooks:
2179
- id: hatch-mypy
2280
name: hatch mypy check
2381
entry: hatch run mypy:check
2482
language: system
2583
types: [python]
2684
pass_filenames: false
85+
86+
- id: mergify-lint
87+
name: mergify lint
88+
entry: hatch run lint:mergify
89+
language: system
90+
files: ^\.mergify\.yml$
91+
pass_filenames: false

AGENTS.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,15 +74,15 @@ hatch run lint:precommit # All linters and other pre-commit hooks
7474

7575
### Pre-commit Hooks
7676

77-
The project uses pre-commit hooks to automatically check file formatting:
77+
Run all linters and formatters via pre-commit:
7878

79-
- **File endings**: Ensures all files end with a single newline
80-
- **Whitespace**: Removes trailing whitespace
81-
- **Syntax**: Validates YAML/TOML files
82-
- **Conflicts**: Prevents committing merge conflict markers
83-
- **Linters**: Runs the `mypy` and `ruff` linters
79+
```bash
80+
hatch run lint:precommit # Run all hooks manually
81+
```
82+
83+
Pre-commit runs automatically on commit after installation with `hatch run lint:install-hooks`.
8484

85-
These run automatically on commit if installed with `hatch run lint:install-hooks`.
85+
**Conditional hooks:** The markdownlint hook skips with a warning when Node.js is unavailable locally. In CI, missing Node.js fails the build.
8686

8787
## Safety and Permissions
8888

docs/develop.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,45 @@
11
# Developing these tools
22

3+
## Pre-commit hooks
4+
5+
The project uses [pre-commit](https://pre-commit.com/) to run linters and formatters automatically on each commit. This ensures consistent code quality across all contributions.
6+
7+
### Setup
8+
9+
Install the hooks once after cloning:
10+
11+
```bash
12+
hatch run lint:install-hooks
13+
```
14+
15+
### Running hooks
16+
17+
Hooks run automatically when you commit. To run all hooks manually:
18+
19+
```bash
20+
hatch run lint:precommit
21+
```
22+
23+
### What the hooks check
24+
25+
- **File formatting**: Trailing whitespace, final newlines, YAML/TOML syntax
26+
- **Python**: Ruff (linting + formatting), mypy (type checking)
27+
- **Markdown**: markdownlint (style and consistency)
28+
- **Config validation**: EditorConfig, Mergify, Python version consistency
29+
30+
### Optional: Markdown linting
31+
32+
The markdownlint hook requires Node.js. If Node.js isn't installed on your system:
33+
34+
- **Locally**: The hook skips with a warning—your commit proceeds normally
35+
- **In CI**: The hook runs strictly and will fail if markdownlint fails to run
36+
37+
To enable markdown linting locally, install Node.js and then:
38+
39+
```bash
40+
npm install -g markdownlint-cli2
41+
```
42+
343
## Unit tests and linter
444

545
The unit tests and linter now use [Hatch](https://hatch.pypa.io/) and a

e2e/flit_core_override/src/package_plugins/flit_core.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import logging
22
import pathlib
3-
import typing
43

54
from packaging.requirements import Requirement
65
from packaging.version import Version
@@ -25,10 +24,15 @@ def build_wheel(
2524
# 'pip wheel'.
2625
#
2726
# https://flit.pypa.io/en/stable/bootstrap.html
28-
logger.info('using override to build flit_core wheel in %s', sdist_root_dir)
27+
logger.info("using override to build flit_core wheel in %s", sdist_root_dir)
2928
external_commands.run(
30-
[str(build_env.python), '-m', 'flit_core.wheel',
31-
'--outdir', str(ctx.wheels_build)],
29+
[
30+
str(build_env.python),
31+
"-m",
32+
"flit_core.wheel",
33+
"--outdir",
34+
str(ctx.wheels_build),
35+
],
3236
cwd=str(sdist_root_dir),
3337
extra_environ=extra_environ,
3438
)

e2e/fromager_hooks/src/package_plugins/hooks.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,7 @@ def after_prebuilt_wheel(
4747
dist_version: str,
4848
wheel_filename: pathlib.Path,
4949
) -> None:
50-
logger.info(
51-
f"running post build hook in {__name__} for {wheel_filename}"
52-
)
50+
logger.info(f"running post build hook in {__name__} for {wheel_filename}")
5351
test_file = ctx.work_dir / "test-prebuilt.txt"
5452
logger.info(f"prebuilt-wheel hook writing to {test_file}")
5553
test_file.write_text(f"{dist_name}=={dist_version}")

e2e/mergify_lint.py

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,19 @@
4141
e2e_dir = pathlib.Path("e2e")
4242
# Look for CI suite scripts instead of individual test scripts
4343
ci_suite_jobs = set(
44-
script.name[:-len(".sh")] for script in e2e_dir.glob("ci_*_suite.sh")
44+
script.name[: -len(".sh")] for script in e2e_dir.glob("ci_*_suite.sh")
4545
)
4646
print("found CI suite scripts:\n ", "\n ".join(sorted(ci_suite_jobs)), sep="")
4747

4848
# Also find all individual e2e test scripts to ensure they're all covered
4949
individual_e2e_scripts = set(
5050
script.name[len("test_") : -len(".sh")] for script in e2e_dir.glob("test_*.sh")
5151
)
52-
print("found individual e2e scripts:\n ", "\n ".join(sorted(individual_e2e_scripts)), sep="")
52+
print(
53+
"found individual e2e scripts:\n ",
54+
"\n ".join(sorted(individual_e2e_scripts)),
55+
sep="",
56+
)
5357

5458
# Remember if we should fail so we can apply all of the rules and then
5559
# exit with an error.
@@ -66,23 +70,27 @@
6670
for ci_suite_file in e2e_dir.glob("ci_*_suite.sh"):
6771
content = ci_suite_file.read_text(encoding="utf8")
6872
# Look for run_test "script_name" calls (excluding commented lines)
69-
for line in content.split('\n'):
73+
for line in content.split("\n"):
7074
# Skip lines that start with # (comments)
7175
stripped = line.strip()
72-
if stripped.startswith('#'):
76+
if stripped.startswith("#"):
7377
continue
7478
for match in re.finditer(r'run_test\s+"([^"]+)"', line):
7579
referenced_scripts.add(match.group(1))
7680

77-
print("scripts referenced in CI suites:\n ", "\n ".join(sorted(referenced_scripts)), sep="")
81+
print(
82+
"scripts referenced in CI suites:\n ",
83+
"\n ".join(sorted(referenced_scripts)),
84+
sep="",
85+
)
7886

7987
# Find any individual e2e scripts that aren't referenced in any CI suite
8088
unreferenced_scripts = individual_e2e_scripts.difference(referenced_scripts)
8189
if unreferenced_scripts:
82-
print(f"\nERROR: The following e2e scripts are not referenced in any CI suite:")
90+
print("\nERROR: The following e2e scripts are not referenced in any CI suite:")
8391
for script in sorted(unreferenced_scripts):
8492
print(f" - {script}")
85-
print(f"Please add these scripts to the appropriate CI suite script.")
93+
print("Please add these scripts to the appropriate CI suite script.")
8694
RC = 1
8795
else:
8896
print("✓ All individual e2e scripts are covered by CI suites!")
@@ -99,15 +107,17 @@
99107
)
100108
)
101109
# for macOS, only expect latest Python version and latest Rust version
102-
expected_jobs.update(set(
103-
str(combo).replace("'", "")
104-
for combo in itertools.product(
105-
python_versions[-1:],
106-
rust_versions[-1:],
107-
test_scripts,
108-
["macos-latest"],
110+
expected_jobs.update(
111+
set(
112+
str(combo).replace("'", "")
113+
for combo in itertools.product(
114+
python_versions[-1:],
115+
rust_versions[-1:],
116+
test_scripts,
117+
["macos-latest"],
118+
)
109119
)
110-
))
120+
)
111121
if not expected_jobs.difference(existing_jobs):
112122
print("found rules for all expected jobs!")
113123
for job_name in sorted(expected_jobs.difference(existing_jobs)):

0 commit comments

Comments
 (0)