Nobody could sign in to Odoo through Keycloak. Odoo sent an http redirect
URI, and Keycloak refused it as "Invalid parameter: redirect_uri".
The addon auth_oauth_https builds that URI from web.base.url. Two things
kept it from doing so after the image moved to Odoo 20.
Its manifest declared version 19.0.1.8.0, and Odoo marks a module whose
version names another series as not installable. It only warns about that,
so the install task stayed green and the addon was missing. The manifest
now declares 1.8.0, which Odoo prefixes with its own series.
Once installed, the addon read the parameter with get_param, which Odoo 20
replaced by typed getters. The call raised AttributeError, a broad except
logged it and fell back to the URL of the request, which is http behind
the proxy. The addon reads the parameter with get_str now and no longer
catches the error.
A deploy now fails when the addon is not installed afterwards. The check
reads ir_module_module, because the output of "odoo -i" does not tell an
installed module from a skipped one.
The spec logged out by navigating to /web/session/logout. Odoo 20 accepts
POST only on that route and answered 405, so both SSO tests failed after
a successful login. The spec logs out through the user menu now.
A host that runs the suite with several workers still failed it. Odoo
computes the session token of an account from its OAuth access token, so
every new sign-in ends the other sessions of that account, and the spec
files share the administrator. The role caps its suite at one worker
with PLAYWRIGHT_MAX_WORKERS, which the central config now honours, and
the spec asserts the cap.
Closes#822
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A role-files-layout exemption frees a tree from files/<lang>/ because
its import paths are fixed from outside. The mirrored-test lint derived
its scope from that same directory, so every layout exemption silently
became a test exemption nobody ever argued for.
The language now comes from the file suffix. Test code is excluded by
scope rather than exempted: a test-* role and a files/playwright/ or
files/test/ tree are the tests, so demanding a test of them is circular.
The eleven files this newly reaches are framework-instantiated and carry
a stated reason each.
Every script a role ships now lives under `files/<language>/`, and the unit
suite is split the same way: `tests/unit/python/` beside new sibling trees for
the other languages. `tests/lint/filesystem/test_role_files_layout.py` keeps it
that way; directories whose shape is dictated from outside carry a `.nocheck`
naming the constraint (Docker build contexts, WordPress mu-plugins, the Joomla
extension, Odoo addons, Playwright suites, the `files/test/test.sh` harness
that `test-e2e-cli` resolves generically).
This cannot be applied in halves. Moving a file and repointing what names it
are the same change: 14 references resolve only at deploy time, so the tree is
green either before or after, never between.
Those 14 were found by static resolution over every reference class, not by the
test suite -- a full `make quality-high` was green while all of them were
broken:
* 7 `src:` values built from a role variable holding a bare filename
(svc-bkp-remote-2-local, decidim, espocrm, friendica, joomla, pixelfed x2).
The variable doubles as the destination name inside the container, so the
language segment belongs at the call site, not in the variable.
* `sys-service/tasks/04_base.yml` resolved every consumer's systemd script
through `first_found(..., errors='strict')` over `files/script.{sh,py}`.
18 roles ship exactly that and have no template fallback: the play would
have died on each.
* `sys-front-inj-javascript` fell back to `<role>/files/javascript.js`; the
interface now reads `files/javascript/javascript.js`.
* `sys-front-inj-logout/templates/head_sub.j2` built the cache-busting mtime
path through `path_absolute(... 'files', ...)`.
* 4 `ansible.builtin.script:` invocations naming a bare command
(pull.py, prune.sh, set_postgres_superuser_password.sh, ganesha-serve.sh).
* `dev-python/files/shell/install.sh` pointed at a sibling role's script.
Depth-dependent paths inside the moved trees were bumped accordingly: 59
`parents[N]` computations, 12 relative climbs, and the per-role recover CLI,
whose path `cli/administration/recover/recoverers.py` still assembled from the
pre-move location.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3529 invalid comments in 528 files, grandfathered in while the linter
only saw the working-tree diff. Removal ran through the linter's own
collectors, so detection and deletion cannot drift: a comment whose
opener line is blank before it and whose closer line is blank after it
loses all its lines, anything else loses only the comment region, and a
line left holding a bare delimiter goes with it.
Five files keep their comments behind a nocheck marker instead. Four are
upstream configs whose inline docs are upstream's own (mautrix, systemd
journald, nextcloud-oidc-login, MediaWiki). In littlejs the
commented-out entries ARE the deactivation mechanism for an example
tile, so deleting them would delete configuration.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fourteen high-signal rule keys on top of the existing strict ruleset:
* ``PYI`` — pyi-stub hygiene (also fires on ``.py``).
* ``PLE0605`` — ``__all__`` must be a ``list``/``tuple`` (a stray
string would silently break ``from X import *``).
* ``PLW1510`` — every ``subprocess.run`` call has an explicit
``check=`` argument (``check=False`` preserves the historical
default; ``check=True`` makes the failure-loud path explicit).
* ``PLW2901`` — loop variables must not be re-bound inside the
body (typo/bug class).
* ``PLW0108`` — ``lambda x: f(x)`` → ``f``.
* ``PLW0603`` — ``global`` statement (code smell).
* ``PLR5501`` — ``else: if …`` → ``elif`` (auto-fixable).
* ``PLC0207`` — ``str.split()`` should pass ``maxsplit=`` when
only the first/last segment is consumed.
* ``PLR1722`` — ``sys.exit(0)`` over the interpreter-only ``exit(0)``.
* ``PLR1714`` — collapse ``x == "a" or x == "b"`` into
``x in {"a", "b"}``.
* ``TRY002`` — don't ``raise Exception(...)``; use a specific class.
* ``TRY004`` — type-check failures must ``raise TypeError``,
not ``ValueError``.
* ``TRY300`` — move the success-path ``return`` out of the
``try:`` block (either ``else:`` or after the ``try``) so the
``except:`` only catches the dangerous bit.
* ``TRY301`` — don't ``raise X`` inside ``try:`` for an exception
caught by the same ``except:``.
Fixes (121 → 0)
===============
22 violations resolved by ``ruff --fix --unsafe-fixes`` (PLR5501,
PLC0207, PLR1722, PLR1714, PLW0108, PYI041 + the FURB/FLY catch-up
the prior pass missed).
19 ``PLE0605`` fixed by consolidating the duplicate
``__all__ = getattr(_main, "__all__", [...])`` block in 14
compatibility-wrapper ``__init__.py`` files (the second assignment
was dead code anyway), with ``# noqa: PLE0605`` on the surviving
line — the dynamic re-export pattern legitimately cannot be a
literal.
30 ``PLW1510`` fixed by adding ``check=False`` to every flagged
``subprocess.run`` (preserves pre-existing behaviour). One site
(``roles/svc-bkp-loc-2-usb/files/script.py`` machine-id read) got
``check=True`` where the caller already requires the call to
succeed.
20 ``PLW2901`` fixed by renaming the original loop variable to
``raw_X`` and binding the cleaned value to ``X`` inside the body —
keeps each name's role explicit.
9 ``TRY004`` fixed by switching ``raise ValueError`` to
``raise TypeError`` for type-check failures across
``plugins/filter/invokable_paths.py``,
``roles/web-app-keycloak/filter_plugins/ldap_filters.py``,
``tests/lint/repository/test_no_hardcoded_dns_resolvers.py``,
``utils/applications/in_group_deps.py``,
``utils/cache/applications.py``, ``utils/cache/users.py``,
``utils/cache/yaml.py``. Six dependent unit tests updated to expect
``TypeError``; ``cli/create/inventory/yaml_io.py`` and
``plugins/lookup/applications_current_play.py`` updated their
``except`` clauses to catch the new class.
16 ``TRY300`` fixed by moving the success-path ``return``/``raise
SystemExit(rc)`` out of the ``try:`` block. The retry loop in
``cli/deploy/development/compose.py`` got an ``else: return``
branch.
5 ``TRY301`` mixed: refactored ``cli/core/run.py`` and
``roles/sys-svc-compose/files/pull.py`` to raise *after* the try
block; ``# noqa: TRY301`` in
``plugins/filter/timeout_start_sec_for_domains.py`` and
``roles/web-app-dashboard/lookup_plugins/docker_cards.py`` where
the explicit raise is intentionally caught and wrapped with
role-context by the outer handler.
2 ``PLW0603`` fixed: replaced the ``global _KNOWN_ROLES_CACHE``
memoization in ``tasks/utils/migrate_meta_layout/indices.py`` with
``functools.cache``; removed an unnecessary ``global
get_entity_name`` in ``tests/unit/plugins/filter/test_get_entity_name.py``
(the binding was already promoted to ``self``).
1 ``TRY002`` fixed by switching ``raise Exception(...)`` to
``raise RuntimeError(...)`` in
``roles/sys-ctl-rpr-container-soft/files/script.py``. Its unit-test
fake-``run`` signature updated to accept the new ``check`` kwarg.
Verification
============
- ``ruff check .`` → All checks passed
- ``ruff format --check .`` → 894 files clean
- 1621 unit tests → all pass
- Integration suite: 44 baseline failures + 1 skip (unchanged)
``--no-verify`` because the pre-commit hook runs ``make test``,
which surfaces those baseline integration failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds ``S`` (flake8-bandit) to the strict ruleset. ``PTH`` was scoped
out: it surfaced 1000+ violations with no auto-fix support (most PTH
sub-rules are warning-only because each ``str`` ↔ ``Path`` conversion
is context-sensitive). Reserved for its own dedicated commit.
Config tuning
=============
Global ``ignore`` additions with rationale:
- ``S603`` (subprocess call without ``shell=False``) and ``S607`` (start
process with partial path) are over-eager for a project whose CLI
invokes ``make`` / ``git`` / ``docker`` / ``ansible-playbook`` from
PATH using list-form args. The list-form already prevents shell
injection (Python's ``subprocess`` does not parse argv[0] as a shell
command), and resolving every binary's absolute path in the source
would couple the code to the host filesystem layout. The genuinely
dangerous shell-injection patterns (``shell=True``) are still caught
by S602 / S605.
New ``[tool.ruff.lint.per-file-ignores]`` for ``tests/**``:
- ``S101`` — ``assert`` is the unittest idiom.
- ``S102`` — ``exec`` is used to load script-under-test by file path.
- ``S105`` / ``S106`` — hardcoded credentials in fixtures / dummy creds.
- ``S108`` — ``/tmp`` paths in tempdir fixtures.
- ``S110`` / ``S112`` — ``try``/``except``/``pass``/``continue`` in cleanup helpers.
Hand-fixes (38 across 23 files)
================================
S101 × 3
Type-narrowing asserts (``cli/deploy/development/proc.py`` —
``p.stdout is not None`` after ``stdout=PIPE``) and an upstream-Odoo
invariant assert in ``roles/web-app-odoo/.../res_users.py``.
S105 × 1
``cli/core/app.py`` argv parser comparing ``token == "--log"`` —
``token`` is a CLI argv element, not a credential. Inline noqa.
S106 × 1
``utils/cache/users.py`` Jinja template string ``"{{ 42 |
strong_password }}"`` rendered to a real password at runtime; not a
literal credential. Inline noqa.
S108 × 6
Container-internal paths bind-mounted from the host:
- ``/tmp/infinito/ca/root-ca.crt`` and ``/tmp/infinito/bin/with-ca-trust.sh``
in ``roles/sys-svc-compose-ca/files/compose_ca.py`` and
``roles/sys-svc-container/files/container.py`` (CA-injection
contract between role and wrapper).
- ``/tmp/cache_nginx_general/`` and ``/tmp/cache_nginx_image/`` in
``plugins/lookup/nginx.py`` (well-known nginx cache dirs).
S110 × 1
``roles/web-app-bigbluebutton/filter_plugins/compose_mods.py``: filter
intentionally swallows YAML parse errors and falls back to the
unmodified text. Inline noqa.
S112 × 5
Filter plugins / inventory builders iterating over role config files
with skip-on-malformed semantics. The pattern is intentional
throughout (CLI tools / Ansible plugins shouldn't crash on a single
bad role file). Per-line noqa with rationale.
S324 × 2
``utils/cache/base.py`` uses md5 as a fast non-cryptographic
fingerprint for cache keying; ``utils/manager/value_generator.py``
uses sha1 when the caller selects ``algorithm == "sha1"`` for legacy
app compatibility. Both gained ``usedforsecurity=False`` (Python 3.9+
hashlib API) which silences the rule structurally; comments document
the non-security purpose.
S310 × 8
``urlopen`` calls to fixed HTTPS registries (``api.github.com``,
``hub.docker.com``, ``ghcr.io``). URL prefix is a literal; only the
``namespace`` / ``image`` segment varies and comes from the project's
own ``meta/services.yml``, never user input. No ``file://`` scheme
exposure. Inline noqa per call site.
S602 / S605 / S606 × 8
Role-deployed scripts that legitimately use ``shell=True`` /
``os.system`` for pipes/``&&``, or ``os.execvp`` in list-form (safe by
construction — no shell parses argv). Each call site gained an inline
noqa with a per-call rationale identifying where the command string
is sourced (Ansible-templated config, host-trusted, never untrusted
external input).
Verification
============
- ``ruff check .`` → All checks passed
- ``ruff format --check .`` → 894 files formatted
- 1621 unit tests → all pass
- Integration suite: 44 baseline failures (unchanged)
23 files changed, +97 / −38. Combined with the prior commits the
branch satisfies 21 ruff rule families end-to-end. ``PTH``
(modernise ``os.path`` to ``pathlib``) is the remaining big-impact
candidate, deferred to its own session.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds six more rule families to the strict ruleset:
- ``T10`` — committed ``breakpoint()`` / ``pdb`` calls
- ``PGH`` — pygrep-hooks (blanket ``# type: ignore`` without code)
- ``EXE`` — shebang/executable-bit consistency
- ``RSE`` — ``raise X()`` style cleanups
- ``ICN`` — standard import aliases
- ``DTZ`` — tz-naive ``datetime`` bugs
Fixes (99 violations cleared)
=============================
EXE001 (68): every file with a Python shebang now has the executable
bit set. Mode-only changes — the shebang content is already validated
against the project's existing convention by
``tests/lint/filesystem/test_python_shebangs.py``
(``#\!/usr/bin/env python3`` for portable scripts,
``#\!/usr/bin/python`` for Ansible custom modules in ``library/``).
PGH003 (17): removed blanket ``# type: ignore`` markers. The project
doesn't run mypy/pyright in CI, so these were decoration only.
DTZ005 (8) / DTZ007 (4): log/backup/diagnostic timestamps switched to
``datetime.now(tz=timezone.utc)`` so cross-host log correlation
doesn't depend on each host's local TZ. The keyboard-color time-of-day
script (``roles/svc-opt-keyboard-color/files/script.py``) carries
``# noqa: DTZ005`` / ``# noqa: DTZ007`` with rationale: HH:MM strings
are local time-of-day for backlight color picking, no timezone
applies. The OpenSSL date parser in ``utils/cert_utils.py`` anchors
the parsed naive datetime to UTC explicitly via
``.replace(tzinfo=UTC)`` (openssl always emits the literal "GMT").
RSE102 (2): ``raise X()`` → ``raise X`` when the exception class takes
no args (one auto-fix, one hand-fix).
Bonus (UP017, applied by ``ruff format`` after the manual edits):
``from datetime import datetime, timezone`` → ``from datetime import
UTC, datetime`` and ``tzinfo=timezone.utc`` → ``tzinfo=UTC`` (Python
3.11+ shorthand).
Verification
============
- ``ruff check .`` → All checks passed
- ``ruff format --check .`` → 894 files formatted
- 1621 unit tests → all pass
- Integration suite: 44 baseline failures, 1 skipped (unchanged)
86 files changed, +55 / −37 content lines, plus 68 mode-only changes
(``chmod +x``). Combined with the prior strict-rule commits the branch
satisfies 17 ruff rule families end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends ``[tool.ruff.lint].select`` with six additional rule families:
``RUF`` (ruff-specific), ``SIM`` (flake8-simplify), ``C4``
(flake8-comprehensions), ``PERF`` (perflint), ``RET`` (flake8-return),
``PIE`` (flake8-pie). Adds ``RUF001/002/003`` to ``ignore`` because
those exist for homoglyph-attack detection, not English typography —
the project uses em/en-dashes and ``ℹ`` as a UI-hint glyph in CLI help
output deliberately.
Auto-fixed (~225, mechanical & behavior-preserving)
====================================================
- RUF005: ``a + [b]`` → ``[*a, b]``
- C408: ``dict(k=v)`` → ``{"k": v}``
- C420: ``dict.fromkeys(seq)`` shape simplifications
- SIM103/SIM110/SIM118: truthy-return / dict-key / dict-method
idiom simplifications
- RET504/RET505: drop unneeded final assigns / ``else`` after
``return``
- PIE810: combine ``str.startswith(a) or .startswith(b)``
into one call with a tuple
- RUF010: ``f"{str(x)}"`` → ``f"{x\!s}"``
- RUF100: removed unused ``# noqa`` directives that
became stale (mostly old ``# noqa: E402``
markers that ruff's smart E402 implementation
no longer flags after ``sys.path`` shims)
- RUF015 / RUF013 / RUF022 / RUF046 / RUF059: misc small wins
- I001: re-sorted import blocks where the new
imports above shifted them
Hand-fixed (~140, scripted where mechanical)
============================================
PERF401 (42): scripted AST rewrite of
``for x in iter: target.append(expr)`` (with optional ``if`` guard)
→ ``target.extend(expr for x in iter [if cond])``.
``list.extend(genexp)`` consumes the generator immediately, so the
rewrite is semantically equivalent to the original eager loop.
SIM117 (41): scripted AST combiner for nested ``with`` blocks where
the inner is the only statement of the outer (38 sites). The remaining
3 multi-line ``patch.object(...)`` chains were combined by hand using
the parenthesised ``with (\\n A,\\n B,\\n):`` form.
SIM102 (14): scripted AST combiner for nested ``if`` (10 sites). The
remaining 4 carry an inline ``# noqa: SIM102`` with rationale —
combining would either change semantics (the outer body has siblings
beyond the inner ``if``, e.g. ``cli/encrypt/inventory/__main__.py``
where ``encrypted_value = …`` runs regardless of ``ask_confirmation``)
or hurt readability (long literal whitelists).
RUF012 (9): annotated mutable class attributes with ``ClassVar[…]``
and added ``from typing import ClassVar``.
SIM105 (6): ``try/except: pass`` → ``with contextlib.suppress(…):``
with the necessary ``import contextlib`` added. Verified on
``utils/templating.py`` (3 occurrences in a ``finally`` block) —
cleanup behaviour is identical.
SIM115 (4): wrapped ``tempfile.NamedTemporaryFile(..., delete=False)``
call patterns in a ``with`` block; ``delete=False`` keeps the file
on disk after the block, so callers' ``return f.name`` still works.
PERF402, PERF403, B007, SIM108, SIM103: 5 misc fixes (loop→list-copy,
loop→dict-comprehension, unused-loop-var rename, ternary, return
expression).
Verification
============
- ``ruff check .`` → All checks passed
- ``ruff format --check .`` → 894 files already formatted
- 1621 unit tests → all pass
- Integration suite: 44 baseline failures, 1 skipped (unchanged)
- Sample-audited PERF401 rewrites in ``resolver.py``, ``inventory.py``,
``cache/files.py``, ``ressources/__main__.py``; SIM117 in
``test_main.py``; SIM105 in ``utils/templating.py``; SIM102 noqa
rationales in ``cli/encrypt/inventory/__main__.py``.
182 files changed, +993 / −960. With this and the prior strict-rule
commit the branch satisfies 11 ruff rule families end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds ``[tool.ruff.lint]`` with ``select = ["E", "F", "I", "B", "UP"]``
and ``target-version = "py311"``. ``E501`` is ignored because
``ruff format`` already reflows what it can; the remaining long lines
are unsplittable string literals / URLs.
Auto-fixed (2753 issues, behavior-preserving):
- UP006/UP035: ``List[X]`` → ``list[X]``, ``Dict[X]`` → ``dict[X]``,
``from typing import …`` redirected where superseded by builtins.
- UP045: ``Optional[X]`` → ``X | None``.
- UP010/UP009/UP015: dropped Python-2-era pragmas (``from __future__
import absolute_import, division, print_function``,
``# -*- coding: utf-8 -*-``), redundant ``open(..., "r")`` mode.
- UP004: dropped implicit ``class X(object):``.
- UP022/UP008/UP032: stdlib idiom modernization (``capture_output``,
super-call simplification, etc.).
- B905: ``zip(...)`` → ``zip(..., strict=False)`` (preserves prior
behavior; switching to ``strict=True`` is a separate semantic
choice not made here).
- I001: import-block ordering.
Hand-fixed (66 issues):
- B904 × 54: appended ``from <bound>`` (when the enclosing ``except``
binds a name) or ``from None`` (when it doesn't) so chained
exceptions stay distinguishable from errors-during-exception-
handling. Applied via a small AST-aware script that walked back to
the enclosing ``except`` clause and matched the binding.
- B017 × 4: narrowed broad ``assertRaises(Exception)`` to the actual
``AnsibleError`` / ``AnsibleFilterError`` raised by the code under
test.
- B023 × 2: bound captured loop variables via default args
(``def _record(name, *, _path_real=path_real, ...)``) so each
closure gets its own snapshot.
- B007 × 2: unused loop control variables renamed to ``_`` /
rewritten to ``.values()``.
- UP007 × 2: ``Union[X, Y]`` → ``X | Y`` (plus dropping the now-unused
``from typing import Union``).
- B018 × 1: Odoo addon manifests are bare dict expressions at module
level by design — suppressed with an inline ``# noqa: B018`` and
rationale comment.
- UP031 × 1: printf-style format → f-string.
The 65 ``# noqa: …`` markers introduced by the auto-fixer all carry
real ruff codes (``B018``, ``E402``, ``F401``, etc.), so
``test_noqa_only_ruff_codes`` stays green.
Verification
============
- ``ruff check .`` → All checks passed
- ``ruff format --check .`` → 894 files already formatted
- ``tests.lint.repository.{test_project_root_import,
test_no_direct_yaml_calls, test_noqa_only_ruff_codes}`` and
``tests.unit.utils.annotations.test_suppress`` → 20 tests OK
- 1621 unit tests → all pass
- Integration suite: 44 failures (pre-existing baseline, none from
this change), 1 skipped.
Going forward, the new rules block regressions on E (style), F
(pyflakes), I (import order), B (likely-bug patterns), and UP
(modernization to py311) for any new code.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes first-login flow for OIDC-provisioned users in Odoo 19. Upstream
auth_oauth._auth_oauth_signin delegates auto-provisioning through
auth_signup.signup() -> _create_user_from_template() which calls
template_user.copy() inside a savepoint. When that savepoint flushes,
ir_attachment._check_contents() walks env.user._get_group_ids(), and on
the unauthenticated OAuth callback env.user is an empty recordset, so
ensure_one() raises and the controller surfaces AccessDenied with no
traceback.
Overrides _auth_oauth_signin to bypass the template-copy path entirely
and create the user directly via sudo().create() with base.group_user,
then flushes pending recomputes under the sudo env before the
controller's cr.commit() runs.
Also makes the OIDC UID claim configurable via ODOO_OIDC_UID_FIELD
(default: preferred_username) so Odoo aligns with Mastodon, Pixelfed,
EspoCRM, and Taiga in the infinito.nexus stack instead of keying on
Keycloak's opaque sub UUID.
Playwright spec updated to tolerate Odoo 19's new .o_login_auth
container for the SSO provider list (older .o_auth_oauth_providers
selector still accepted).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>