Files
computer-playbook/utils/install/lint/__main__.py
kevinveenbirkenbachandClaude Opus 5.5 a9a12da75c fix(install): install the pinned shfmt release on every distro
`shfmt.ensure()` returned as soon as any `shfmt` was on PATH and tried
the distribution package before the release pinned in
roles/sys-lint/meta/services.yml. The Debian dev container therefore
formats with its packaged 3.8.0 (/usr/bin/shfmt), Fedora with its packaged
3.7.0, the CentOS one with the pinned v3.14.1 (/usr/local/bin/shfmt), and
they disagree on
scripts/tests/deploy/swarm/utils/unmount/nfs_mounts.sh: every gate in a
CentOS stack moved the `then` of the heredoc `if` below the terminator.

ensure() now follows ruff's installer: it reads `shfmt --version`, keeps
the binary only when it reports the pinned release, and otherwise installs
the release binary into /usr/local/bin. It fails when PATH still resolves
another version afterwards instead of formatting with it. The system
package path is gone.

nfs_mounts.sh is committed in the pinned release's format (byte-identical
to the v3.14.1 output from the CentOS container), so the first gate on any
distro leaves the tree clean.

The install-lint stamp now also depends on roles/sys-lint/meta/services.yml,
the pin file of actionlint, hadolint and shfmt, so a bumped pin reaches
environments whose stamp predates it; until now only pyproject.toml and
scripts/install/lint.sh invalidated it.

Fixes #1155

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 19:16:22 +02:00

253 lines
5.9 KiB
Python

