DEVX-111: feat: add check_api_identity_checks, setup_ssh_key, and api utils
Post-merge / detect-type (push) Successful in 9s
Post-merge / validate-commit-msg (push) Successful in 9s
Build Images / detect-type (push) Successful in 42s
Post-merge / vikunja (push) Successful in 17s
Post-merge / release (push) Successful in 52s
Post-merge / configure-repo (push) Successful in 23s
Post-merge / badges (push) Successful in 55s
Post-merge / sync-wiki (push) Successful in 58s
Post-merge / publish (push) Successful in 21s
Build Images / build-and-push (push) Successful in 3m15s
Build Images / cleanup (push) Successful in 3m38s

This commit was merged in pull request #170.
This commit is contained in:
2026-07-05 14:11:58 +00:00
parent 333641f862
commit 2c0118111d
9 changed files with 737 additions and 0 deletions
+9
View File
@@ -110,6 +110,7 @@ devx-ensure-venv:
.PHONY: devx-lint-ruff devx-lint-format devx-typecheck devx-lint-bandit devx-lint-deps devx-lint
.PHONY: devx-clean devx-pre-push
.PHONY: devx-check-mutable-globals devx-check-dep-docs devx-check-test-coverage devx-check-docs devx-check-test-speed
.PHONY: devx-check-api-identity-checks devx-setup-ssh-key
.PHONY: devx-test-unit devx-pytest-cov
.PHONY: devx-setup-image devx-lint-dockerfiles
@@ -327,6 +328,14 @@ devx-check-docs:
devx-check-test-speed:
@$(DEVX_PYTHON) -m devx.tools.check_test_speed
# Scan integration tests for unsafe is True/is False identity checks
devx-check-api-identity-checks:
@$(DEVX_PYTHON) -m devx.tools.check_api_identity_checks
# Set up SSH private key from SSH_PRIVATE_KEY env var
devx-setup-ssh-key:
@$(DEVX_PYTHON) -m devx.tools.setup_ssh_key
# ── Pre-push validation ───────────────────────────────────────────────────────
# Run lint + tests before push (projects can override with project-specific targets)
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""Scan integration tests for unsafe ``is True``/``is False`` identity checks.
Many APIs (e.g. Mattermost) return boolean values as strings (``"true"``,
``"false"``) rather than native JSON booleans. Using ``is True`` or
``is not False`` on such responses silently fails because ``"true" is True``
evaluates to ``False`` in Python.
This tool scans ``tests/integration/test_*.py`` files for identity checks
on API response values and reports them as errors.
Configuration (``[tool.devx.check_api_identity_checks]`` in pyproject.toml):
``scan_dirs`` — list of directories to scan (default: ``["tests/integration"]``)
``skip_patterns`` — list of filename patterns to skip (default: ``["test_*_helpers.py"]``)
``noqa_marker`` — comment to suppress individual lines (default: ``# noqa``)
Usage::
python3 -m devx.tools.check_api_identity_checks
python3 -m devx.tools.check_api_identity_checks --scan-dir tests/integration
"""
from __future__ import annotations
import re
from pathlib import Path
import click
from devx.config import _load_pyproject_devx
from devx.i18n import _
DEFAULT_SCAN_DIRS = ["tests/integration"]
DEFAULT_SKIP_PATTERNS = ["test_*_helpers.py"]
DEFAULT_NOQA_MARKER = "# noqa"
# Matches: x is True, x is False, x is not True, x is not False
_IDENTITY_CHECK_RE = re.compile(r"\bis\s+(not\s+)?(True|False)\b")
def _load_config() -> tuple[list[str], list[str], str]:
"""Load configuration from pyproject.toml [tool.devx.check_api_identity_checks]."""
devx_cfg = _load_pyproject_devx()
cfg_raw = devx_cfg.get("check_api_identity_checks", {})
if not isinstance(cfg_raw, dict):
return DEFAULT_SCAN_DIRS, DEFAULT_SKIP_PATTERNS, DEFAULT_NOQA_MARKER
cfg: dict[str, object] = cfg_raw # type: ignore[assignment]
scan_dirs_raw = cfg.get("scan_dirs", DEFAULT_SCAN_DIRS)
scan_dirs: list[str] = [str(d) for d in scan_dirs_raw] if isinstance(scan_dirs_raw, list) else DEFAULT_SCAN_DIRS
skip_raw = cfg.get("skip_patterns", DEFAULT_SKIP_PATTERNS)
skip_patterns: list[str] = [str(p) for p in skip_raw] if isinstance(skip_raw, list) else DEFAULT_SKIP_PATTERNS
noqa_marker = str(cfg.get("noqa_marker", DEFAULT_NOQA_MARKER))
return scan_dirs, skip_patterns, noqa_marker
def _matches_skip_pattern(path: Path, skip_patterns: list[str]) -> bool:
"""Check if a file path matches any skip pattern."""
name = path.name
return any(Path(name).match(pattern) for pattern in skip_patterns)
def find_identity_checks(
file_path: Path,
repo_root: Path,
noqa_marker: str,
) -> list[str]:
"""Return a list of issue strings for unsafe identity checks in *file_path*."""
issues: list[str] = []
try:
source = file_path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
return issues
rel = str(file_path.relative_to(repo_root))
for lineno, line in enumerate(source.splitlines(), 1):
if noqa_marker in line:
continue
match = _IDENTITY_CHECK_RE.search(line)
if match:
issues.append(
f"{rel}:{lineno}: unsafe identity check '{match.group()}' "
f"— APIs may return string 'true'/'false'. "
f"Use string comparison or _is_truthy()/_is_falsy() helpers."
)
return issues
@click.command()
@click.option(
"--scan-dir",
multiple=True,
help=_("Directory to scan (default: tests/integration). Can be repeated."),
)
def cli(scan_dir: tuple[str, ...]) -> None:
"""Scan integration tests for unsafe ``is True``/``is False`` identity checks."""
repo_root = Path.cwd()
config_scan_dirs, skip_patterns, noqa_marker = _load_config()
scan_dirs = list(scan_dir) if scan_dir else config_scan_dirs
all_issues: list[str] = []
for scan_dir_name in scan_dirs:
scan_path = repo_root / scan_dir_name
if not scan_path.exists():
continue
for py_file in scan_path.rglob("test_*.py"):
if _matches_skip_pattern(py_file, skip_patterns):
continue
all_issues.extend(find_identity_checks(py_file, repo_root, noqa_marker))
if all_issues:
click.echo(
_("Found {count} unsafe identity check(s) in integration tests.", count=len(all_issues)),
err=True,
)
for issue in all_issues:
click.echo(f" {issue}", err=True)
raise click.ClickException(
_(
"Use string comparison or _is_truthy()/_is_falsy() helpers instead. "
"Add '{marker}' to suppress individual lines.",
marker=noqa_marker,
)
)
click.echo(_("[check-api-identity-checks] Passed: no unsafe identity checks found"))
if __name__ == "__main__": # pragma: no cover
cli() # pragma: no cover
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Set up SSH private key for CI jobs that need SSH access to remote hosts.
Writes the ``SSH_PRIVATE_KEY`` env var to ``~/.ssh/id_rsa``, starts
``ssh-agent``, and adds the key. Replaces the repeated inline shell
pattern in CI workflow files.
Usage::
python3 -m devx.tools.setup_ssh_key
Reads ``SSH_PRIVATE_KEY`` from the environment. Exits 0 on success,
1 on missing key.
"""
from __future__ import annotations
import os
import subprocess # nosec B404
import sys
from pathlib import Path
import click
from devx.i18n import _
def setup_ssh_key(private_key: str | None = None) -> bool:
"""Set up SSH private key and start ssh-agent.
Args:
private_key: The SSH private key content. If None, reads from
``SSH_PRIVATE_KEY`` environment variable.
Returns:
True if setup succeeded, False if key is missing.
"""
key = private_key or os.environ.get("SSH_PRIVATE_KEY", "")
if not key:
click.echo(_("SSH_PRIVATE_KEY not set — skipping SSH key setup"), err=True)
return False
ssh_dir = Path.home() / ".ssh"
ssh_dir.mkdir(parents=True, exist_ok=True)
key_path = ssh_dir / "id_rsa"
key_path.write_text(f"{key}\n", encoding="utf-8")
key_path.chmod(0o600)
# Start ssh-agent and add the key
agent_result = subprocess.run( # nosec B603, B607
["ssh-agent", "-s"],
capture_output=True,
text=True,
check=False,
)
if agent_result.returncode != 0:
click.echo(_("Failed to start ssh-agent: {error}", error=agent_result.stderr), err=True)
return False
# Parse ssh-agent output to set env vars
for raw_line in agent_result.stdout.splitlines():
stripped = raw_line.strip()
if "=" in stripped and ";" in stripped:
var, val = stripped.split("=", 1)
val = val.rstrip(";")
os.environ[var] = val
# Add the key (non-fatal if it fails — key may already be loaded)
subprocess.run( # nosec B603, B607
["ssh-add", str(key_path)],
capture_output=True,
text=True,
check=False,
)
return True
@click.command()
def cli() -> None:
"""Set up SSH private key from SSH_PRIVATE_KEY env var."""
if setup_ssh_key():
click.echo(_("SSH key set up successfully"))
sys.exit(0)
click.echo(_("SSH key setup skipped (no key provided)"), err=True)
sys.exit(1)
if __name__ == "__main__": # pragma: no cover
cli() # pragma: no cover
+64
View File
@@ -3278,5 +3278,69 @@
"pl": "[check-deps] Wszystkie podstawowe narzędzia są dostępne.",
"ru": "[check-deps] Все основные инструменты доступны.",
"zh": "[check-deps] 所有核心工具均已就绪。"
},
"SSH_PRIVATE_KEY not set — skipping SSH key setup": {
"en": "SSH_PRIVATE_KEY not set — skipping SSH key setup",
"bg": "SSH_PRIVATE_KEY не е зададен — пропускане на SSH ключ настройката",
"de": "SSH_PRIVATE_KEY nicht gesetzt — SSH-Schlüssel-Setup übersprungen",
"pl": "SSH_PRIVATE_KEY nie ustawione — pomijanie konfiguracji klucza SSH",
"ru": "SSH_PRIVATE_KEY не задан — пропуск настройки SSH-ключа",
"zh": "SSH_PRIVATE_KEY 未设置 — 跳过 SSH 密钥设置"
},
"Failed to start ssh-agent: {error}": {
"en": "Failed to start ssh-agent: {error}",
"bg": "Неуспешно стартиране на ssh-agent: {error}",
"de": "Starten von ssh-agent fehlgeschlagen: {error}",
"pl": "Nie udało się uruchomić ssh-agent: {error}",
"ru": "Не удалось запустить ssh-agent: {error}",
"zh": "启动 ssh-agent 失败: {error}"
},
"SSH key set up successfully": {
"en": "SSH key set up successfully",
"bg": "SSH ключът е настроен успешно",
"de": "SSH-Schlüssel erfolgreich eingerichtet",
"pl": "Klucz SSH skonfigurowany pomyślnie",
"ru": "SSH-ключ успешно настроен",
"zh": "SSH 密钥设置成功"
},
"SSH key setup skipped (no key provided)": {
"en": "SSH key setup skipped (no key provided)",
"bg": "Настройката на SSH ключ е пропусната (не е предоставен ключ)",
"de": "SSH-Schlüssel-Setup übersprungen (kein Schlüssel bereitgestellt)",
"pl": "Pominięto konfigurację klucza SSH (brak klucza)",
"ru": "Настройка SSH-ключа пропущена (ключ не предоставлен)",
"zh": "SSH 密钥设置已跳过(未提供密钥)"
},
"Found {count} unsafe identity check(s) in integration tests.": {
"en": "Found {count} unsafe identity check(s) in integration tests.",
"bg": "Намерени са {count} небрежни проверки за идентичност в интеграционните тестове.",
"de": "{count} unsichere Identitätsprüfung(en) in Integrationstests gefunden.",
"pl": "Znaleziono {count} niebezpiecznych sprawdzeń tożsamości w testach integracyjnych.",
"ru": "Найдено {count} небезопасных проверок идентичности в интеграционных тестах.",
"zh": "在集成测试中发现 {count} 个不安全的身份检查。"
},
"Use string comparison or _is_truthy()/_is_falsy() helpers instead. Add '{marker}' to suppress individual lines.": {
"en": "Use string comparison or _is_truthy()/_is_falsy() helpers instead. Add '{marker}' to suppress individual lines.",
"bg": "Използвайте сравнение на низове или _is_truthy()/_is_falsy() помощници. Добавете '{marker}' за потискане на отделни редове.",
"de": "Verwenden Sie String-Vergleich oder _is_truthy()/_is_falsy() Hilfsfunktionen. Fügen Sie '{marker}' hinzu, um einzelne Zeilen zu unterdrücken.",
"pl": "Użyj porównania ciągów lub pomocników _is_truthy()/_is_falsy(). Dodaj '{marker}', aby pominąć pojedyncze linie.",
"ru": "Используйте строковое сравнение или помощники _is_truthy()/_is_falsy(). Добавьте '{marker}' для подавления отдельных строк.",
"zh": "使用字符串比较或 _is_truthy()/_is_falsy() 辅助函数。添加 '{marker}' 以抑制个别行。"
},
"[check-api-identity-checks] Passed: no unsafe identity checks found": {
"en": "[check-api-identity-checks] Passed: no unsafe identity checks found",
"bg": "[check-api-identity-checks] Мина: не са намерени небрежни проверки за идентичност",
"de": "[check-api-identity-checks] Bestanden: keine unsicheren Identitätsprüfungen gefunden",
"pl": "[check-api-identity-checks] Passed: nie znaleziono niebezpiecznych sprawdzeń tożsamości",
"ru": "[check-api-identity-checks] Пройдено: небезопасных проверок идентичности не найдено",
"zh": "[check-api-identity-checks] 通过:未发现不安全的身份检查"
},
"Directory to scan (default: tests/integration). Can be repeated.": {
"en": "Directory to scan (default: tests/integration). Can be repeated.",
"bg": "Директория за сканиране (по подразбиране: tests/integration). Може да се повтаря.",
"de": "Zu scannendes Verzeichnis (Standard: tests/integration). Kann wiederholt werden.",
"pl": "Katalog do skanowania (domyślnie: tests/integration). Można powtarzać.",
"ru": "Директория для сканирования (по умолчанию: tests/integration). Можно повторять.",
"zh": "要扫描的目录(默认:tests/integration)。可重复。"
}
}
+3
View File
@@ -0,0 +1,3 @@
"""Shared utility functions for devx and consumer projects."""
from __future__ import annotations
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Utilities for handling API response values.
Many APIs return boolean values as strings (``"true"``, ``"false"``)
rather than native JSON booleans. The Mattermost ``/api/v4/config/client``
endpoint is a notable example. These helpers handle both string and
boolean responses safely.
Usage::
from devx.utils.api import is_truthy, is_falsy
if not is_truthy(config.get("EnableOpenServer")):
raise ValueError("EnableOpenServer not enabled")
"""
from __future__ import annotations
def is_truthy(value: str | bool | None) -> bool:
"""Check if an API config value is truthy.
The API may return strings (``"true"``/``"false"``) or native
booleans. This helper handles both.
Args:
value: The value to check (string, bool, or None).
Returns:
True if the value represents a truthy boolean.
"""
if isinstance(value, bool):
return value
return str(value).lower() == "true"
def is_falsy(value: str | bool | None) -> bool:
"""Check if an API config value is falsy.
The API may return strings (``"true"``/``"false"``) or native
booleans. This helper handles both.
Args:
value: The value to check (string, bool, or None).
Returns:
True if the value represents a falsy boolean.
"""
if isinstance(value, bool):
return not value
return str(value).lower() == "false"