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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTocXkGUUazHdKKd3r9k71
This commit is contained in:
Dmitry
2026-08-26 22:09:59 +03:00
co-authored by Claude Opus 5
parent b953909e0a
commit a7b0635830
3 changed files with 288 additions and 0 deletions
+85
View File
@@ -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
+134
View File
@@ -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 <RUNNER_TOKEN из Gitea Settings -> 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"
+69
View File
@@ -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