test(lint): demand a package-install nocheck on every role package_install

The batch lint cannot see the conditions an include or a role invocation puts
around a task file, so an install there could pass as unconditional and be
dropped onto every host. Every package_install task in a role now has to say
why it cannot ride the deploy-start batch; the 26 existing ones name their
reason (needed before update, repository first, task or include condition,
conditional role, removal, controller delegation).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5.5 committed 2026-10-28 23:00:46 +01:00
1 parent 18ea75049e
commit cb4d340de9
26 files changed
+55 -28

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
---
- block:
- name: 📦 Install GCC compiler and runtime (cross-distro)
package_install:
package_install: # nocheck: package-install sys-pip builds on gcc before the update role and the batch run
id: gcc
- ansible.builtin.set_fact:
+1 -1
View File
@@ -24,7 +24,7 @@
}}
- name: 📦 Install Nix via package manager on Arch-based systems
package_install:
package_install: # nocheck: package-install Arch only, other distros take the installer path
id: nix
become: true
when:
@@ -49,7 +49,7 @@
- name: 📦 Install the btrfs tools the snapshot launcher shells out to
become: true
package_install:
package_install: # nocheck: package-install only on hosts with a btrfs mount
id: btrfs-tools
when: ansible_facts.mounts | selectattr('fstype', 'equalto', 'btrfs') | list | length > 0
+1 -1
View File
@@ -7,7 +7,7 @@
no-resolv and would answer .onion only.
- name: "📦 Egress router | Install dnsmasq (host split-DNS forwarder)"
become: true
package_install:
package_install: # nocheck: package-install only on the egress router this file is included for
id: dnsmasq
- name: "📁 Egress router | Ensure the dnsmasq drop-in directory exists"
ansible.builtin.file:
+1 -1
View File
@@ -18,7 +18,7 @@
when: WIREGUARD_MESH_NAMES | length == 0
- name: "📦 Install the WireGuard tooling"
package_install:
package_install: # nocheck: package-install the mesh bootstrap runs before the update role and the batch run
id:
- wireguard
- iproute
+1 -1
View File
@@ -10,7 +10,7 @@
server IP/hostname is mandatory.
- name: "📦 Install the NFS client"
package_install:
package_install: # nocheck: package-install the controller installs it too, by delegation from the NFS server role
id: nfs-client
- name: "🔧 Match NFSv4 idmap to the ganesha test server"
@@ -1,6 +1,6 @@
---
- name: "📦 Ensure e2fsprogs is installed for mkfs.ext4"
package_install:
package_install: # nocheck: package-install only the ext4 loop overlay backing includes this file
id: e2fsprogs
- name: "✅ Refuse to mount over a non-empty export base"
@@ -29,7 +29,7 @@
mode: "0755"
- name: "📦 Install the NFS client"
package_install:
package_install: # nocheck: package-install runs on the controller by delegation, outside any host's batch
id: nfs-client
- name: "🔎 Read current mount source at controller {{ DIR_VAR_LIB }}"
@@ -1,6 +1,6 @@
---
- name: "📦 Install the userspace nfs-ganesha server"
package_install:
package_install: # nocheck: package-install only the ganesha NFS flavor includes this file
id:
- nfs-ganesha
- libntirpc
@@ -1,12 +1,12 @@
---
- name: "📦 Install the kernel NFS server"
package_install:
package_install: # nocheck: package-install only the kernel NFS flavor includes this file
id:
- nfs-client
- nfs-kernel-server
- name: "📦 Install the SELinux python binding (seboolean module + selinux fact)"
package_install:
package_install: # nocheck: package-install only the kernel NFS flavor includes this file
id: selinux-python-binding
- name: "📊 Re-gather the SELinux fact now the binding is present"
+1 -1
View File
@@ -17,7 +17,7 @@
system_service_tpl_on_failure: "{{ lookup('unit_name','sys-ctl-alm-email@', suffix=False) }}%i.service"
- name: 📦 install curl
package_install:
package_install: # nocheck: package-install only hosts with Telegram alerts run this role
id: curl
- include_tasks: utils/once/flag.yml
+1 -1
View File
@@ -9,7 +9,7 @@
loop_var: sys_ctl_cln_bkps_dependency
- name: 📦 install lsof
package_install:
package_install: # nocheck: package-install only hosts that clean backups run this role
id: lsof
- name: 📦 install psutil via pip
@@ -10,7 +10,7 @@
loop_var: sys_ctl_mtn_cert_renew_dependency
- name: 📦 install certbot
package_install:
package_install: # nocheck: package-install only hosts with Let's Encrypt certificates run this role
id: certbot-nginx
- include_role:
+1 -1
View File
@@ -1,7 +1,7 @@
---
- name: ✅ Ensure hostname package is installed
become: true
package_install:
package_install: # nocheck: package-install skipped inside containers
id: hostname-tool
when: not DOCKER_IN_CONTAINER | bool
+1 -1
View File
@@ -5,7 +5,7 @@
when: run_once_dev_gcc is not defined
- name: 🐍 python pip install
package_install:
package_install: # nocheck: package-install needed before the update role and the batch run
id: python-pip
- include_tasks: utils/once/flag.yml
+1 -1
View File
@@ -1,7 +1,7 @@
---
- block:
- name: 📦 Install sudo (Arch/Manjaro)
package_install:
package_install: # nocheck: package-install user-root needs sudo before the update role and the batch run
id: sudo
- name: ▶️ run the sudo tasks once
set_fact:
+1 -1
View File
@@ -1,6 +1,6 @@
---
- name: 📦 install certbot
package_install:
package_install: # nocheck: package-install only hosts with Let's Encrypt certificates run this role
id: certbot
- name: "📂 Include tasks for 'No-Webroot-ACME-CHALLENGE-METHOD'"
+1 -1
View File
@@ -8,7 +8,7 @@
when: ansible_facts['os_family'] == 'RedHat'
- name: 📦 Install Docker packages
package_install:
package_install: # nocheck: package-install needs the Docker repository the role adds first
id: docker-cli
register: sys_svc_container_packages_install
retries: 5
+1 -1
View File
@@ -7,7 +7,7 @@
global_install: true
- name: ✅ Ensure 'ip' and 'ping' commands are available for automtu
package_install:
package_install: # nocheck: package-install only for the automtu measurement this file is included for
id:
- iproute
- iputils-ping
+1 -1
View File
@@ -1,6 +1,6 @@
---
- name: "✅ Ensure msmtp-mta is absent (conflicts with postfix smtp-forwarder)"
package_install:
package_install: # nocheck: package-install removes msmtp-mta, the batch only installs
id: msmtp-mta
state: absent
+1 -1
View File
@@ -26,7 +26,7 @@
- when: lookup('email').external | bool
block:
- name: 📦 Install msmtp-mta when Mailu is used and package exists on distro
package_install:
package_install: # nocheck: package-install only inside the Mailu block and only where the distro ships msmtp-mta
id: msmtp-mta
register: sys_svc_mail_msmtp_mta
+1 -1
View File
@@ -13,7 +13,7 @@
when: sys_svc_mail_smtp_postfix_binary.stat.exists
- name: 🗑️ Remove postfix when external SMTP is used (conflicts with msmtp-mta)
package_install:
package_install: # nocheck: package-install removes postfix, the batch only installs
id: postfix
state: absent
when: lookup('email').external | bool
+1 -1
View File
@@ -12,7 +12,7 @@
when: run_once_sys_daemon is not defined
- name: "📦 Install local SMTP relay (Postfix)"
package_install:
package_install: # nocheck: package-install only when the email relay is not external
id: postfix
- name: "🔧 Configure Postfix as localhost-only relay"
+1 -1
View File
@@ -12,7 +12,7 @@
when: ansible_facts['os_family'] == 'Debian'
- name: 📦 Install OpenSSH client
package_install:
package_install: # nocheck: package-install user-root and the mesh bootstrap need it before the update role and the batch run
id: openssh-client
- include_tasks: utils/once/flag.yml
@@ -1,6 +1,6 @@
---
- name: 📦 Install ACL utilities
package_install:
package_install: # nocheck: package-install only the permissions step this file is included for needs setfacl
id: acl
- name: ✅ Ensure backups root exists and owned by backup
@@ -12,7 +12,12 @@ role's own task installs it. Four rules hold that split together:
them on hosts where the condition is false;
* every id of a role the constructor runs ahead of the batch is
``batch: false``, because the batch would install it too late;
* a role file references only ids the shared root file declares.
* a role file references only ids the shared root file declares;
* every ``package_install`` task of a role carries
``# nocheck: package-install <reason>`` on its module line or the line
above, naming why it cannot ride the batch. The batch cannot see the
conditions an include or a role invocation puts around a task file, so an
unmarked install would slip past the first two rules.
A ``run_once_<role>`` guard is no condition here: it only keeps a role from
running twice in one deploy.
@@ -25,7 +30,8 @@ import unittest
from pathlib import Path
from typing import Any
from utils.cache.files import iter_project_files
from utils.annotations.suppress import is_suppressed_at
from utils.cache.files import iter_project_files, read_text
from utils.cache.yaml import load_yaml_any
from utils.packages.registry import build_registry, load_references
from utils.roles.invocations import (
@@ -45,6 +51,8 @@ _RUN_ONCE_GUARD_RE = re.compile(
r"|not\s+\(\s*run_once_\w+\s*\|\s*default\(false\)\s*\|\s*bool\s*\))$"
)
_BATCH_LOOKUP = "lookup('package_batch'"
_RULE = "package-install"
_MODULE_LINE_RE = re.compile(r"^\s*(?:-\s+)?package_install:")
def _conditions(task: dict[str, Any]) -> list[str]:
@@ -179,6 +187,25 @@ class TestPackageBatch(unittest.TestCase):
+ "\n".join(f" - {o}" for o in offenders)
)
def test_role_package_installs_are_marked(self) -> None:
offenders: list[str] = []
for path in _role_task_files():
lines = read_text(str(path)).splitlines()
offenders.extend(
f"{path.relative_to(PROJECT_ROOT)}:{number}"
for number, line in enumerate(lines, start=1)
if _MODULE_LINE_RE.match(line)
and not is_suppressed_at(lines, number, _RULE)
)
if offenders:
self.fail(
f"{len(offenders)} package_install task(s) in a role bypass the "
"deploy-start batch without saying why. Drop the task so the batch "
"installs the id, or flag the id 'batch: false' and mark the task "
f"'# nocheck: {_RULE} <why it cannot ride the batch>':\n"
+ "\n".join(f" - {o}" for o in offenders)
)
def test_references_name_shared_ids(self) -> None:
offenders = [
f"roles/{role}/meta/packages.yml: '{package_id}'"