ruff 0.17.0 adds `datetime` -> `dt` to the default aliases of
flake8-import-conventions, and the project selects ICN, so lint-python
failed with ICN001 in seven files.
The files now import `datetime as dt`. ruff's unsafe fix also renamed
two local names in utils/github/playwright_summary.py into the alias:
the `_to_iso_z` parameter and the parsed value in
`_parse_iso_timestamp`. Both are now called `moment`; with the bare fix,
nine tests of test_playwright_summary.py failed with UnboundLocalError
and AttributeError.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The readiness module http_ready rendered insecure_skip_verify from
TLS_MODE and lookup('ca_injected', application_id) (#851). The lookup is
true only when Prometheus itself has a domain, TLS and the self-signed
mode, so in a self-signed deployment where Prometheus runs in another
mode the exporter had no CA file and skipped verification. All targets
share the module, so a target with a publicly trusted certificate was
probed without verification too, and anyone on the probe's path could
answer for a failed application.
http_ready now always verifies. A second module, http_ready_private_ca,
skips verification, and only the targets that need it select it: those
in the self-signed mode while the CA is not mounted into Prometheus,
through the __param_module label, which overrides the job's module for
that target. The default self-signed deployment mounts the CA and keeps
every target on the verifying module.
Verified on a local self-signed Tor round, where the CA is mounted:
the rendered blackbox.yml holds http_ready with insecure_skip_verify:
false and http_ready_private_ca with true, and no target of the scrape
configuration carries the module label. All targets of that round are
http onion names, so the https path itself ran only in the unit tests:
tests/unit/python/roles/web-app-prometheus pass, among them a publicly
trusted target that keeps the verifying module while the CA is missing,
a self-signed target that switches to the private-CA module, and the
verifying module rendering insecure_skip_verify: false without the CA.
Against the templates of main the three new cases fail.
Closes#851
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A local deploy stopped four times in two days in "Ensure Docker apt key
and sources are consistent" (#849). apt_repo.sh downloads the Docker GPG
key on every run, and when download.docker.com refused the connection,
curl's four attempts were over within about six seconds and the script
returned 1, although the key from an earlier run was installed and is
all the following apt operations need.
install_key now fails only when no key is installed. With a key in
place, a refused download prints a warning and keeps it. A downloaded
key that differs still replaces the installed one, and the 0644 mode is
enforced in every case. The keyring path became an argument:
ensure-key-and-sanitize passes the keyring it already hands to
sanitize_sources, so the role's call still resolves to
/etc/apt/keyrings/docker.asc, and a test can point the script at a
temporary keyring.
Verified: tests/unit/python/roles/sys-svc-container/files/test_apt_repo.py
stubs curl and sudo. A refused download keeps the installed key and ends
with changed=0, a refused download without a key exits 1, and a new key
replaces the installed one. A copy of the script whose keep branch always
errors, the old behaviour, fails the first test. Two local deploys ran
the new script with rc 0 and an empty stderr and left the installed key
untouched. shellcheck is clean.
Not verified live: the refused download itself, which needs
download.docker.com to refuse a connection during a deploy. A host
without a key still needs the download and still fails when it is
refused.
Closes#849
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
templates/style.css.j2 maps page, panes, object tree, tabs and primary
actions onto the design tokens and makes the menu bar the brand frame;
templates/design.js.j2 follows pgAdmin's native theme switch. The logo
and login-logo slots meta/services.yml declares sit on frame chrome, and
with design on templates/env.j2 sets PGADMIN_CONFIG_APP_NAME to the
configured title.
pgAdmin keeps its internal user in its own database, and
PGADMIN_DEFAULT_PASSWORD only seeds the first start. tasks/main.yml now
flushes the compose handlers and, on the stack host outside server
mode, runs tasks/administrator.yml with files/python/administrator.py:
it creates the internal administrator if missing, otherwise realigns
password, active and locked state, failed login attempts and the
Administrator role. The identity is the platform administrator as on
feature/store-i18n: PGADMIN_ADMIN_EMAIL and PGADMIN_ADMIN_PASSWORD in
vars/main.yml, meta/users.yml declares the user, and meta/secrets.yml,
whose only credential the role no longer reads, is gone.
tests/unit/python/roles/web-app-pgadmin/files/test_administrator.py
covers the script. meta/services.yml names the container pgadmin for
PGADMIN_CONTAINER_ADDRESS.
files/playwright/test-design.js checks tokens, that the administrator
signs in with that password and holds admin rights, panes, tree, tabs,
the menu bar frame and its focus indicator, primary actions, the theme
switch, logo, favicon and title, and captures the gallery against the
role's own database server; templates/playwright.env.j2 hands the
platform administrator, the design URLs and the database to it.
README.md links the gallery; TODO.md records the Gravatar request and
the memory pressure at the 256m limit.
The design was verified green on the compose stack (10 passed); the
switch to the platform administrator is checked statically only, the
first live run is the verification on the merged state.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Open WebUI loads /static/custom.css on every page, so tasks/00_core.yml
renders templates/custom.css.j2 when design is on and meta/volumes.yml
mounts it read-only there; it maps the sign-in page, the chat shell,
Markdown, tables and code onto the design tokens and leaves the OLED
dark theme to the user. templates/design.js.j2 mirrors the native theme
setting into data-design-theme. meta/services.yml keeps logo and title
replacement off, so Open WebUI keeps its own branding.
With sso and ldap both off, tasks/utils/admin.yml runs files/python/
administrator.py, which creates the native administrator or resets its
password and promotes it, so the native sign-in has an account; the
identity is lookup('users', 'platform_administrator') as on
feature/store-i18n, declared in meta/users.yml next to the user the
role still reads elsewhere. tests/unit/python/roles/web-app-openwebui/
files/test_administrator.py covers the script with fake values.
files/playwright/test-design.js checks tokens, sign-in page, chat shell,
a stored answer, the native theme setting, the provisioned
administrator, the in-house custom.css, logo and title, and captures
the gallery; files/playwright/_shared.js waits for editable password
fields. README.md links the gallery; TODO.md records the open
self-registration with sso and ldap off and the bot password and
bearer tokens in container exec argv.
The design was verified green on the compose stack (10 passed); the
switch to the platform administrator is checked statically only, the
first live run is the verification on the merged state.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Listmonk serves its admin and public pages with the custom CSS and JS it
keeps in the settings table, so the role writes the design there:
tasks/03_design.yml, included after the settings reaper in
tasks/02_manager_ops.yml, pipes templates/design/appearance.sql.j2 into
psql. With design on it writes the marked admin stylesheet, admin script
and public stylesheet, but only over an empty value or one that starts
with the role's own marker, so custom CSS an operator wrote stays; with
design off it empties only its own values. The same SQL sets
app.logo_url, app.favicon_url and app.site_name from the design lookup
and restores the former values when design is off; those keys move from
LISTMONK_SETTINGS to LISTMONK_DESIGN_BRANDING in vars/main.yml.
templates/design/admin.css.j2 maps the colors of the Bulma and Buefy
admin onto the design tokens, templates/design/public.css.j2 does the
same for the subscription pages, and templates/design/admin.js.j2 keeps
favicon and title on the configured ones while the single-page app
rewrites its head. meta/services.yml declares the 182x34 logo slot of
the navbar; the emptied files/style.css goes.
files/playwright/test-design.js checks tokens, public pages, the
administration, navigation and tags, the code editors, logo, favicon and
title, that the role writes only its marked settings, and captures the
gallery; the native form takes ADMIN_NATIVE_PASSWORD. The unit test
renders the SQL for design on, design off and design off without a
fallback. README.md links the gallery; TODO.md records the settings task
that always reports changed, the secrets it renders into the shell argv,
and the logo URLs taken unguarded when the logo replacement is off.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Funkwhale stores its theme in global preferences, so
files/python/design_preferences.py sets the instance name, primary,
accent and background color, ui__custom_css from
templates/custom.css.j2 and the logo and banner as own uploads. It only
writes while ui__custom_css is empty or starts with the role's marker;
custom CSS an administrator wrote reports FOREIGN and stays untouched,
and design off takes back only the role's own values. tasks/main.yml
flushes the compose handlers and includes tasks/01_manager.yml, and
templates/compose.yml.j2 names the api container so container exec can
reach it.
tasks/01_manager.yml probes the administrator with
files/python/admin_state.py (MISSING, DRIFT or OK for active, staff,
superuser and password), creates it with fw users create --superuser
or realigns password and rights with fw users set, the password always
through the task environment. The account is platform_administrator:
meta/users.yml and the ADMIN_* lines of templates/playwright.env.j2 are
identical to feature/store-i18n (3c72c2002a), so the host
administrator's sudo password never becomes a Funkwhale password and
the later merge keeps these hunks clean. store-i18n provisions no
Funkwhale administrator at all.
templates/design.js.j2 follows Funkwhale's own theme switch, and
files/playwright/test-design.js checks the tokens in both modes, the
instance theming with palette, custom CSS and pod name, the start page
hero and sign-in card, the signed-in interface, a theme picked in
Funkwhale, logo, favicon and title, the platform administrator's sign-in
with administration rights and a visitor's refused sign-up, and captures
the gallery. Unit tests cover
both scripts. TODO.md records the Typesense key in the sidecar's argv
and the icons the front fetches from api.iconify.design.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
BookWyrm styles its pages from compiled Bulma themes, so the design
arrives as one of them. files/scss/infinito.scss maps the theme onto the
design tokens; the Dockerfile copies it into css/themes/ and
tasks/00_core.yml stages it in the build context and triggers the build.
The old files/style.css overrides are dropped, the theme replaces them.
tasks/01_site.yml reads the generated logos and runs
files/python/site_design.py through manage.py shell, with every value
handed over in the task environment. The script provisions the
administrator as superuser with the admin and moderator groups and
realigns its password, sets the site name and the navigation logo, touch
icon and favicon, registers the corporate theme and makes it the default
only on a fresh site, and closes the setup wizard. With design switched
off it removes only its own theme and logos. It prints BOOKWYRM_RESULT,
which changed_when and failed_when read.
templates/design.js.j2 mirrors the light or dark theme a user picks in
BookWyrm into data-design-theme, meta/csp.yml admits scripts, styles and
fonts from the object store that serves the static files, and
meta/services.yml declares a 100x100 navigation logo slot.
files/playwright/test-design.js checks the tokens in both modes, the
theme's page, primary action, selection and text, the divider, the
closed setup wizard, an upstream theme a user picks, title and logo, and
captures the gallery.
TODO.md records the registration keys BookWyrm 0.9.3 never reads, the
stale reason that blocks the administrator persona, the SeaweedFS test
that only signs in through Keycloak, and the missing static files of the
variant without an object store.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
templates/style.css.j2 maps the colors of Baserow's compiled stylesheet
onto the design tokens, with the vendor pseudo-element lists split so no
browser drops a rule. templates/design.js.j2 sets the favicon, and
meta/services.yml declares a 160x32 logo and a 32x32 symbol slot.
files/playwright/test-design.js checks the tokens in both modes, the
sign-in page, the signed-in interface, the API documentation, logo,
title and the collapsed sidebar symbol, and captures the gallery.
Baserow's sign-in form asks for the e-mail address and authenticates it
against the username. files/python/bootstrap_admin.py therefore stores
the superuser under its normalized e-mail address. It moves a superuser
kept under the username, or found by its first name after the address
changed, onto the current address, gives an adopted account staff and
superuser rights, realigns the password through check_password,
completes the profile and closes the first-administrator signup page.
It prints BOOTSTRAP_STATE, which the task's changed_when reads.
Ported from feature/store-i18n as identical text, so the later merge
keeps these hunks clean: the platform_administrator account in
roles/user-administrator/meta/users.yml as store-i18n has it now
(username platformadmin, administrator role), roles/web-app-baserow/
meta/users.yml and the BASEROW_BOOTSTRAP_ADMIN_* switch (3c72c2002a and
its follow-ups), and the argv form of the migration, bootstrap and MCP
tasks, which hand only the secrets through -e NAME plus environment
(177255329f). The bootstrap task keeps its own changed_when, because
store-i18n's 'created' never matches this script's output; the
password realign of 23e642a3f3 is part of the script.
templates/playwright.env.j2 reads ADMIN_* from platform_administrator,
the README persona note names the e-mail identifier, and TODO.md is
gone with the argv port that resolves it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Both provisioners follow nextCursor for workflows and step skip for API
keys until a short page arrives. A cursor n8n repeats, or a key listing
that ignores skip, would keep that loop running forever and hang the
deploy instead of failing it.
workflows_named now remembers every cursor it followed and exits with
"FAILED listing workflows: nextCursor repeats" when one comes back.
api_keys_labelled exits when a non-empty page brings no key id it has not
already seen. Normal paging is unchanged.
Four regression tests drive both scripts against a fake API that repeats
its cursor or ignores skip; without the guards exactly those four fail.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
n8n 2 returns ten API keys and a hundred workflows per page by default. A
stale managed key behind the first page survived, and a managed workflow
behind it was created a second time. Both scripts now follow every page,
narrowed by label or name, and keep the exact match in the script.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
n8n 2 pages GET /rest/api-keys as {data: {items, counts, totals, owners}},
ten keys at a time, and matches `label` as a substring, so walking the body
raised "'str' object has no attribute 'get'". It also enforces API key
scopes, so activating the MCP server workflow needs `workflow:activate`.
Both scripts now read every list and create response through one
normalisation that unwraps the page, look the managed key up by label, and
name an unexpected shape by its keys only. The normalisation is taken over
from a392e2dd49 on feature/store-i18n.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The entrypoint removed the old tree before it copied the new one. A
copy that failed left the volume without a working Moodle, and every
restart removed again instead of recovering. It now copies into a
staging directory inside the volume and swaps the trees once the copy
is complete, so a failed copy leaves the old tree in place.
An image that ships an older release than the volume records no longer
replaces it and exits with a message instead. Two instances on
different releases would otherwise overwrite each other's tree.
upgrade.php reports "No upgrade needed" on stderr, so the task counted
every run on a current site as a change. It reads stderr now.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MCP Adapter 0.7.0 for WordPress refuses a request that follows
initialize without the MCP-Protocol-Version header: 'Invalid Request:
MCP-Protocol-Version header is required for a 2025-06-18 session'. The
contract probe never sent it, so the CLI test of web-app-wordpress
failed at tools/list.
The probe now keeps the version the server answers initialize with and
sends it on every later request of the streamable HTTP transport, which
the specification asks of a client since 2025-06-18.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The pin moved from 4.5.11 to 5.3.0, and the role fitted neither a new
install nor an existing one.
Moodle 5 serves from public/ and routes every other path through
r.php. nginx now uses public/ as its root, hands unknown paths to the
router and passes PATH_INFO for scripts with slash arguments, and
config.php declares the router as configured. The paths the role checks
and writes (version.php, auth, webservice, the OIDC plugin) moved below
public/, the URL of the OIDC plugin stays auth/oidc. The source archive
no longer contains vendor/, so the image installs the Composer
dependencies it declares. The OIDC plugin is pinned to v5.2.1, the
newest release.
An existing site kept its old code: the entrypoint filled the code
volume once and never again. It now records the release it copied and
replaces the tree when the image carries another one, and a new task
runs admin/cli/upgrade.php. Plugins that were installed through the web
interface are not carried over, the README says how to ship them.
A site installed with 5.3 forces login by default, which 4.5 did not,
so its start page answers 303 to the login page and the webserver
health check refused it. The role declares 200 and 303 as the codes
that check accepts: 200 is what an upgraded site keeps answering.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflicts:
- roles/web-app-pihole/meta/services.yml: main dropped the unused redis
entry, this branch changed the javascript entry directly below it. The
redis entry is gone and the javascript entry keeps the flag bound to
the SSO flag of the role and its nocheck reasons.
- README.md: the generated role table keeps the renamed web-svc-design
role and lists Pi-hole without Redis and without the CDN.
The role diagram of Pi-hole is regenerated, because both sides removed
an edge and the index of the red link moved.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The guard required a register on every async kcadm job and nothing more.
A result that is registered but no longer handed to async_reap_jobs,
directly or through the list it is collected into, passed it, although
the reap then neither polls that job nor sees it fail.
The test now follows each registered result to an async_reap_jobs
expression, by variable name with word boundaries, so kc_declared_client
does not pass for kc_declared_client_jobs.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The stub answered every absolute name, so a guard that asked for the
backend twice and never for the exporter passed both tests. The stub now
records the names it is asked for, and the tests require the backend and
the exporter.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
COLLABORA_WOPI_HOST read the scheme and the port with
lookup('tls', 'web-app-nextcloud', ...) and the host with
lookup('domain', 'web-app-nextcloud'). The tls lookup aligns an app term
to its consumer: for a clearnet Collabora it answers for the clearnet
sibling of an onion-primary Nextcloud, while the domain lookup keeps the
onion name. Such a deployment renders https://<onion>:443, an origin
Nextcloud does not use, so the allowlist does not match its WOPI calls.
Nextcloud pins overwritehost and overwriteprotocol to its primary domain,
so the allowlist now asks the tls lookup for that domain instead of for
the app.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Since the targets take their scheme from the domain they probe, two
switches of the http_ready module no longer fit. Both still followed the
TLS state of the Prometheus vhost.
fail_if_not_ssl refused an http target, an onion vhost for example, as
soon as Prometheus itself was served over TLS. It is false now. The
module does not follow redirects, so the scheme of the target URL already
decides the transport.
insecure_skip_verify switched certificate checks off whenever Prometheus
itself had no TLS, also for an https target. Checks now stay on, except in
a self-signed deployment whose CA is not mounted into the Prometheus
containers, where the exporter has nothing to verify against.
The per-role test of the spec parsed every answer of the query API as
JSON. An answer that is not OK now counts as "no probe yet", so the poll
keeps waiting instead of ending on a parse error.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The check listed the torrcs without the limit through "grep -L ...; true".
With no torrc under /opt/chutney/net/nodes, or with one that cannot be
read, grep printed nothing, the trailing true turned its error into
success, and the test reported that every node runs one worker.
The check now counts the limit per file with "grep -cH" and reads the exit
status. A status above 1 or an empty answer fails the test, and a file
with a count of 0 is named as before.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Tor starts its worker threads by the number of CPU cores it detects, and
the cpus limit of the container does not change that number. On a host
with 20 cores every node of the private chutney network ran 21 threads and
grew to about 0.6 GB. The seven relays and authorities filled the 2g
limit and its swap within ten minutes, the kernel killed them one by one,
and no circuit could be built any more.
The entrypoint now writes NumCPUs 1 into the torrc of every chutney node
before it starts the network. Measured on that host over ten minutes: the
nodes run 3 threads and stay at 65 to 86 MB, the container at 0.47 GB
without swap, where it had filled 2 GB plus 2 GB of swap before.
The CLI test of the role fails when a chutney node runs without the limit.
Closes#824
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
Every blackbox probe of a Tor deployment answered 404, so AppDown fired for
all deployed apps although they were up. The job built its targets from
canonical_domains_map(DOMAIN_PRIMARY), which knows no onion domain. In a Tor
deployment the vhosts answer under <sub>.<onion> only, and the front proxy
served the clearnet names from its default server.
The targets now come from lookup('domains'), the map the vhosts are built
from, and each target takes its scheme from the domain it probes instead of
from the Prometheus vhost.
Which roles are probed follows lookup('roles_with_service', 'prometheus'),
the list the role's own spec expects metrics for. It leaves out the database
roles, which have a domain in the map and no vhost, and the roles that
declare scrape: false because their vhost carries no Prometheus block.
Two roles declared more than their vhost delivers. web-svc-mirror renders a
vhost of its own with no backend behind it, and on a live stack its probe
answered 404 under the onion name as well, so it declares scrape: false now.
web-app-peertube has a backend, but its vhost template never included the
Prometheus locations. It includes them now.
The spec could not see any of this in a compose deployment. dotenv_quote
wraps PROMETHEUS_TARGET_ROLES_JSON in quotes there, JSON.parse returned a
string, the role list came out empty and the per-role test was skipped. The
value is decoded now, and the test also requires probe_success == 1 for
every role in the list.
Closes#792
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Of the 76 open code scanning alerts, 39 point at code that can change.
svc-ai-mcp-adapter:
- A path argument that is empty or made only of dots is refused.
Percent-encoding keeps a separator inside its segment, but "." and ".."
need none, so an upstream that resolves dot segments could answer from a
path the contract does not name.
- parse_duration splits on the units. The pattern it used before rescanned
a long run of digits from every start position.
Tests and the README generators:
- Every Jinja environment built with autoescape=False and a "noqa: S701"
uses select_autoescape now, as six test modules and
utils/templating/jinja.py already did. It escapes templates named
*.html, *.htm and *.xml, which none of these call sites loads.
- Dead code goes: a doubled "rel" block in the email lint, an unread
_SKIP_DIRS, and a module that _resolve returned to no caller.
- Two "except ...: pass" blocks become contextlib.suppress and
assertRaises.
- Nine test modules import unittest.mock one way instead of two, and one
lookup test imports its module once.
Closes#807
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>