Reason: ruff parses every `# noqa: <code>` marker as a flake8 directive and warns on every non-flake8 rule key. Every catalog rule in `docs/contributing/actions/testing/suppression.md` is project-specific (kebab-case), so `# noqa:` markers fired the warning on every parse. `# nocheck:` is the project synonym ruff ignores. Scope: - 41 `roles/*/meta/services.yml` + 1 task + 1 .cfg.j2 template: pure noqa→nocheck swap for catalog rules. Mixed flake8 markers (`# noqa: E402, X`) preserved verbatim. - 5 production .py + 2 production scripts: same swap. - All catalog-rule lints' docstrings updated; redundant "or # nocheck:" parentheticals collapsed to a single nocheck form. - `utils/annotations/suppress.py`: docstring rewritten to spell out the new convention. Parser still accepts both keywords as synonyms. - `docs/contributing/actions/testing/suppression.md`: convention rewritten to mandate `nocheck:`; missing `domain-spot` and `literal-protocol-lookup` rows added; "enforced by" pointer added. New guard: - `tests/lint/repository/test_noqa_only_ruff_codes.py`: meta-lint failing any `# noqa: <code>` whose codes don't match `[A-Z]+\d+` (real ruff/flake8 shape). Allowlists the impl, its unit test, the catalog page, the direct-yaml lint, and itself. - `tests/lint/repository/test_no_direct_yaml_calls.py`: hardened to reject the `noqa:` form via inlined regex (no longer routed through `utils.annotations.suppress`). Tangential cleanups carried in this same commit: - `utils/cache/yaml.py`, `utils/handler/yaml.py`: 6 `direct-yaml` markers swapped. - `utils/cache/base.py`: `from . import PROJECT_ROOT, ROLES_DIR` moved up to the import block (E402). - `cli/create/role/__init__.py`: reordered so PROJECT_ROOT is defined before `__main__` is imported (also fixed a latent circular import). - PROJECT_ROOT migration tail: 2 new `__init__.py` modules + 10 test files swapped from `def repo_root()` to `from . import PROJECT_ROOT`. - ~20 ruff auto-fix removals of unused `from pathlib import Path`. Verification: - `ruff check .` clean. - 23 suppression-related tests pass. - The new lint detects all three regression shapes (project rule alone, mixed with real ruff code, lowercase-only). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
16 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.
- 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). |
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. |
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_services_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'). |
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)
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
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.