Move five role-scoped lint tests out of the catch-all
tests/lint/repository/ tree into the ansible/roles/ tree where
they conceptually belong, dropping the redundant `role_` /
`meta_` filename prefixes:
test_role_file_spot.py → roles/test_file_spot.py
test_role_readme.py → roles/test_readme.py
test_role_meta_info.py → roles/meta/test_info.py
test_role_meta_layout.py → roles/meta/test_layout.py
test_role_meta_main_galaxy_schema → roles/meta/test_main_galaxy_schema.py
Plus regroup two web-app-only Playwright tests under
roles/web-app/playwright/ so the path itself states the scope:
test_playwright_env_keys_used → roles/web-app/playwright/test_env_keys_used.py
test_web_app_roles_have_playwright_spec
→ roles/web-app/playwright/test_has_spec.py
New __init__.py files set PROJECT_ROOT for the new package depths.
External references (suppression catalog entry, design doc,
web-app-joomla README, two integration-test docstrings) updated
to the new paths. Stale path strings in legacy requirement docs
left untouched per the snapshot policy from req 019.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
21 KiB
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>)*
Rules:
noqaandnocheckare accepted as synonyms by the parser, but by repo convention every project rule MUST usenocheck:. Reason:# noqa: <code>is also parsed by ruff as a flake8 directive, and ruff warns on non-flake8 rule keys (every key in this catalog is project-specific, so the warning fires on every marker).nocheck:is the project-specific keyword ruff ignores. Reservenoqa:for real flake8 / ruff codes (E402,F401, …). The convention is enforced by test_noqa_only_ruff_codes.py, which fails any# noqa: <code>whose codes don't match the ruff/flake8 shape[A-Z]+\d+.- The keyword is matched case-insensitively.
<rule>is a kebab-case identifier from the catalog below.- Multiple rules MAY be combined on one comment, comma-separated:
# nocheck: shared, email.
Accepted comment prefixes (so the marker fits any file format that the test scans):
| Prefix | Use in |
|---|---|
# |
Python, YAML, shell, conf, INI, … |
// |
JS, JSONC, … |
{# … #} |
Jinja2 templates |
<!-- … --> |
HTML, Markdown |
Implementation: utils/annotations/suppress.py.
Position semantics 📍
The placement rule is per check. The catalog column "Position" uses these labels:
- same line: the marker MUST be on the same line as the construct it suppresses.
- same line / span: the marker MUST be on the construct's own line or, for multi-line call expressions, any physical line spanned by that call.
- line above: the marker MUST be on the immediately preceding non-empty line. Blank lines between the marker and the construct break the association.
- same or above: either of the above is accepted.
- comment block above: the marker may sit on any line in a contiguous comment block above the construct (no blank line between marker and construct).
- head (first 30 lines): the marker MAY appear anywhere in the first 30 lines of the file. Used for file-level opt-outs that should stay visible at the top of the file.
- anywhere: the marker may appear on any line of the file. Used for whole-file opt-outs whose semantics legitimately apply to the entire file.
Catalog 📚
| Rule | Position | Affected test | Effect |
|---|---|---|---|
url |
same or above | test_urls_reachable.py | Skips probing the literal HTTP(S) URL. Use for CDN roots and API base URLs that return 4xx without a path. |
docker-version |
line above | test_image_versions.py | Skips the live version-update warning for that image's version: key. |
direct-yaml |
same line / span | test_no_direct_yaml_calls.py | Allows the marked yaml.safe_load / safe_dump call to bypass utils.cache.yaml. This rule's checker hard-rejects noqa: and accepts only nocheck: (ruff parses noqa: as flake8 and warns on non-flake8 codes). |
redundant-bool-ternary |
same line | test_no_redundant_bool_patterns.py | Allows the redundant | bool | ternary('true','false') pattern on the marked line, only when a consumer really requires the lowercase true/false tokens. |
redundant-bool-comparison |
same line | test_no_redundant_bool_patterns.py | Allows ... | bool == True/False/true/false on the marked line, only when a real tri-state guard is intended (e.g. distinguishing None from False). |
shared |
comment block above | test_service_shared_consistency.py | Marks a service whose enabled: true legitimately does not require shared: true. |
email |
line above | test_email.py | Suppresses the missing-email-integration warning when paired with enabled: false and shared: false. |
logout |
line above | test_logout_dashboard.py | Opts a web-app-* role out of the universal-logout integration. Marker MUST be on the line directly above logout:, paired with enabled: false and shared: false. |
dashboard |
line above | test_logout_dashboard.py | Opts a web-app-* role out of the dashboard tile integration. Same shape as logout. |
oidc |
line above | test_sso.py | Opts the role's native OIDC path out (so that oauth2 must take over, OR, if oauth2 is also opted out, the role legitimately has no login flow). Same shape as email. |
oauth2 |
line above | test_sso.py | Opts the role's oauth2-proxy path out. In combination with # nocheck: oidc it declares "this role has no login flow at all". Same shape as email. |
file-size |
head (first 30 lines) | test_python_file_size.py | Opts the entire .py file out of the 500-line cap. |
project-walk |
same line / span | test_no_raw_project_walk.py | Allows a raw Path.rglob, os.walk, or glob.glob call when the walk is intentionally outside the cached project-tree helper contract. |
cache-read |
same line / span | test_no_uncached_file_reads.py | Allows a raw Path.read_text / Path.read_bytes call in a tests/lint/ test when the read is intentionally outside the LRU-cached utils.cache.files.read_text contract. |
run-once |
anywhere | test_run_once_tags.py, 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 | 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 | Marks a single line or a whole file under roles/ as legitimately calling docker / docker compose / docker-compose directly. The check is scoped to roles/; bootstrap scripts and CI workflows outside roles/ are not scanned and need no marker. |
hardcoded-dns-resolver |
same or above | test_no_hardcoded_dns_resolvers.py | Marks a line that legitimately needs a literal IP from NETWORK_PUBLIC_DNS_RESOLVERS at the substitution point (CoreDNS forward directives that don't run through Jinja, host-bootstrap shell scripts, documentation examples). The variable in group_vars/all/08_networks.yml is the SPOT for these IPs everywhere else. |
dynamic-flag |
same line (per-flag) OR comment block above key (whole block) | test_dynamic_flags.py | Marks a roles/*/meta/services.yml flag whose value legitimately stays literal. Per-flag (same line) is the typical shape for databases (enabled: true literal, shared still dynamic). Block-level (comment block above the service key) is for entries where both flags stay literal (e.g. css). |
lookup-config-path |
same or above | tests/integration/lookups/config/ (literal / variable / wildcard / role-local) | Skips a single lookup('config', …) call from every path-validation pass. Use when the call legitimately resolves at runtime against state that is NOT visible to the static scan, typically a self-referential role like web-app-oauth2-proxy reading services.oauth2.* keys that other roles publish but its own meta/services.yml does not. Note: the role-local pass already ignores any call whose app argument is NOT the literal application_id (e.g. _BBB_COTURN_ROLE, oauth2_proxy_application_id), so cross-role lookups need no marker for that pass. Mark a call only when the literal / variable / wildcard pass complains. |
unused-var |
same or above | 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 in some .yml / .yaml / .j2" check. Use when the var is consumed somewhere the lint cannot see (Python plugin code, an external tool reading the rendered Ansible inventory, set_fact chains driven by computed names, etc.). Do NOT use to silence a legitimately dead var; prune the declaration instead. |
project-root-import |
same or above | tests/lint/repository/test_project_root_import.py | Permits a local PROJECT_ROOT / parents[N] / os.pardir chain on the marked line. Reserved for two cases: a __main__.py bootstrap shim that prepends the repo root to sys.path before any package import resolves, and a standalone script under roles/<role>/files/ that has no package container to import from. The marker MUST carry an inline comment that explains why the local computation is unavoidable. |
domain-spot |
same or above | tests/lint/repository/test_domain_primary_spot.py | Marks a line that legitimately uses a <word>.{{ DOMAIN_PRIMARY }} host literal outside the SPOT (roles/<role>/meta/server.yml / meta/variants.yml). Use only for legacy domain cleanups (e.g. removing stale nginx config for a renamed host) where the canonical SPOT cannot represent the historical name. |
literal-protocol-lookup |
same or above | tests/lint/repository/test_literal_protocol_with_lookup.py | Allows a hardcoded http:// / https:// literal next to a lookup(...) call. Reserved for legitimate internal cases where the protocol is genuinely fixed: Docker-network upstreams that always speak plaintext (http://<container>:<port>), loopback URLs in CI fixtures, and similar. Production-facing URLs MUST go through lookup('tls', …, 'protocols.web'). |
self-path-reference |
same line | tests/lint/repository/test_no_self_path_reference.py | Allows a file to contain its own relative path from the repo root as a string literal on the marked line. Reserved for cases where the self-reference is load-bearing (e.g. a static site generator that injects its own source path into the rendered output). Files at the repo root (no / in their relative path) are exempt without a marker. |
role-file-spot |
same or above | tests/lint/ansible/roles/test_file_spot.py | Allows a .py file to carry a literal role-file path (e.g. "meta/services.yml") on the marked line instead of the matching ROLE_FILE_* constant from utils.roles.mapping. Reserved for tests that assert the constant's literal value, migration scripts that operate on raw paths before the SPOT applies, or doc strings that embed the path. |
playwright-service-flag |
comment block above | tests/integration/roles/test_playwright_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. Use only when the service legitimately cannot or must not be gated from Playwright (e.g. requirement-006 self-gate exceptions where the role IS the provider, like web-app-keycloak for oidc). |
playwright-service-gate |
same or above | tests/integration/roles/test_playwright_spec_env_gates.py | Exempts a <NAME>_SERVICE_ENABLED= line in roles/<role>/templates/playwright.env.j2 from the "must be gated by requireService / skipUnlessServiceEnabled / isServiceEnabled / isServiceDisabledReason in files/playwright.spec.js" check. Use only when the flag legitimately exists for non-spec consumers but the spec has no scenario to gate. |
Examples 💡
url, on the line carrying the literal URL or on the line above it:
ensure_proxy_repo raw raw-packagist "$(... https://repo.packagist.org/ ...)" # nocheck: url
# nocheck: url
api_base_url: "https://example.org/v1"
docker-version, directly above a version: key:
service_x:
# nocheck: docker-version
version: "4.5"
direct-yaml, anywhere on the call's physical line or on any line spanned by the multi-line call. This rule MUST use nocheck: (not noqa:): ruff parses # noqa: <code> as a flake8 directive and warns about non-flake8 codes like direct-yaml.
data = yaml.safe_load(text) # nocheck: direct-yaml
shared, somewhere in the comment block immediately above a service key:
# This service is inherently role-local.
# nocheck: shared
my_local_service:
enabled: true
email, directly above the email: block, paired with both flags false:
# nocheck: email
email:
enabled: false
shared: false
file-size, at the top of a long Python file:
"""Module docstring."""
# nocheck: file-size (single-host orchestration script ships as one file)
project-walk, on the raw walk call or on any line spanned by the call:
for path in temp_dir.rglob("*"): # noqa: project-walk
...
Use project-walk only for scans that genuinely do not fit
utils.cache.files.iter_project_files, such as temporary directories,
.git metadata, or a deliberately tiny fixed path probe. Broad scans of the
repository tree SHOULD use the cached helpers instead.
cache-read, on the raw read call or on any line spanned by the call:
text = tmp_path.read_text() # noqa: cache-read (synthetic fixture written above)
Use cache-read only when the file legitimately does not belong to the project-tree LRU cache, e.g. a tempdir written and read inside the same test, a file outside the repository root, or an in-memory io.StringIO-backed Path. Reads of project files (under roles/, tasks/,
docs/, tests/, etc.) MUST go through
utils.cache.files.read_text so the LRU cache is shared across the
pytest session.
run-once, at the top of a role's tasks/main.yml:
# nocheck: run-once
- name: ...
run-once-suffix, on the exact when: list-item line:
when:
- run_once_svc_db_openldap is not defined # nocheck: run-once-suffix
raw-docker, per-line on a single role-task line that legitimately calls docker directly (e.g. a bootstrap step that runs before the container wrapper is on the path):
- name: "Bootstrap engine before the wrapper exists"
ansible.builtin.command: docker info # nocheck: raw-docker
raw-docker, file-level at the top of a role's bundled shell script that legitimately drives the engine directly:
#!/usr/bin/env bash
# nocheck: raw-docker. Runs from sys-svc-container's installer before the wrapper symlink lands.
docker buildx build ...
dynamic-flag, two placements depending on scope:
Per-flag (same line), for databases where enabled: true reflects a static fact about the role's needs but shared still resolves dynamically:
mariadb:
enabled: true # nocheck: dynamic-flag
shared: "{{ 'svc-db-mariadb' in group_names }}"
Block-level (comment block above the service key), for service entries whose both flags legitimately stay literal (e.g. css, javascript):
# nocheck: dynamic-flag
css:
enabled: true
shared: true
project-root-import, on the line that locally derives the repo root inside a __main__.py sys.path bootstrap (the marker line MUST explain why the local computation runs before any package import resolves), or inside a standalone script that has no package container to import from:
PROJECT_ROOT = Path(__file__).resolve().parents[1] # nocheck: project-root-import, sys.path bootstrap before package imports resolve
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
unused-var, on the var declaration line (or directly above it), when the var is consumed by Python plugin code or another non-YAML/Jinja surface that the static scan cannot reach:
# nocheck: unused-var # consumed by plugins/filter/<role>_render.py
ROLE_TEMPLATE_PATH: "templates/<role>/render.j2"
ROLE_TEMPLATE_PATH: "templates/<role>/render.j2" # nocheck: unused-var # consumed by plugins/filter/<role>_render.py
lookup-config-path, on the line carrying a lookup('config', …) call (or directly above it) when the path legitimately resolves against runtime-only state that the static scan cannot see, for example a self-referential role that consumes the services.<self>.* keys other roles publish about it:
# nocheck: lookup-config-path
upstreams = {{ lookup('config', application_id, 'services.oauth2.origin.host') }}
upstreams = {{ lookup('config', application_id, 'services.oauth2.origin.host') }} {# nocheck: lookup-config-path #}
hardcoded-dns-resolver, on the line that emits the literal IP, when the substitution point cannot consume NETWORK_PUBLIC_DNS_RESOLVERS (e.g. a CoreDNS forward directive in a non-Jinja template):
forward . 1.1.1.1 8.8.8.8 # nocheck: hardcoded-dns-resolver
Tool-Native noqa Codes 🧹
Some Python files also use Ruff-compatible uppercase # noqa codes. These codes are handled by Ruff and related Python linters, not by utils/annotations/suppress.py. They MAY appear in Python files when the local exception is intentional and clear from context.
| Code | Tool meaning | Typical allowed use in this repository |
|---|---|---|
BLE001 |
Blind exception caught | Narrow command-wrapper boundaries that must 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 🆕
- Pick a kebab-case
<rule>identifier. - Decide which position semantic from the list above fits the check.
- Use the
is_suppressed_at/is_suppressed_in_head/is_suppressed_anywherehelpers from utils/annotations/suppress.py in your test. Do NOT roll a new regex. - Add the rule to the catalog table on this page.