From a7b06358301ba0946d92f3f0160152ad143eb060 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 26 Aug 2026 22:09:59 +0300 Subject: [PATCH] Add lint configuration and Gitea Actions CI Configure yamllint and ansible-lint, plus a workflow running yamllint, ansible-lint and ansible-playbook --syntax-check over every playbook. ansible-lint uses the moderate profile: on the current code it reports exactly the same violations as basic, so it costs nothing today while holding a higher bar for new code. skip_list is empty; noisy legacy rules go to warn_list with a comment on why and when to restore them. Correctness and safety rules stay fatal. Two constraints are encoded in the workflow: syntax-check must run from ansible/ because roles_path is relative, and ansible-lint needs absolute ANSIBLE_ROLES_PATH/ANSIBLE_COLLECTIONS_PATH when run from the root. The runner is not registered yet; registration notes are in the workflow. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GTocXkGUUazHdKKd3r9k71 --- .ansible-lint | 85 ++++++++++++++++++++++++ .gitea/workflows/lint.yml | 134 ++++++++++++++++++++++++++++++++++++++ .yamllint | 69 ++++++++++++++++++++ 3 files changed, 288 insertions(+) create mode 100644 .ansible-lint create mode 100644 .gitea/workflows/lint.yml create mode 100644 .yamllint diff --git a/.ansible-lint b/.ansible-lint new file mode 100644 index 0000000..e837362 --- /dev/null +++ b/.ansible-lint @@ -0,0 +1,85 @@ +--- +# ansible-lint для HomeLab infras. +# +# Profile: moderate. +# - `production` / `safety` дали бы сотни нарушений на текущем коде (fqcn, +# jinja[spacing], no-handler, key-order, галактические метаданные ролей) — +# линтер стал бы шумом, который все игнорируют. +# - Фактическая проверка: на этом репозитории `basic` и `moderate` дают +# ровно один и тот же набор нарушений (все сработавшие правила помечены +# profile:basic). То есть `moderate` сегодня ничего не стоит, но держит +# планку выше для нового кода. Отсюда выбор. +# - Правила корректности и безопасности (syntax-check, risky-file-permissions, +# risky-shell-pipe, risky-octal, no-changed-when, no-free-form, deprecated-*, +# jinja[invalid], sanity) НЕ отключены — они остаются fatal. + +profile: moderate + +exclude_paths: + - archive/ # исторические NixOS/docker-compose/ansible конфиги, read-only + - ansible/.venv/ # gitignored, локальное venv + - ansible/collections/ # gitignored, установленные galaxy-коллекции + - ansible/generated/ # gitignored артефакты + - .opencode/ # конфиги агентов + node_modules + - tools/ # tools/grimmory-mcp — JS, не ansible + - node_modules/ + - .direnv/ + - .git/ + # Зашифрованный ansible-vault: линтер не может его расшифровать и шумит + # предупреждениями о Decryption failed. + - ansible/inventory/host_vars/gyro/vault.yml + +# Явно указываем, что считать плейбуками/тасками — иначе ansible-lint +# принимает inventory/*.yml и roles/*/files/*.yml за плейбуки. +kinds: + - playbook: 'ansible/playbooks/*.yml' + - tasks: 'ansible/tasks/*.yml' + +# ------------------------------------------------------------------ +# warn_list — правила, которые СЕЙЧАС массово срабатывают на легаси-коде. +# Они видны в выводе как warning, но не роняют CI. Это осознанный +# «нулевой baseline»: CI зелёный, долг виден. +# По каждому пункту — почему и стоит ли возвращать в fatal. +# ------------------------------------------------------------------ +warn_list: + # 40 срабатываний. Роли используют осмысленные кросс-ролевые префиксы + # (openvpn_*, monitoring_*, emergency_*), а не имя роли. Переименование + # затронет inventory, host_vars и все плейбуки разом. + # ВЕРНУТЬ В FATAL: после разового переименования переменных ролей. + - var-naming[no-role-prefix] + + # 19 срабатываний. Имена задач в нижнем регистре ("restart gitea lxc"). + # Чисто косметика, на поведение не влияет. + # ВЕРНУТЬ В FATAL: после массового причёсывания имён (дешёвый разовый PR). + - name[casing] + + # 18 срабатываний. Часть файлов без "---" в начале. + # ВЕРНУТЬ В FATAL: тривиально чинится, но затрагивает 18 файлов. + - yaml[document-start] + + # 6 срабатываний. Все — верификационные команды с changed_when: false + # (curl для проверки HTTPS-эндпоинта, systemctl is-active, git config + # внутри чужого чекаута, docker exec caddy validate). Замена на + # uri/systemd/git-модули здесь не улучшает код, а иногда невозможна + # (команда исполняется внутри pct/docker exec). + # ВЕРНУТЬ В FATAL: вряд ли — правило по сути false-positive для этого стиля. + - command-instead-of-module + + # 5 срабатываний: безымянные `- import_playbook:` записи в *-update.yml. + # 1 срабатывание: безымянный `- block:` в ru-vps-mihomo-harden.yml. + # Влияет только на читаемость вывода ansible-playbook. + # ВЕРНУТЬ В FATAL: да, после того как проставят name (мелкий PR). + - name[play] + - name[missing] + + # 1 срабатывание: emergency_access/tasks/client.yml:46 — become_user без + # become. Это, вероятно, НАСТОЯЩИЙ баг (ключ создаётся не тем пользователем), + # но чинить его — задача не линтера. Держим в warn_list, чтобы CI не был + # красным с первого дня; ВЕРНУТЬ В FATAL сразу после фикса. + - partial-become + +# skip_list пуст намеренно: ничего не отключаем полностью, всё либо fatal, +# либо видимый warning. +skip_list: [] + +use_default_rules: true diff --git a/.gitea/workflows/lint.yml b/.gitea/workflows/lint.yml new file mode 100644 index 0000000..597721f --- /dev/null +++ b/.gitea/workflows/lint.yml @@ -0,0 +1,134 @@ +--- +# Статические проверки Ansible-кода HomeLab infras. +# +# ГДЕ ЭТО ДОЛЖНО ВЫПОЛНЯТЬСЯ +# -------------------------- +# Gitea живёт на LXC `gitea` (192.168.1.25). Gitea Actions по умолчанию +# ВЫКЛЮЧЕНЫ и не имеют ни одного раннера — этот workflow не запустится, +# пока раннер не зарегистрирован ОТДЕЛЬНО, вручную: +# +# 1. Включить Actions в Gitea: +# app.ini -> [actions] ENABLED = true +# и в настройках репозитория: Settings -> Actions -> Enable. +# +# 2. Поднять act_runner. Подходящий хост — LXC `docker-test` +# (192.168.1.29): там уже есть Docker, а сборка контейнеров раннера +# не мешает проду. Ставить раннер на сам LXC `gitea` не стоит — +# CI-нагрузка не должна валить git-сервис. +# +# 3. Зарегистрировать раннер (на docker-test): +# act_runner register --no-interactive \ +# --instance http://192.168.1.25:3000 \ +# --token Actions -> Runners> \ +# --name docker-test-runner \ +# --labels ubuntu-latest:docker://catthehacker/ubuntu:act-latest +# +# Метка `ubuntu-latest` обязательна — именно её просит `runs-on` ниже. +# +# 4. Раннеру нужен исходящий интернет (PyPI + Ansible Galaxy). +# На docker-test трафик может идти через mihomo/OpenVPN — проверить, +# что pip и galaxy резолвятся, иначе шаг установки упадёт. +# +# Регистрация раннера НЕ автоматизирована этим репозиторием: она требует +# одноразового токена из веб-интерфейса Gitea. +# +# Локально те же проверки воспроизводятся через nix: +# nix develop -c yamllint . +# nix develop -c ansible-lint +# nix develop -c sh -c 'cd ansible && for f in playbooks/*.yml; do ansible-playbook --syntax-check "$f"; done' + +name: lint + +on: + push: + pull_request: + +jobs: + lint: + name: yamllint + ansible-lint + syntax-check + runs-on: ubuntu-latest + + env: + # ansible.cfg лежит в ansible/ и использует ОТНОСИТЕЛЬНЫЕ пути + # (roles_path = roles). Из корня репозитория он не работает, поэтому + # пути задаются абсолютно через окружение. Без этого ansible-lint + # выдаёт 12 ложных syntax-check[specific] «role not found». + ANSIBLE_ROLES_PATH: ${{ github.workspace }}/ansible/roles + ANSIBLE_COLLECTIONS_PATH: ${{ github.workspace }}/ansible/collections + ANSIBLE_INVENTORY: ${{ github.workspace }}/ansible/inventory/hosts.yml + # Ansible шумит депрекейшенами ядра — в CI они не наши. + ANSIBLE_DEPRECATION_WARNINGS: "false" + PIP_DISABLE_PIP_VERSION_CHECK: "1" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + # Образ catthehacker/ubuntu:act-latest уже несёт python3, но не всегда + # python3-venv. Ставим явно, чтобы шаг не был хрупким. + - name: Ensure python3 + venv + run: | + set -eux + if ! command -v python3 >/dev/null 2>&1 || ! python3 -m venv --help >/dev/null 2>&1; then + apt-get update + apt-get install -y --no-install-recommends python3 python3-venv python3-pip + fi + python3 --version + + - name: Install ansible-core, ansible-lint, yamllint + run: | + set -eux + python3 -m venv /tmp/lintenv + . /tmp/lintenv/bin/activate + python3 -m pip install --upgrade pip + # ansible-core/proxmoxer/requests берём из репозитория, чтобы CI и + # локальное окружение не разъезжались. + python3 -m pip install -r ansible/requirements.txt + # Линтеры пинуем: обновление ansible-lint регулярно добавляет новые + # правила и красит CI без единого коммита в инфраструктуру. + python3 -m pip install 'ansible-lint==25.8.2' 'yamllint==1.37.1' + echo "/tmp/lintenv/bin" >> "$GITHUB_PATH" + + - name: Install Galaxy collections + run: | + set -eux + ansible-galaxy collection install \ + -r ansible/requirements.yml \ + -p ansible/collections + + - name: Versions + run: | + set -eux + ansible --version | head -n1 + ansible-lint --version + yamllint --version + + # Конфиг в /.yamllint. Падает только на ошибках (табы, дубли ключей, + # битый YAML); стилевые замечания идут как warning и CI не роняют. + - name: yamllint + run: yamllint -f standard . + + # Конфиг в /.ansible-lint, profile: moderate. + - name: ansible-lint + run: ansible-lint + + # syntax-check запускается ИЗ ansible/, иначе ansible.cfg с + # относительными roles_path не подхватывается и 13 плейбуков падают + # с «role not found». Переменные Proxmox (PROXMOX_*) для syntax-check + # НЕ нужны: `lookup('env', ...)` на этапе парсинга не вычисляется, + # проверено — все 37 плейбуков проходят с пустым окружением. + - name: ansible-playbook --syntax-check (все плейбуки) + working-directory: ansible + run: | + set -u + rc=0 + for f in playbooks/*.yml; do + if ansible-playbook --syntax-check "$f" >/tmp/sc.log 2>&1; then + echo "ok $f" + else + rc=1 + echo "FAIL $f" + sed 's/^/ /' /tmp/sc.log + fi + done + exit "$rc" diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..ecacd6d --- /dev/null +++ b/.yamllint @@ -0,0 +1,69 @@ +--- +# yamllint для HomeLab infras. +# Цель: ловить реальные поломки YAML (табы, дубли ключей, битые отступы, +# незакрытые кавычки), а не навязывать стиль. Всё, что даёт шум на живом +# Ansible-коде, ослаблено осознанно — см. комментарии. + +extends: default + +# gitignore-style пути, которые линтить не нужно. +ignore: | + /ansible/.venv/ + /ansible/collections/ + /ansible/generated/ + /archive/ + /.opencode/ + /tools/ + node_modules/ + /ansible/inventory/host_vars/*/vault.yml + /.direnv/ + /result + /result-* + +rules: + # Длинные строки — норма: pct/docker/curl-команды в one-line shell, + # длинные URL, ZeroTier/OpenVPN-конфиги. Перенос сделал бы их менее читаемыми. + line-length: disable + + # Ansible исторически допускает yes/no наравне с true/false, и часть репо + # написана так. Не ошибка — не мешаем. + truthy: + allowed-values: ['true', 'false', 'yes', 'no'] + check-keys: false + + # Комментарии вида "#comment" и inline-комментарии на одном пробеле + # встречаются часто и ни на что не влияют. + comments: + require-starting-space: true + min-spaces-from-content: 1 + comments-indentation: disable + + # Ansible-стиль: списки под ключом с отступом — вопрос вкуса, обе формы валидны. + indentation: + spaces: 2 + indent-sequences: consistent + check-multi-line-strings: false + + # Пустая строка в конце файла обязательна (реальные diff-артефакты), + # но лишние пробелы в конце строк — только предупреждение. + trailing-spaces: enable + new-line-at-end-of-file: enable + + # Ошибки, которые ломают парсинг или молча меняют смысл — строго. + key-duplicates: enable + octal-values: + forbid-implicit-octal: true + forbid-explicit-octal: true + + # "---" в начале файла — полезная конвенция Ansible, но не критично. + document-start: + level: warning + + # braces/brackets: Jinja-выражения в inline-словарях часто дают ложные + # срабатывания на пробелах внутри {{ }}. + braces: + min-spaces-inside: 0 + max-spaces-inside: 1 + brackets: + min-spaces-inside: 0 + max-spaces-inside: 1