"""CLI entry point for ``python -m utils.install.lint``.
Mirrors the prior ``scripts/install/lint.sh`` interface: no-arg / ``all``
runs every group + manages the stamp file; explicit groups bypass the
stamp; ``--force`` (as first arg) drops the stamp and mmdc's browser cache
and reinstalls.
"""
from __future__ import annotations
import contextlib
import hashlib
import os
import shutil
import sys
from pathlib import Path
from utils.cache import PROJECT_ROOT
from utils.install.lint import (
actionlint,
eslint,
hadolint,
markdownlint_cli2,
mbake,
mermaid_cli,
packages,
php,
playwright,
ruby,
ruff,
shellcheck,
shfmt,
sqlfluff,
stylelint,
)
from utils.install.lint.ansible import collections as ansible_collections
from utils.install.lint.ansible import commands as ansible_commands
from utils.install.lint.ansible import lint as ansible_lint
from utils.install.primitives import warn
from utils.roles.mapping import ROLE_FILE_META_SERVICES
_STAMP_KEY = hashlib.sha256(sys.executable.encode("utf-8")).hexdigest()[:8]
_STAMP = f"build/install-lint-{_STAMP_KEY}.stamp"
_PINS = f"roles/sys-lint/{ROLE_FILE_META_SERVICES}"
_STAMP_DEPS = (
"scripts/install/lint.sh",
"pyproject.toml",
_PINS,
)
_STAMP_TOOLS = (
"actionlint",
"ansible-lint",
"ansible-playbook",
"eslint",
"hadolint",
"markdownlint-cli2",
"mbake",
"php",
"ruby",
"ruff",
"shellcheck",
"shfmt",
)
def _install_action_tools() -> None:
actionlint.ensure()
def _install_ansible_tools() -> None:
ansible_commands.ensure()
ansible_collections.ensure()
ansible_lint.ensure()
def _install_python_tools() -> None:
shfmt.ensure()
ruff.ensure()
def _install_php_tools() -> None:
php.ensure()
def _install_ruby_tools() -> None:
ruby.ensure()
def _install_shellcheck_tools() -> None:
shellcheck.ensure()
def _install_dockerfile_tools() -> None:
hadolint.ensure()
def _install_markdown_tools() -> None:
markdownlint_cli2.ensure()
def _install_mermaid_tools() -> None:
mermaid_cli.ensure()
def _install_makefile_tools() -> None:
mbake.ensure()
def _install_javascript_tools() -> None:
eslint.ensure()
def _install_playwright_tools() -> None:
playwright.ensure()
def _install_packages_tools() -> None:
packages.ensure()
def _install_php_tools() -> None:
php.ensure()
def _install_ruby_tools() -> None:
ruby.ensure()
def _install_sql_tools() -> None:
sqlfluff.ensure()
def _install_css_tools() -> None:
stylelint.ensure()
_GROUP_FN_NAMES = {
"action": "_install_action_tools",
"ansible": "_install_ansible_tools",
"python": "_install_python_tools",
"php": "_install_php_tools",
"ruby": "_install_ruby_tools",
"shellcheck": "_install_shellcheck_tools",
"dockerfile": "_install_dockerfile_tools",
"markdown": "_install_markdown_tools",
"mermaid": "_install_mermaid_tools",
"makefile": "_install_makefile_tools",
"javascript": "_install_javascript_tools",
"playwright": "_install_playwright_tools",
"packages": "_install_packages_tools",
"sql": "_install_sql_tools",
"css": "_install_css_tools",
}
def _install_all() -> None:
_install_action_tools()
_install_ansible_tools()
_install_python_tools()
_install_php_tools()
_install_ruby_tools()
_install_shellcheck_tools()
_install_dockerfile_tools()
_install_markdown_tools()
_install_mermaid_tools()
_install_makefile_tools()
_install_javascript_tools()
_install_playwright_tools()
def _stamp_is_fresh(repo_root: Path) -> bool:
stamp_path = repo_root / _STAMP
if not stamp_path.is_file():
return False
stamp_mtime = stamp_path.stat().st_mtime
for dep in _STAMP_DEPS:
dep_path = repo_root / dep
if not dep_path.is_file():
warn(f"[install-lint] missing dependency: {dep}")
return False
if dep_path.stat().st_mtime > stamp_mtime:
return False
return (
all(shutil.which(tool) is not None for tool in _STAMP_TOOLS)
and mermaid_cli.browser_installed()
)
def _touch_stamp(repo_root: Path) -> None:
stamp_path = repo_root / _STAMP
stamp_path.parent.mkdir(parents=True, exist_ok=True)
with contextlib.suppress(PermissionError):
stamp_path.parent.chmod(0o777)
stamp_path.touch()
def _dispatch(group: str) -> None:
if group == "all":
_install_all()
return
fn_name = _GROUP_FN_NAMES.get(group)
if fn_name is None:
raise RuntimeError(
"Usage: python -m utils.install.lint "
"[all|action|ansible|python|shellcheck|dockerfile|markdown|mermaid|makefile|"
"javascript|playwright|packages|php|ruby|sql|css]..."
)
globals()[fn_name]()
def main(argv: list[str] | None = None) -> int:
args = list(sys.argv[1:] if argv is None else argv)
repo_root = Path(PROJECT_ROOT)
force = False
if args and args[0] == "--force":
force = True
stamp_path = repo_root / _STAMP
if stamp_path.exists():
stamp_path.unlink()
shutil.rmtree(mermaid_cli.browser_cache(), ignore_errors=True)
args = args[1:]
if not args:
args = ["all"]
is_all_mode = len(args) == 1 and args[0] == "all"
if is_all_mode and not force and _stamp_is_fresh(repo_root):
return 0
# chdir for installers that resolve relative paths (eslint's npm ci
previous_cwd = Path.cwd() if Path.cwd().exists() else None
os.chdir(repo_root)
try:
for group in args:
_dispatch(group)
except RuntimeError as exc:
warn(str(exc))
return 1
finally:
if previous_cwd is not None and previous_cwd.exists():
os.chdir(previous_cwd)
if is_all_mode:
_touch_stamp(repo_root)
return 0
if __name__ == "__main__":
raise SystemExit(main())