mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
The Docker updater watches image plus version, the repository updater
repository plus ref. A dozen pins fell outside both - an app_version, a
release, a tag in a registry neither speaks - so nothing ever told anyone
they had gone stale.
Such a pin now names its upstream next to itself in an `update:` block:
git_tags, registry_tags, npm, http_regex or a role-local script, with
`match` selecting a tag flavour and `strip` removing a prefix the pin does
not carry. utils/update/source.py resolves them beside the two existing
updaters, `python -m cli.contributing.update.source` bumps them, the
update-version-sources job opens one PR per role, and
tests/external/update/source warns while a bump is pending.
Two lints hold the rule: every semver pin is watched by one of the three
updaters or opted out with a reason, and a semver only ever sits behind
`version`, `<what>_version` or `ref` - which renames moodle's release to
app_version and simpleicons' local_version to local_tag, the latter being
the tag of a locally built image rather than an upstream version. A
declaration naming a key the entity does not carry, or a type the resolver
does not know, fails the lint instead of resolving nothing in silence.
Picked from feature/store-i18n (94d607fd0a) onto current main. Two things
differ from the original: svc-ai-lmstudio no longer carries an app_version
pin, so its declaration is dropped, and web-app-stalwart's webui_version,
added since, declares the git tags of the webui repository.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
139 lines
48 KiB
Markdown
139 lines
48 KiB
Markdown
# Suppression Markers 🚫
|
|
|
|
The unified `# noqa` / `# nocheck` syntax that suppresses individual checks across the infinito-nexus test suite.
|
|
|
|
You MUST use these markers only when the check genuinely does not apply. You MUST NOT use them to silence legitimate issues.
|
|
|
|
## Grammar 📐
|
|
|
|
A suppression marker is a comment carrying one or more rule keys:
|
|
|
|
```
|
|
<comment-prefix> (noqa|nocheck): <rule>(, <rule>)*
|
|
```
|
|
|
|
`noqa` and `nocheck` are case-insensitive synonyms in the parser. Every project rule MUST use `nocheck:` because `# noqa: <code>` is also parsed by ruff as a flake8 directive and warns on non-flake8 keys. Reserve `noqa:` for real flake8 / ruff codes (`E402`, `F401`, …). [test_noqa_only_ruff_codes.py](../../../../tests/lint/filesystem/python/test_noqa_only_ruff_codes.py) hard-rejects `# noqa: <code>` whose codes do not match `[A-Z]+\d+`.
|
|
|
|
`<rule>` is a kebab-case identifier from the catalog below. Multiple rules MAY be combined comma-separated.
|
|
|
|
| Prefix | Use in |
|
|
| ------------ | --------------------------------------- |
|
|
| `#` | Python, YAML, shell, conf, INI, … |
|
|
| `//` | JS, JSONC, … |
|
|
| `{# … #}` | Jinja2 templates |
|
|
| `<!-- … -->` | HTML, Markdown |
|
|
|
|
Implementation: [suppress.py](../../../../utils/annotations/suppress.py).
|
|
|
|
## Position semantics 📍
|
|
|
|
The placement rule is per check. The catalog "Position" column uses these labels.
|
|
|
|
| Label | Meaning |
|
|
| ----------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
| `same line` | Marker MUST be on the construct's line. |
|
|
| `same line / span` | Marker MUST be on the construct's line or any physical line spanned by a multi-line call. |
|
|
| `line above` | Marker MUST be on the immediately preceding non-empty line. Blank lines break the association. |
|
|
| `same or above` | Either of the above is accepted. |
|
|
| `comment block above` | Marker MAY sit on any line of a contiguous comment block above the construct. No blank line between marker and construct. |
|
|
| `head (first 30 lines)` | Marker MAY appear anywhere in the first 30 lines of the file. For file-level opt-outs that stay visible at the top. |
|
|
| `anywhere` | Marker MAY appear on any line of the file. For whole-file opt-outs. |
|
|
|
|
## Catalog 📚
|
|
|
|
| Rule | Position | Affected test | Effect |
|
|
| --------------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| `url` | same or above | [test_urls_reachable.py](../../../../tests/external/repository/test_urls_reachable.py) | Skips probing the literal HTTP(S) URL. Use for CDN roots and API base URLs that 4xx without a path. |
|
|
| `onion-flag` | same or above | [test_onion_port_flag.py](../../../../tests/lint/ansible/services/test_onion_port_flag.py) | Accepts a `false` under `ports.onion`. The marker MUST carry a reason naming why that port stays off the node onion; a bare marker is rejected. |
|
|
| `docker-version` | same or above | [test_image_versions.py](../../../../tests/external/update/docker/test_image_versions.py), [test_semver_pinning.py](../../../../tests/lint/ansible/services/test_semver_pinning.py) | Skips both the live version-update warning and the non-semver-pin lint for that image's `version:` key. |
|
|
| `repository-version` | same or above | [test_repository_versions.py](../../../../tests/external/update/repository/test_repository_versions.py), [test_semver_pinning.py](../../../../tests/lint/ansible/services/test_semver_pinning.py) | Skips both the live ref-update warning and the non-semver-pin lint for that entity's `ref:` key. |
|
|
| `version-source` | same or above | [test_version_sources.py](../../../../tests/external/update/source/test_version_sources.py) | Skips the live update warning for a pin that declares its upstream in an `update:` block. |
|
|
| `unwatched-version` | same or above | [test_version_pins_are_watched.py](../../../../tests/lint/repository/test_version_pins_are_watched.py) | Accepts a semver pin that no updater bumps. The marker MUST carry a reason naming why the version stays where it is. |
|
|
| `dockerfile-arg-default` | same or above | [test_arg_without_default.py](../../../../tests/lint/docker/dockerfile/test_arg_without_default.py) | Allows the marked `ARG` to carry a default. The caller normally supplies every build argument so a missing one fails the build instead of selecting a base image nobody chose. |
|
|
| `debian-codename` | same or above | [test_no_debian_codename_tags.py](../../../../tests/lint/repository/test_no_debian_codename_tags.py) | Allows an image reference whose tag names a Debian release by codename. Reserved for an upstream that publishes no numeric tag; the marker MUST name which upstream, because a codename pin can never be raised by the updater. |
|
|
| `direct-yaml` | same line / span | [test_no_direct_calls.py](../../../../tests/lint/repository/yaml/test_no_direct_calls.py) | Allows the marked `yaml.safe_load` / `safe_dump` call to bypass `utils.cache.yaml`. MUST use `nocheck:`, not `noqa:`. |
|
|
| `curl-timeout` | same or above | [test_curl_via_filter.py](../../../../tests/lint/repository/test_curl_via_filter.py) | Allows a bare curl invocation without the `curl` filter or inline `--max-time`. |
|
|
| `redundant-bool-ternary` | same line | [test_no_redundant_bool_patterns.py](../../../../tests/lint/filesystem/python/test_no_redundant_bool_patterns.py) | Allows the redundant `\| bool \| ternary('true','false')` pattern when a consumer requires the lowercase tokens. |
|
|
| `redundant-bool-comparison` | same line | [test_no_redundant_bool_patterns.py](../../../../tests/lint/filesystem/python/test_no_redundant_bool_patterns.py) | Allows `... \| bool == True/False/true/false` when a real tri-state guard is intended (distinguishing `None` from `False`). |
|
|
| `shared` | comment block above | [test_service_shared_consistency.py](../../../../tests/lint/ansible/services/test_service_shared_consistency.py) | Marks a service whose `enabled: true` does not require `shared: true`. |
|
|
| `email` | line above | [test_email.py](../../../../tests/lint/ansible/roles/web-app/integration/test_email.py) | Suppresses the missing-email-integration warning. Pair with `enabled: false` and `shared: false`. |
|
|
| `logout` | line above | [test_logout_dashboard.py](../../../../tests/lint/ansible/roles/web-app/integration/test_logout_dashboard.py) | Opts a `web-app-*` role out of the universal-logout integration. Marker MUST be directly above `logout:`. Pair with `enabled: false` and `shared: false`. |
|
|
| `dashboard` | line above | [test_logout_dashboard.py](../../../../tests/lint/ansible/roles/web-app/integration/test_logout_dashboard.py) | Opts a `web-app-*` role out of the dashboard tile integration. Same shape as `logout`. |
|
|
| `sso` | line above | [test_sso.py](../../../../tests/lint/ansible/roles/web-app/integration/test_sso.py) | Opts the role's unified `services.sso` block out. Declares "no login flow"; paired with `enabled: false` + `shared: false`. |
|
|
| `file-size` | head (first 30 lines) | [test_python_file_size.py](../../../../tests/lint/repository/structure/test_python_file_size.py) | Opts the entire `.py` file out of the 500-line cap. |
|
|
| `comments-valid` | head (first 30 lines) | [test_comments_valid.py](../../../../tests/lint/repository/test_comments_valid.py) | Opts the entire file out of the useful-comment check. Reserved for vendored / upstream config templates (e.g. Discourse `app.yml`) whose inline documentation is all intentional. |
|
|
| `info-media` | head of `meta/main.yml` (first 30 lines) | [test_info_homepage_video.py](../../../../tests/lint/ansible/roles/meta/test_info_homepage_video.py) | Opts an invokable role out of the required `meta/info.yml` `homepage` + `video` entries. Marker lives in `meta/main.yml` (a comment-only `info.yml` would fail the info schema lint). Reserved for fully self-written roles (backups, host plumbing) that have no upstream product page or demo video. |
|
|
| `credential-masking` | same or above | [test_credential_tasks_are_masked.py](../../../../tests/lint/ansible/modules/test_credential_tasks_are_masked.py) | Allows the marked task to carry a credential without `no_log` on itself or a parent. Marker goes on or directly above the task's `- name:`. |
|
|
| `env-default` | same or above | [test_no_env_defaults_in_javascript.py](../../../../tests/lint/filesystem/env/test_no_env_defaults_in_javascript.py) | Allows JavaScript to substitute a value for an unset environment variable. Reserved for a value the code itself owns and no role renders. Normalising to `""`, `"[]"`, `false`, `0` or another `process.env` entry needs no marker. |
|
|
| `jinja-mapping-key` | same or above | [test_no_jinja_in_mapping_keys.py](../../../../tests/lint/ansible/roles/meta/test_no_jinja_in_mapping_keys.py) | Allows a Jinja expression to stand as a mapping key under `roles/*/meta/`. Ansible renders the values of an `include_vars` payload and leaves the keys as written, so the consumer receives the source text. Marker goes on or directly above the line. |
|
|
| `host-node-resolver` | same or above | [test_host_node_resolver_is_unconditional.py](../../../../tests/lint/ansible/tasks/test_host_node_resolver_is_unconditional.py) | Allows a conditional `resolve_host_cid` include whose `<PREFIX>_HOST_NODE` a `delegate_to` in the same role reads. Marker goes on or directly above the include's `- name:`. |
|
|
| `project-walk` | same line / span | [test_no_raw_project_walk.py](../../../../tests/lint/filesystem/python/test_no_raw_project_walk.py) | Allows a raw `Path.rglob`, `os.walk`, or `glob.glob` call outside the `utils.cache.files.iter_project_files` contract. |
|
|
| `cache-read` | same line / span | [test_no_uncached_file_reads.py](../../../../tests/lint/filesystem/python/test_no_uncached_file_reads.py) | Allows a raw `Path.read_text` / `Path.read_bytes` call in `tests/lint/` outside the LRU-cached `utils.cache.files.read_text` contract. |
|
|
| `lookup-cache-import` | same or above | [test_no_cache_imports_in_lookups.py](../../../../tests/lint/filesystem/python/test_no_cache_imports_in_lookups.py) | Allows a lookup plugin (`plugins/lookup/` or `roles/*/lookup_plugins/`) to import a config-merge function from `utils.cache.applications` / `.domains` / `.users` instead of reaching the SPOT via `lookup_loader.get('applications'/'config'/'domains'/'users')`. Reserved for the SPOT providers (`applications` / `config` / `applications_current_play` / `users` / `domains`) and the raw-volume accessors (`volume` / `nginx`). |
|
|
| `run-once` | anywhere | [test_run_once_tags.py](../../../../tests/lint/ansible/modules/test_run_once_tags.py), [test_schema.py](../../../../tests/integration/roles/run_once/test_schema.py) | Marks a role's `tasks/main.yml` as intentionally re-runnable. Skips both the run-once tag check and the suffix check. |
|
|
| `run-once-suffix` | same line | [test_schema.py](../../../../tests/integration/roles/run_once/test_schema.py) | Allows a single `when:` item to reference a `run_once_<other>` flag whose suffix differs from the current role. |
|
|
| `raw-docker` | same or above; head (first 30 lines) | [test_no_raw_docker.py](../../../../tests/integration/infrastructure/docker/test_no_raw_docker.py) | Marks a single line or a whole file under `roles/` as legitimately calling `docker` / `docker compose` / `docker-compose` directly. |
|
|
| `hardcoded-dns-resolver` | same or above | [test_no_hardcoded_dns_resolvers.py](../../../../tests/lint/repository/network/test_no_hardcoded_dns_resolvers.py) | Marks a line that needs a literal IP from `NETWORK_PUBLIC_DNS_RESOLVERS` where the substitution point cannot consume the variable (CoreDNS non-Jinja templates, host-bootstrap shell, doc examples). |
|
|
| `dynamic-flag` | same line OR comment block above key | [test_dynamic_flags.py](../../../../tests/integration/roles/meta/services/test_dynamic_flags.py) | Marks a `roles/*/meta/services.yml` flag whose value stays literal. Per-flag (same line) for databases. Block-level (comment block above) when both flags stay literal. |
|
|
| `native-metrics` | comment block above | [test_native_metrics_contract.py](../../../../tests/integration/roles/meta/services/prometheus/test_native_metrics_contract.py) | Marks a role whose `services.prometheus.native_metrics.enabled` and `templates/prometheus.yml.j2` presence are intentionally inconsistent. Reserved for prepared-but-default-off scrape fragments (operator opt-in). |
|
|
| `prometheus-scrape` | same or above | [test_roles_with_service.py](../../../../tests/unit/python/plugins/lookup/test_roles_with_service.py) | Documents a `services.prometheus.scrape: false` opt-out in `roles/<role>/meta/services.yml`. The role keeps `services.prometheus.{enabled,shared}: true` (so the role-wiring contract stays satisfied) but `roles_with_service('prometheus')` drops it from `PROMETHEUS_TARGET_ROLES_JSON`. Reserved for 301-redirect-only vhosts that never serve a request through lua-resty-prometheus, so they emit no `app="<role>"` label and have no scrape target. The marker accompanies the `scrape:` data flag the lookup honors. |
|
|
| `matomo-track` | same or above | [test_roles_with_service.py](../../../../tests/unit/python/plugins/lookup/test_roles_with_service.py) | Documents a `services.matomo.track: false` opt-out in `roles/<role>/meta/services.yml`. The role keeps `services.matomo.{enabled,shared}: true` (so the role-wiring contract stays satisfied) but `roles_with_service('matomo')` drops it from `MATOMO_TARGET_ROLES_JSON` and the matomo tracker e2e contract. Reserved for static-file (autoindex) and 301-redirect vhosts that carry no user-facing HTML page worth a `_paq` tracker. The marker accompanies the `track:` data flag the lookup honors. |
|
|
| `variants-static-override` | same line | [test_variants_no_static_override.py](../../../../tests/integration/roles/meta/services/test_variants_no_static_override.py) | Allows a `roles/*/meta/variants.yml` leaf override of an attribute whose base value in `meta/services.yml` is a static literal. Place on the override line in `variants.yml`. Use only when the consumer chain is provably immune to the variant-merge propagation gap; the default is to make the base value dynamic via `"{{ '<role>' in group_names }}"`. |
|
|
| `variants-shared-false-once` | same or above | [test_variants_shared_false_once.py](../../../../tests/lint/ansible/services/test_variants_shared_false_once.py) | Exempts one `roles/*/meta/variants.yml` `services.<key>.shared: false` pin from the one-local-provider-variant limit for a service whose base `meta/services.yml` uses literal `enabled: true` and dynamic `shared`. Place on the extra `shared: false` line or service key line. Place above the service key in `meta/services.yml` to exempt the service. |
|
|
| `variants-services-match` | same or above | [test_services_match.py](../../../../tests/integration/roles/meta/variants/test_services_match.py) | Allows a `roles/*/meta/variants.yml` `services.<key>` entry to reference a service key that is NOT declared as a top-level entry in `meta/services.yml`. Reserved for resolver-only matrix hooks (typically on invokable infrastructure roles) whose variant entries pin the round's companion topology via service-key flags the resolver maps to provider roles, with no real consumer contract in the base meta. |
|
|
| `variants-auth-isolation` | same or above | [test_auth_isolation.py](../../../../tests/integration/roles/meta/variants/test_auth_isolation.py) | Exempts an auth-isolated variant entry in `roles/*/meta/variants.yml` (one that enables exactly one of LDAP / OIDC / OAuth2) from the rule that every non-auth service-key in the role's variant union must be pinned to literal `false`. Place on the leading `- services:` line of the variant or on the line above. Use only when the broader closure is intentional. |
|
|
| `lookup-config-path` | same or above | [config/](../../../../tests/integration/ansible/lookups/config/) | Skips a single `lookup('config', …)` call from path-validation passes. Use when the call resolves against runtime-only state, typically self-referential roles. |
|
|
| `unused-var` | same or above | [test_role_and_group_vars_used.py](../../../../tests/lint/ansible/variables/test_role_and_group_vars_used.py) | Exempts a top-level key in `roles/<role>/{vars,defaults}/main.yml` or `group_vars/**/*.yml` from the "must be referenced" check. Use when the consumer is non-YAML/Jinja (Python plugins, external tools). |
|
|
| `project-root-import` | same or above | [test_project_root_import.py](../../../../tests/lint/filesystem/python/test_project_root_import.py) | Permits a local `PROJECT_ROOT` / `parents[N]` / `os.pardir` chain. Reserved for `__main__.py` `sys.path` bootstrap and standalone scripts under `roles/<role>/files/`. The marker MUST carry an inline explanation. |
|
|
| `domain-spot` | same or above | [test_domain_primary_spot.py](../../../../tests/lint/repository/network/test_domain_primary_spot.py) | Marks a line using a `<word>.{{ DOMAIN_PRIMARY }}` host literal outside the meta SPOT. Reserved for legacy domain cleanups that the canonical SPOT cannot represent. |
|
|
| `literal-protocol-lookup` | same or above | [test_literal_protocol_with_lookup.py](../../../../tests/lint/repository/yaml/test_literal_protocol_with_lookup.py) | Allows a hardcoded `http://` / `https://` literal next to a `lookup(...)` call. Reserved for genuinely fixed protocols (Docker-network upstreams, loopback URLs in CI fixtures). |
|
|
| `self-path-reference` | same line | [test_no_self_path_reference.py](../../../../tests/lint/filesystem/python/test_no_self_path_reference.py) | Allows a file to contain its own relative path from the repo root as a string literal. Reserved for load-bearing self-references. Repo-root files are exempt without a marker. |
|
|
| `role-file-spot` | same or above | [test_file_spot.py](../../../../tests/lint/ansible/roles/test_file_spot.py) | Allows a `.py` file to carry a literal role-file path instead of the matching `ROLE_FILE_*` constant from `utils.roles.mapping`. Reserved for tests asserting the constant's value, migration scripts, or doc strings. |
|
|
| `playwright-service-flag` | comment block above | [test_env_services_match.py](../../../../tests/integration/roles/playwright/test_env_services_match.py) | Exempts a top-level service in `roles/<role>/meta/services.yml` from the "must be registered as `<NAME>_SERVICE_ENABLED=` in `templates/playwright.env.j2`" check. |
|
|
| `playwright-service-gate` | same or above | [test_spec_env_gates.py](../../../../tests/integration/roles/playwright/test_spec_env_gates.py) | Exempts a `<NAME>_SERVICE_ENABLED=` line in `roles/<role>/templates/playwright.env.j2` from the "must be gated in `files/playwright/playwright.spec.js`" check. |
|
|
| `mcp-consumer-services-entry` | head of file | [test_mcp_consumer_services_entry.py](../../../../tests/lint/ansible/services/test_mcp_consumer_services_entry.py) | Exempts a server-capable `roles/<role>/meta/mcp.yml` from the "must carry a `"{{ '<client>' in group_names }}"`-gated `services.<client-entity>` entry for every role declaring `mcp_consumer: true`" check. Reserved for a provider that legitimately owns no co-deploy edge to its clients. |
|
|
| `mcp-consumer-variant` | head of file | [test_mcp_consumer_variant.py](../../../../tests/lint/ansible/services/test_mcp_consumer_variant.py) | Exempts a server-capable `roles/<role>/meta/mcp.yml` from the "must ship one `meta/variants.yml` entry enabling every admitted consumer, with `sso` pinned on, and pulling in a consumer variant that does not pin `sso: false`" check. Reserved for a provider whose pairing is genuinely proven elsewhere. |
|
|
| `pkgmgr-deprecated` | same or above | [test_pkgmgr_deprecated.py](../../../../tests/lint/repository/dependencies/test_pkgmgr_deprecated.py) | Suppresses the warn-only deprecation notice for `include_role` / `import_role` of `pkgmgr` / `pkgmgr-install`. The check emits a `DeprecationWarning` locally and a GitHub Actions `::warning` annotation in CI. Prefer `pip install <name>` or `pip install git+<repo>`. |
|
|
| `deprecated-http-header` | same or above | [test_no_deprecated_http_security_headers.py](../../../../tests/lint/repository/network/test_no_deprecated_http_security_headers.py) | Exempts a deprecated `add_header` / `Header set/add` line for `X-Frame-Options` / `X-XSS-Protection` / `X-Download-Options` / `X-Permitted-Cross-Domain-Policies`. Prefer Content-Security-Policy via the role's `meta/server.yml` (`csp.flags` / `csp.whitelist`). |
|
|
| `container-name` | same or above | [test_mode_incompatible_keys.py](../../../../tests/lint/ansible/jinja/test_mode_incompatible_keys.py) | Allows a raw `container_name:` key in a `roles/*/templates/**/*.j2` template. Default path: route through the generic `compose_only` lookup (`{{ lookup('compose_only', 'container_name', NAME) }}`) so swarm-mode replicas do not collide on the static name. |
|
|
| `pull-policy-key` | same or above | [test_mode_incompatible_keys.py](../../../../tests/lint/ansible/jinja/test_mode_incompatible_keys.py) | Allows a raw `pull_policy:` key in a `roles/*/templates/**/*.j2` template. Default path: route through the generic `compose_only` lookup (`{{ lookup('compose_only', 'pull_policy', 'never') }}`) so swarm does not reject the compose-only extension with "Additional property pull_policy is not allowed". |
|
|
| `restart-key` | same or above | [test_mode_incompatible_keys.py](../../../../tests/lint/ansible/jinja/test_mode_incompatible_keys.py) | Allows a raw `restart:` key in a `roles/*/templates/**/*.j2` template. Default path: route through the `compose_restart` lookup so swarm-mode does not silently drop the operator's `docker_restart_policy`. |
|
|
| `compose-structure` | same or above; head (first 30 lines) | [test_compose_template_structure.py](../../../../tests/lint/ansible/jinja/test_compose_template_structure.py) | Exempts a `roles/*/templates/*compose.yml.j2` line, or the whole file (head marker), from the canonical compose skeleton: base-include header, `{{ service_name }}:` service keys each preceded by their `{% set service_name %}`, all per-service `{% set %}` knobs on two-space-indented dash-free lines above the key, `{% include %}` at four spaces inside the service body, and column-0 `compose_volumes` / `compose_networks` at the tail. The layout renders correctly because the `stack_host_template` action plugin sets `lstrip_blocks=True` for `compose.yml.j2`. Reserved for legitimate deviations that genuinely cannot follow the skeleton. |
|
|
| `seaweedfs-required` | head (first 30 lines) | [test_database_requires_seaweedfs.py](../../../../tests/integration/roles/meta/services/test_database_requires_seaweedfs.py) | Exempts a database-backed `web-app-*` role from the requirement to declare a seaweedfs object-store service. Place in the head of `roles/<role>/meta/services.yml`. Use only when the application genuinely needs no object storage. |
|
|
| `tor-contract` | anywhere in file | [test_tor_contract.py](../../../../tests/lint/ansible/roles/meta/test_tor_contract.py) | Exempts a domain-serving role (`web-`/`svc-db-` prefix or explicit `domains.canonical`) from the "must declare a `tor:` block in `meta/services.yml`" contract. Reserved for roles with no onion surface, e.g. pure redirect helpers. |
|
|
| `templated-conditional` | same or above | [test_no_templated_conditionals.py](../../../../tests/lint/ansible/test_no_templated_conditionals.py) | Exempts a `when` / `failed_when` / `changed_when` / `until` value that legitimately carries `{{ }}` / `{% %}`. Reserved for genuine edge cases; the default path is a bare expression. Per-item render/apply data guards belong on a `condition:` data field (honored by the sys-svc-compose render helper and `LISTMONK_SETTINGS`), not `when:`. |
|
|
| `container-address-quote` | same or above | [test_address_never_quoted.py](../../../../tests/lint/ansible/shell/container/test_address_never_quoted.py) | Allows `*_CONTAINER_ADDRESS \| quote` in a role task file. Reserved for sites that never see the swarm `$(...)` fragment (`DEPLOYMENT_MODE == 'compose'`-gated tasks, `compose_mode_force` roles) or consumers that re-evaluate the argument themselves. |
|
|
| `container-exec-resolver` | same or above; `argv:` target item | [test_exec_uses_resolver.py](../../../../tests/lint/ansible/shell/container/test_exec_uses_resolver.py) | Allows a `container exec` / `container inspect` target that does not route through the `container_address` lookup. Reserved for a container ID already resolved at `delegate_to` runtime (`*_LOCAL_CID`), where the lookup would return a manager-only subshell, and for deliberate bare-name partner probes gated on compose co-location. |
|
|
| `container-address-needs-shell` | same or above; `argv:` target item | [test_argv_exec_needs_shell.py](../../../../tests/lint/ansible/shell/container/test_argv_exec_needs_shell.py) | Allows a `container_address`-derived target to be reached from a `command: argv:` list, which runs no shell to evaluate the swarm `$(...)` fragment. Reserved for roles pinned to compose via `compose_mode_force`, where the lookup can only ever yield a bare name. |
|
|
| `container-inspect-type` | same or above; `argv:` inspect item | [test_inspect_pins_type.py](../../../../tests/lint/ansible/shell/container/test_inspect_pins_type.py) | Allows an untyped `container inspect` under `roles/`. Default path: pin `--type container`, because a role's overlay network carries the same entity name as its main container, so an untyped inspect can resolve the network and report `map has no entry for key "State"`. Use the typed subcommand (`container volume inspect` etc.) when the target genuinely is not a container. |
|
|
| `container-healthcheck-unused` | same or above the service key or its `healthcheck:` line | [test_container_healthcheck_declared_is_used.py](../../../../tests/lint/ansible/jinja/test_container_healthcheck_declared_is_used.py) | Allows a `services.<key>.healthcheck` that no `lookup('container_healthcheck', …)` call renders. Default path: add the lookup to the service block, because an unrendered declaration reads like the service is watched while the compose file carries no probe. Reserved for a call the static resolver cannot attribute — mastodon renders every service through a macro whose `service_name` is a macro parameter. |
|
|
| `image-healthcheck` | same or above the service key | [test_image_healthcheck_declared.py](../../../../tests/integration/roles/test_image_healthcheck_declared.py) | Allows a `roles/*/meta/services.yml` entry whose image ships no `HEALTHCHECK` to declare none either. Default path: add `healthcheck:` to the entry, because a container without a probe counts as healthy the moment its process starts — which is how a service that came back empty passes a restore drill. Reserved for containers nothing can probe: a one-shot process that exits, or an image with neither a port nor a shell. |
|
|
| `csp-unsafe` | same line | [test_csp_unsafe_requires_justification.py](../../../../tests/lint/ansible/roles/meta/test_csp_unsafe_requires_justification.py) | Documents an `unsafe-*: true` flag in `roles/*/meta/csp.yml`. MUST be paired with a `Reason:` on the same line naming what breaks without the relaxation. `unsafe-inline` re-admits the inline execution an XSS needs and `unsafe-eval` re-admits string-to-code, so the flag has to be verified against the running app, not copied from another role. |
|
|
| `min-storage-headroom` | same line OR comment block above key | [test_min_storage_headroom.py](../../../../tests/external/update/docker/test_min_storage_headroom.py) | Allows a `roles/*/meta/services.yml` `min_storage` to exceed the measured compressed size of its own `image:`/`version:` by more than 1 GB. MUST be paired with a `Reason:` on the same comment line naming what occupies the space. Reserved for services whose data volume genuinely dominates the footprint; the default path is to lower the value, because an over-declared budget makes a host look too small and prunes a cache the deploy did not need to lose. |
|
|
| `healthcheck-binary` | same or above | [test_healthcheck_binary_on_slim_base.py](../../../../tests/lint/ansible/roles/meta/test_healthcheck_binary_on_slim_base.py) | Exempts a `roles/*/meta/services.yml` healthcheck flavor from the requirement that the role's `files/Dockerfile` install the binary the probe runs, when the service sits on a `-slim` image. MUST name where the binary comes from instead. A `-slim` tag carries the runtime and nothing around it, so `curl` is absent and the probe fails from the first beat; under swarm the task is then reaped with exit 137 and every deployment holding the service in its closure fails on the convergence wait, naming the service rather than the missing binary. |
|
|
| `galaxy-git-parity` | same or above | [test_galaxy_entries_have_git_source.py](../../../../tests/lint/repository/dependencies/test_galaxy_entries_have_git_source.py) | Exempts a `requirements/requirements.galaxy.yml` collection from the requirement to also carry a `type: git` entry in `requirements/requirements.git.yml`. Reserved for collections that publish no usable git remote; without the git entry a `galaxy.ansible.com` outage fails `make install`, because the fallback in `scripts/install/ansible.sh` cannot resolve the collection either. |
|
|
| `loop-var-shadow` | same or above | [test_loop_var_shadowing.py](../../../../tests/lint/ansible/tasks/test_loop_var_shadowing.py) | Allows a `loop_var` to take the name of a variable declared in `group_vars/**` or in a role's `vars`/`defaults`. Inside the loop that name carries the item, so every lookup, template and nested include reading it sees the item instead, and ansible only warns. Reserved for a loop whose body reads the name as the item on purpose, typically because the item IS that value: state which meaning the body relies on. |
|
|
| `firewall-fragment-accept` | same or above | [test_firewall_fragment_verdicts.py](../../../../tests/lint/ansible/jinja/test_firewall_fragment_verdicts.py) | Allows a rule in a `roles/*/templates/nftables.conf.j2` fragment to end in an `accept` verdict. Default path: express the intent as a `drop` of what must not pass, or put the rule in the chain carrying the policy it has to overrule, because an `accept` terminates only its own base chain and every other chain at that hook still runs, docker's `drop` policy on forwarded packets included. Reserved for an accept whose only job is to skip the fragment's own later rules. |
|
|
| `shell-template` | head (first 30 lines) | [test_no_shell_templates.py](../../../../tests/lint/filesystem/shell/test_no_shell_templates.py) | Allows a `*.sh.j2` to exist. Default path: ship the script as a plain `.sh` under `files/shell/` and pass its values as `ExecStart` arguments or `Environment=` entries, because shellcheck, the repository's `.sh` guards and every test skip a Jinja template, so nothing reads what actually runs on the host. Reserved for a script whose shape is generated, not just its values. Place the marker BELOW the shebang, never above it, and prefer the Jinja comment form so it does not reach the rendered script. |
|
|
| `systemd-via-sys-service` | same or above | [test_systemd_units_via_sys_service.py](../../../../tests/lint/ansible/modules/test_systemd_units_via_sys_service.py) | Allows a role task to render a `*.service.j2` itself instead of handing the unit to `sys-service`. Default path: name the template `templates/systemctl.service.j2` and include `sys-service` with `system_service_id`, so the unit also gets the daemon reload, the enable, and the timer, async and flush behaviour. Reserved for units `sys-service` cannot carry, such as a user-scope unit under `$HOME`. |
|
|
| `software-identity` | same or above | [test_software_identity_variables.py](../../../../tests/lint/ansible/jinja/test_software_identity_variables.py) | Allows a `.j2` template to write the software's name, domain or URL as a literal. Default path: render `SOFTWARE_NAME`, `SOFTWARE_DOMAIN`, `SOFTWARE_URL` or `ORGANIZATION`, which `group_vars/all/00_general.yml` declares once and derives from each other, so a rename reaches the template too. Reserved for a literal that must survive a rename, such as an identifier a third party issued under the old name. |
|
|
|
|
## Tool-Native `noqa` Codes 🧹
|
|
|
|
Some Python files use Ruff-compatible uppercase `# noqa` codes. Ruff and related Python linters handle these, not [suppress.py](../../../../utils/annotations/suppress.py). They MAY appear when the local exception is intentional and clear from context.
|
|
|
|
| Code | Tool meaning | Typical allowed use |
|
|
| -------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
| `BLE001` | Blind exception caught | Narrow command-wrapper boundaries that convert any underlying failure into a domain-specific error. |
|
|
| `E402` | Module import not at top | Tests that MUST adjust `sys.path` or load fixtures before importing the module under test. |
|
|
| `E731` | Lambda assigned to variable | Small test-local callbacks or path formatters where a named function would add noise. |
|
|
| `F401` | Imported but unused | Re-export modules or import-probe tests where import success is the assertion. |
|
|
| `SLF001` | Private member access | Test-only introspection of parser or object internals when the public surface does not expose the needed state. |
|
|
|
|
## Adding a new rule 🆕
|
|
|
|
1. Pick a kebab-case `<rule>` identifier.
|
|
2. Decide which position semantic from the list above fits the check.
|
|
3. Use `is_suppressed_at` / `is_suppressed_in_head` / `is_suppressed_anywhere` from [suppress.py](../../../../utils/annotations/suppress.py) in your test. Do NOT roll a new regex.
|
|
4. Add the rule to the catalog table on this page.
|