diff --git a/.github/actions/setup-runtimes/action.yml b/.github/actions/setup-runtimes/action.yml index 3bea018..cbdbbab 100644 --- a/.github/actions/setup-runtimes/action.yml +++ b/.github/actions/setup-runtimes/action.yml @@ -7,7 +7,7 @@ inputs: linters: description: > Comma-separated linters to prepare runtimes for. - Supported values: yamllint, pymarkdown, ruff, shellcheck, actionlint, rubocop, erb-lint + Supported values: yamllint, pymarkdown, ruff, shellcheck, actionlint, rubocop, erb-lint, pre-commit required: true runs: using: composite @@ -16,20 +16,23 @@ runs: if: > contains(inputs.linters, 'yamllint') || contains(inputs.linters, 'pymarkdown') || - contains(inputs.linters, 'ruff') + contains(inputs.linters, 'ruff') || + contains(inputs.linters, 'pre-commit') with: python-version: "3.x" - run: | pkgs="" - [[ "$LINTERS" == *yamllint* ]] && pkgs="$pkgs yamllint" - [[ "$LINTERS" == *pymarkdown* ]] && pkgs="$pkgs pymarkdownlnt" - [[ "$LINTERS" == *ruff* ]] && pkgs="$pkgs ruff" + [[ "$LINTERS" == *yamllint* ]] && pkgs="$pkgs yamllint" + [[ "$LINTERS" == *pymarkdown* ]] && pkgs="$pkgs pymarkdownlnt" + [[ "$LINTERS" == *ruff* ]] && pkgs="$pkgs ruff" + [[ "$LINTERS" == *pre-commit* ]] && pkgs="$pkgs pyyaml identify" pip install $pkgs shell: bash if: > contains(inputs.linters, 'yamllint') || contains(inputs.linters, 'pymarkdown') || - contains(inputs.linters, 'ruff') + contains(inputs.linters, 'ruff') || + contains(inputs.linters, 'pre-commit') env: LINTERS: ${{ inputs.linters }} - run: | diff --git a/.github/workflows/lint-shared.yml b/.github/workflows/lint-shared.yml index 4e36092..cf7ee06 100644 --- a/.github/workflows/lint-shared.yml +++ b/.github/workflows/lint-shared.yml @@ -13,11 +13,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - uses: rubykatzen/baseline/.github/actions/check-precommit-sync@v0.7 - if: contains(inputs.linters, 'pre-commit') + - uses: rubykatzen/baseline/.github/actions/setup-runtimes@v0.7 with: linters: ${{ inputs.linters }} - - uses: rubykatzen/baseline/.github/actions/setup-runtimes@v0.7 + - uses: rubykatzen/baseline/.github/actions/check-precommit-sync@v0.7 + if: contains(inputs.linters, 'pre-commit') with: linters: ${{ inputs.linters }} - uses: rubykatzen/baseline/.github/actions/lint-yamllint@v0.7 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c7ed397..5d0cad5 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,4 +7,4 @@ jobs: lint: uses: ./.github/workflows/lint-shared.yml with: - linters: yamllint, pymarkdown, shellcheck, actionlint, rubocop, pre-commit + linters: yamllint, pymarkdown, ruff, shellcheck, actionlint, rubocop, pre-commit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 67ec17d..6fe5f7d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,6 +4,7 @@ repos: hooks: - id: yamllint - id: pymarkdown - - id: rubocop + - id: ruff - id: shellcheck - id: actionlint + - id: rubocop diff --git a/hooks/check-precommit-sync.py b/hooks/check-precommit-sync.py index 8befe53..3c8c84b 100644 --- a/hooks/check-precommit-sync.py +++ b/hooks/check-precommit-sync.py @@ -1,27 +1,96 @@ #!/usr/bin/env python3 -import yaml, sys, os +import os +import re +import subprocess +import sys +from pathlib import Path -try: - with open(".pre-commit-config.yaml") as f: - config = yaml.safe_load(f) -except FileNotFoundError: - print("::error::.pre-commit-config.yaml not found — required when pre-commit is in linters.") - sys.exit(1) +import yaml +from identify.identify import tags_from_path -hooks = next( - ({h["id"] for h in r["hooks"]} for r in config["repos"] if "rubykatzen/baseline" in r["repo"]), - None, -) -if hooks is None: - print("::error::No rubykatzen/baseline entry in .pre-commit-config.yaml.") - sys.exit(1) +BASELINE_ROOT = Path(__file__).parent.parent + + +def load_hook_defs(): + with open(BASELINE_ROOT / ".pre-commit-hooks.yaml") as f: + return yaml.safe_load(f) + + +def repo_files(): + result = subprocess.run(["git", "ls-files"], capture_output=True, text=True, check=True) + return [Path(p) for p in result.stdout.splitlines() if p] + + +def file_tags(path): + try: + return tags_from_path(str(path)) + except ValueError: + return frozenset() + + +def hook_needed(hook, files): + if "types" in hook: + required = set(hook["types"]) + return any(required.issubset(file_tags(f)) for f in files) + if "files" in hook: + pattern = re.compile(hook["files"]) + return any(pattern.search(str(f)) for f in files) + return False + + +def needed_hooks(hook_defs, files): + return {h["id"] for h in hook_defs if hook_needed(h, files)} -linters = {x.strip() for x in os.environ["LINTERS"].split(",") if x.strip() not in ("", "pre-commit")} -if linters != hooks: +def load_config(): + try: + with open(".pre-commit-config.yaml") as f: + return yaml.safe_load(f) + except FileNotFoundError: + print("::error::.pre-commit-config.yaml not found — required when pre-commit is in linters.") + sys.exit(1) + + +def baseline_configured_hooks(config): + hooks = next( + ({h["id"] for h in r["hooks"]} for r in config["repos"] if "rubykatzen/baseline" in r["repo"]), + None, + ) + if hooks is None: + print("::error::No rubykatzen/baseline entry in .pre-commit-config.yaml.") + sys.exit(1) + return hooks + + +def parse_linters(): + return {x.strip() for x in os.environ["LINTERS"].split(",") if x.strip() not in ("", "pre-commit")} + + +hook_defs = load_hook_defs() +files = repo_files() +config = load_config() +configured = baseline_configured_hooks(config) +linters = parse_linters() +needed = needed_hooks(hook_defs, files) + +ok = True + +missing = needed - configured +if missing: + print(f"::error::Hooks needed for repo files but missing from .pre-commit-config.yaml: {sorted(missing)}") + ok = False + +extra = configured - needed +if extra: + print(f"::notice::Hooks configured but no matching files found: {sorted(extra)}") + +if configured != linters: print("::error::Linter mismatch between workflow and .pre-commit-config.yaml") print(f" workflow: {sorted(linters)}") - print(f" pre-commit: {sorted(hooks)}") + print(f" pre-commit: {sorted(configured)}") + ok = False + +if not ok: sys.exit(1) -print(f"In sync: {sorted(linters)}") +print(f"In sync: {sorted(configured)}")