diff --git a/.github/workflows/hooks.yml b/.github/workflows/hooks.yml new file mode 100644 index 0000000..f09ba17 --- /dev/null +++ b/.github/workflows/hooks.yml @@ -0,0 +1,34 @@ +# Runs .pre-commit-config.yaml, so the hooks that ci.yml does not run itself - +# bandit, shellcheck, markdownlint, actionlint, betterleaks, uv-lock - gate a +# pull request instead of depending on somebody having installed them. Two of +# them sat red on main unnoticed before this existed. +# +# Its own workflow rather than a job in ci.yml, because ci.yml skips prose-only +# changes and markdownlint is the hook that most needs to see those. +name: Hooks + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + prek: + name: pre-commit hooks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + + - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 + with: + enable-cache: true + # uv itself, the same one ci.yml pins. + version: "0.11.28" + + # prek pinned like every other tool here; each hook's own version is + # pinned by its rev in .pre-commit-config.yaml. + - name: Run every hook over every file + run: uvx prek@0.5.4 run --all-files --show-diff-on-failure diff --git a/collector/README.md b/collector/README.md index 04c4a1c..e4b6806 100644 --- a/collector/README.md +++ b/collector/README.md @@ -35,6 +35,7 @@ for f in image/*.sh; do bash -n "$f"; done # The entry points parse GITHUB_TOKEN=dummy docker compose config --quiet # The compose file parses docker build -t jq-monitoring:ci . # The image builds uvx --with mkdocs-material==9.6.14 mkdocs build --strict # The book, broken links fail it +uvx prek@0.5.4 run --all-files # Every hook in .pre-commit-config.yaml ``` CI then starts that image against a throwaway `repos.yml` and checks it serves diff --git a/collector/jq_collector/localgit.py b/collector/jq_collector/localgit.py index 41bfb86..2eab3ca 100644 --- a/collector/jq_collector/localgit.py +++ b/collector/jq_collector/localgit.py @@ -31,7 +31,7 @@ import logging import os -import subprocess +import subprocess # nosec B404 - only ever runs git, argv-only; see _git import time from typing import TypedDict @@ -46,8 +46,11 @@ def _git(path: str, *args: str) -> str | None: """Run a read-only git command, returning None if it fails.""" + # A fixed argv with no shell, so path and args reach git as arguments and + # never as a command line; `git` is resolved on PATH on purpose, since the + # image and a laptop keep it in different places. try: - result = subprocess.run( + result = subprocess.run( # nosec B603 B607 ["git", "--no-optional-locks", "-C", path, *args], capture_output=True, text=True, diff --git a/collector/jq_collector/origin.py b/collector/jq_collector/origin.py index 983067c..7e1bf96 100644 --- a/collector/jq_collector/origin.py +++ b/collector/jq_collector/origin.py @@ -22,7 +22,7 @@ from __future__ import annotations import logging -import subprocess +import subprocess # nosec B404 - only ever runs git, argv-only; see read from dataclasses import dataclass log = logging.getLogger(__name__) @@ -124,8 +124,9 @@ def read(path: str) -> Origin | None: clones are somebody's live working copies and a monitor has no business taking a lock in one. """ + # Fixed argv, no shell, git from PATH - the same reasoning as localgit._git. try: - result = subprocess.run( + result = subprocess.run( # nosec B603 B607 ["git", "--no-optional-locks", "-C", path, "remote", "get-url", "origin"], capture_output=True, text=True, diff --git a/image/entrypoint.sh b/image/entrypoint.sh index 3743985..596f111 100755 --- a/image/entrypoint.sh +++ b/image/entrypoint.sh @@ -41,6 +41,8 @@ mkdir -p /data/prometheus /data/grafana # -- shut down together ------------------------------------------------------ pids=() +# Invoked only through the trap below, which shellcheck cannot follow. +# shellcheck disable=SC2329 stop() { trap - TERM INT # Signal the group rather than each pid: promtool-style children and