Files
computer-playbook/utils/install/lint
kevinveenbirkenbachandClaude Opus 5 6fee7ca136 feat(quality): parse every shipped language and run its unit suite
Four parse checks join the `lint-*` family, each using the language's own
parser rather than a reimplementation: `php -l`, `ruby -c`, `sqlfluff parse`,
and stylelint with an empty rule set (it raises CssSyntaxError whatever the
rules say, so the check stays syntax-only and grows no style opinion). Three
unit runners join `test-unit`: node:test, PHPUnit, minitest -- the first and
third ship with their runtime, so only PHPUnit adds a dependency.

Provisioning is best effort and the lint script decides whether it can run. A
machine without PHP must not fail the gate, but it must not look like success
either: the skip names how many files went unchecked. The first version of
these scripts read `git ls-files`, which inside the compose container returns
nothing (git refuses the bind-mounted repo as "dubious ownership") and reported
"No SQL files found." over 52 files -- green, and a lie. They use `find` now,
as the surrounding lint scripts already did.

sqlfluff needed two corrections before it parsed anything real. `%(name)s` is a
psycopg parameter marker, not SQL, so the placeholder templater is configured
rather than the files exempted; and the dialect follows each role's declared
engine (`meta/services.yml`), which is postgres for six roles and mysql for
svc-db-mariadb and pixelfed.

Recipes stay in scripts, not in the Makefile: the targets delegate, matching
every other target in the file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 01:31:10 +02:00
..
…
…
…
…

utils/install/lint

Per-tool installer policy for the lint toolchain. For the package's overall role see README.md. For general README conventions see documentation.md.

Purpose

Each module in this directory owns exactly one lint tool and exposes a single ensure() function. The CLI in __main__.py groups these ensure calls and applies the stamp-based skip for the default "all" invocation.

File Naming

  • A module name MUST match the lowercase snake_case of the tool it installs:
    • actionlint -> actionlint.py
    • markdownlint-cli2 -> markdownlint_cli2.py
    • ansible-lint -> ansible/lint.py
  • Aggregate installers (one helper covers a tightly coupled command pair) take the topic name without a key suffix. Example: ansible/commands.py installs both ansible-playbook and ansible-galaxy via the same system-package candidates.

Module Shape

Every per-tool module MUST expose the same surface so the dispatcher can call it uniformly:

"""<one-line docstring: what gets installed and via which channel>."""

from __future__ import annotations


def ensure() -> None:
    """Install <tool> if not present. Raises RuntimeError on failure."""
  • ensure() MUST be idempotent: return immediately when the tool is already on PATH (or importable, for Python modules).
  • ensure() MUST raise RuntimeError with a descriptive message on failure.
  • A module MAY read tool-specific environment overrides (e.g. ACTIONLINT_VERSION, RUFF_PIP_SPEC) at call time.

Import Rules

  • A per-tool module MUST NOT import another per-tool module. Shared logic flows through utils.install.primitives, utils.install.pip, utils.install.npm, utils.install.github_release, and utils.install.system_pkg only.
  • A per-tool module MAY import from utils.cache (for PROJECT_ROOT) and from stdlib.

Group Dispatch

__main__.py maps each CLI group to a fixed set of ensure() calls:

Group Modules called (in order)
action actionlint
ansible ansible_commands, ansible_collections, ansible_lint
python shfmt, ruff
shellcheck shellcheck
markdown markdownlint_cli2
makefile mbake
javascript eslint
all (default) every group above, in the order listed

The all mode honors a per-interpreter stamp at build/install-lint-<hash>.stamp (the suffix is the first 8 chars of sha256(sys.executable), so host and container track independently even though build/ is bind-mounted). Stamp dependencies are lint.sh and pyproject.toml. Group-targeted invocations always run and never touch the stamp.