svc-runner started its stack with 'compose --chdir /opt/github-runner up'
and no --project, so the wrapper named the project after the directory,
github-runner, while every other role uses its entity name. Pass 2 of the
swarm drill in fork run 41744154601 (svc-runner#0) then stopped in the
pre-flight: network 'runner' belongs to compose project 'github-runner'.
The deploy now passes --project from entity_name, and a unit test renders
the real task, runs the real wrapper and checks the pre-flight accepts the
network it creates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
followed_tags caught OSError, ValueError, KeyError and RuntimeError, but
http.client.IncompleteRead, BadStatusLine and LineTooLong derive from
http.client.HTTPException only. One truncated digest response therefore
escaped to collect_updates (docker.py:344, no try) and aborted the whole
Docker update run instead of printing the follow warning for that image.
Every other fetch site of the updater already catches HTTPException
(docker.py, pip.py).
Reported by CodeRabbit on #1370.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_prefix_grouped_files flagged roles/web-app-funkwhale/files/python/same_origin/, whose three files all started with same_origin_; they were untracked while the lint ran in the objstore worktree. The sources are now settings.py, storage.py and urls.py, volumes.yml mounts them under the unchanged targets /app/same_origin_settings.py, /app/same_origin_storage.py and /app/same_origin_urls.py, so module names and DJANGO_SETTINGS_MODULE stay the same; the mirrored tests and the network_modes.md link follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Setup ran add-ldap on every deploy under the name LDAP ({{ DOMAIN_PRIMARY }}); the name column compares case-sensitively, so a casing change created a second source (echoserver had LDAP (Infinito.Nexus) ID 1 with 331 users and LDAP (infinito.nexus) ID 3 with 12), and setup updated only the new one. Cleanup looked the source up with an awk regex in which the parentheses grouped, so it never matched. Setup and cleanup now find LDAP sources by their type (LDAP (via BindDN)), the source is named LDAP, and gitea_ldap_source_plan decides from the per-source user and external-login counts (files/sql/auth-source-users.sql through database_query, only when more than one source exists): keep the source users are linked to, else the lowest id, and delete the others; users on more than one source fail the deploy loudly and delete nothing. The bind password now travels on stdin, is read on the host, forwarded with container exec -e and expanded only by the shell inside the container, with no_log; it left GITEA_LDAP_AUTH_ARGS. On echoserver the 12 users of ID 3 were moved to ID 1 by hand first, so the next deploy keeps ID 1 and deletes ID 3.
Residual: the Gitea CLI takes the password only as --bind-password, so it sits in the argv of the in-container gitea process while add-ldap or update-ldap runs. The IAM line's 5f152d9f28 passes it through task environment: and keeps the name lookup; at the IAM merge take this side for the LDAP tasks and the GITEA_LDAP_* vars.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tests/lint/ansible/roles/test_file_spot.py rejects a hard-coded
"roles/a/tasks/main.yml" in test_stage.py and asks for the
ROLE_FILE_TASKS_MAIN constant. The fixture only needs some nested file,
not a role, so it uses pkg/a/data.yml and pkg/a/.git instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On runner image 20261101 kernel.apparmor_restrict_unprivileged_unconfined
is on, so the kernel turns runc's change_onexec into the stack
docker-default//&unconfined. docker-default grants no signal to an
unconfined peer, and every in-container kill() is denied; run 41743807934
lost four package builds that way (journal: apparmor="DENIED"
operation="signal" comm="rsync" signal=usr2).
teardown.sh now writes 0 to the sysctl and records the value it found
under /run/infinito; restore.sh writes that value back and drops the
record. A missing sysctl or one already at 0 is left alone, and a second
teardown keeps the first record. CI runs teardown before every deploy,
workspace and dns job; dev hosts through make environment-bootstrap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run 41743807934 lost Matrix#2, Workspace (debian:13), Gitea#0 and Wazuh#0
to the full job budget right after "Preparing build workspace". On runner
image 20261101 the kernel stacks every container onto
docker-default//&unconfined, which refuses every in-container kill(); the
host journal logs apparmor="DENIED" operation="signal" comm="rsync"
signal=usr2. rsync's local copy waits for exactly that SIGUSR2 and sleeps
until the job is killed.
scripts/install/stage.sh copies the tree through a tar pipe, which sends
no signal; --format=posix keeps nanosecond mtimes. package.sh uses it on
the arch and debian paths and installs tar instead of rsync.
Copying this repository both ways gives identical content, modes, types,
link targets and mtimes on all 11306 entries. test_stage.py runs stage.sh
under a seccomp filter that fails every signal syscall with EACCES; the
old rsync copy hangs past its timeout under the same filter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a long-lived instance the role's DOMAIN, ROOT_URL and SSH_DOMAIN variables never reach app.ini: the image's setup script writes them only when app.ini does not exist yet and applies only GITEA__section__KEY variables on later starts. echoserver's Gitea therefore still runs with ROOT_URL https://code.cymais.cloud/, sends that host as the OAuth2 callback, and Keycloak answers 'Invalid parameter: redirect_uri' (its log shows exactly https://code.cymais.cloud/user/oauth2/SSO%20Login%20%28INFINITO.NEXUS%29/callback). env.j2 now sets GITEA__server__DOMAIN, GITEA__server__ROOT_URL and GITEA__server__SSH_DOMAIN. The OAuth2 source is named 'SSO Login', because external_login_user.provider holds 25 characters and the 26-character domain-suffixed name would break every account link; setup and cleanup find the source by its type instead of by name, so the rename happens in place and no second button appears. OpenID 2.0 sign-in and sign-up are switched off. CI missed it because every CI run installs fresh and the Gitea specs never click the SSO button.
The IAM line's 144871ada1 carries the same rename and lookup plus a patched Gitea build for onion sign-in, but keeps the plain ROOT_URL variable; at the IAM merge keep these GITEA__server__ lines together with that patch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decidim#0 and Funkwhale#0 of fork run 41744154601 failed on the tor rows because both apps handed the browser presigned or public URLs on the object-store host, which publishes no onion vhost. Decidim now sets STORAGE_CDN_HOST to its own origin and switches Active Storage to rails_storage_proxy; an initializer routes direct uploads through the disk-service token route (Rails writes them to S3 server-side), points the redirect and legacy blob routes and the rich-text BlobRenderer at the proxy, safelists storage reads in Rack::Attack, and refuses to boot when the patched activestorage 7.2.2.2 / decidim 0.31.6 signatures change. Funkwhale runs a settings module with a storage whose public names (attachments, sized images, dynamic preferences) resolve to MEDIA_URL and whose exports get a signed one-hour token link; a /media view checks every name against clean_name and rejects backslashes and percent signs, signs per request method and answers with X-Accel-Redirect; the front nginx routes those paths to the API, sends relative protected covers back through it and no longer forwards X-Forwarded-Host, which broke the presigned signature. Both apps reach the store over its internal URL; the public-host CSP entry, extra_hosts entry and CA bundle are gone. The Playwright specs assert the direct upload goes to the app and accept no object-store origin.
Verified by compose deploys of both roles, clearnet update and tor-only reinstall, failed=0 with green Playwright and no object-store host in the rendered pages or APIs; swarm and Decidim variant 1 (disk storage) are not deployed yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WORDPRESS_OPTIONS now merges the role's own contributions (admin_email from the platform administrator, blogname from the design title while the design service is on, the freedomtranslate-wp config.options while that addon is on) under services.wordpress.options, so the inventory still wins and wordpress_options_checked guards the merged map. design.php no longer writes blogname, the addon task no longer writes its options, and WORDPRESS_OPTIONS_OWNED_ELSEWHERE drops the three. Every deploy now converges admin_email, and blogname while the design is on; a write WP-CLI reports as unchanged no longer counts as a change, and the FreedomTranslate readback runs after the options write.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_prefix_grouped_files flagged cli/administration/inventory/provision/services_disabler.py and services_disabled_conflicts.py, which share the 'services' prefix since the conflict check moved into its own module. Both now live in cli/administration/inventory/provision/services/ as disabler.py and disabled_conflicts.py; every importer, the purge script, the BLE001 per-file ignore and the requirement 037 link follow the new path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts resolved: the inventory validator keeps store-i18n's custom-role
declaration skip and gains the open-subtree check; the suppression catalogue
keeps both new rows; WORDPRESS_PERMALINK_STRUCTURE gives way to the options
map. WORDPRESS_OPTIONS_OWNED_ELSEWHERE also lists the store-i18n writers the
merge brought along: the design logos (WORDPRESS_DESIGN_LOGOS keys) and the
freedomtranslate options; two custom-role validator tests pass open_paths.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deploys since 197d74416d rewrote the blog's permalink structure to
/%postname%/, so links of the form /blog/YYYY/MM/DD/<slug>/ answer 404
(docs/security/user.md:24 fails the URL check).
services.wordpress.options maps WordPress option names to values; the role
reads each option, writes only the ones that drift and flushes the rewrite
rules after a write. permalink_structure is pinned to
/blog/%year%/%monthnum%/%day%/%postname%/, the value WordPress itself sets
for a subdirectory network. A guard refuses options the role writes
elsewhere, secret-named keys and values WordPress would not read back
unchanged.
The inventory validator accepts any child key under a services.yml key that
carries '# nocheck: inventory-open-subtree <reason>'; services.wordpress.options
carries it, everything else stays strict.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
services_disabler.py grew past the 500-line limit with the dependents step
(gate pass 16, test_python_file_size). The conflict check moves unchanged to
services_disabled_conflicts.py; the YAML helper it shares becomes public
(load_yaml_mapping_tolerant) and the two importers follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_file_spot flagged the hand-written vars/main.yml path in the chat
integration test (gate pass 16).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Update run 41722395224 proposed no sys-os bump although fedora:latest
points at 45 and a local dry run proposes 44 -> 45. followed_tags turned
every failed digest lookup into "no candidate" without a word, so the run
cannot tell a rate limit from a pin that is current.
tag_digest now retries an HTTP 429 twice with a short backoff, as the tag
listing does, and followed_tags reports on stderr what it compared and
which tags it kept, or the error that made it propose nothing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The design inject renders a role's style.css once per vhost but copied every
render to one shared CDN path, so the last render won. xwiki's stylesheet
embeds its own origin; on a multi-network node the onion render landed last
and every clearnet page loaded onion URLs, failing network-leak.spec.js
whenever the pass picked clearnet.
The first network keeps style.css, every further network gets
<label>-style.css (onion-style.css), keyed by the same rule the tls lookup
uses for the render (serving_network), and each vhost links its own file.
Single-network roles keep the exact old path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The chat-integration plugin ships with core and defaults to
chat_integration_enabled: false, while the addon task was empty, so the
spec's first assertion could never pass and topic notifications never left
Discourse for Matrix or Mattermost.
The addon now turns on chat_integration_enabled plus the provider of every
partner the round deploys, idempotently, reads the settings back and fails
when one does not stick. The spec asks the plugin's real admin route
(/admin/plugins/discourse-chat-integration/providers.json; the old path is a
404 on v2026.10.0) and requires an enabled partner provider, since the
catalogue now lists disabled providers too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tor-only CI row disables web-svc-coturn, which no onion can serve, but the
disabler switched off only service entries keyed coturn. Nextcloud's talk
entry is gated on 'web-svc-coturn' in group_names and variant 0 pins it true,
so Talk stayed on without a TURN server and 01_fullstack.yml's assert stopped
every Nextcloud#0 tor row.
When disable resolves to a provider role P, every entry whose base enabled is
exactly the presence test of P is written false as well, and the reconcile
releases those entries once P is no longer disabled. Negated, compound and
variable gates are left alone. Across all 755 planner row states this changes
one value an app sees: talk on Nextcloud#0 tor rows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pacman's DatabaseOptional fetches <repo>.db.sig after every database; Arch
publishes none, so every mirror answers 404. The XferCommand ran curl with
--retry-all-errors, which retried that permanent 404 three times, 2 s apart,
for about 18 s per refresh on every Arch host (deploy log 17.4-18.3 s).
pacman-xfer keeps retrying transient failures (transport errors, 5xx, every
4xx but 404/410), honours Retry-After on 429/503 up to 30 s and drops a failed
attempt's partial bytes like curl's own retry did; 404 and 410 fail at once.
The shell-download-retry lint accepts a download its script retries in a
bounded counter loop that leaves on success and sleeps between attempts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_context.sh runs under set -euo pipefail and tested membership with
`printf '%s\n' "${_names}" | grep -qxF <name>`. grep -q exits on the first
match; when printf still has lines to write it dies of SIGPIPE (141), and
pipefail turns the matched test into false. resolve_db_service then fell
through to the central <dep>_<dep> name or printed nothing, and
has_swarm_service reported a deployed service as missing, so
skip_if_no_swarm_service let the converge and chaos gates exit 0 untested.
The race grows with the stack's service count and with CPU load (store-i18n
gate pass 11: 'mariadb_mariadb' != 'magento_database').
The three tests read the listing from a here-string, which is no pipeline.
Two regression tests put the match first in a 10000-name listing and fail
on every run against the old helper. Root cause found by i18n 2.0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ruff S701 flagged the jinja2 Environment without autoescape in the single-boot rails test; select_autoescape(default_for_string=False) keeps the rendered Ruby stdin unescaped, the repository's pattern.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DEV_PYTHON_REQUIRES is shorter than the old name, so ruff format joins the
dict comprehension in test_install.py onto one line, and the CI format
check would report the drift.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_no_raw_project_walk flagged tree.rglob in files_in; the walk scans the test's own temporary directory, the exception the lint documents, so it carries the project-walk nocheck instead of the cached project helpers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three separate 'rails runner -' calls each paid a full Rails boot (47 s, 42 s and 39 s on the echoserver production deploy). platform_admin.yml now runs one runner whose stdin is a small step driver (run_steps.rb) followed by the unchanged root-password, platform-admin and appearance scripts, each wrapped in its own InfinitoSteps.run block: every step keeps its own [CHANGED]/[FAIL] verdict prefixed with its name, exit(0) ends only that step, and the run fails naming every failed step. Rails 7.2.3.1 evals runner stdin at TOPLEVEL_BINDING inside an executor that passes SystemExit through; puts writes to the swapped $stdout while the logger writes to files. The design render and slurp move to tasks/utils/design.yml ahead of it, keeping the numbered includes ascending, and the file layout leaves feature/iam-profile-rbac's hunks on platform_admin.yml and 00_core.yml mergeable without conflicts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ruff S701 flagged the jinja2 Environment without autoescape in the auth_oidc sync test; select_autoescape(default_for_string=False) keeps the from_string shell body unescaped, as other tests do, and autoformat wrapped one long filecmp call.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shorter variable name from 0bdb14cc1d (#1358) lets the dict comprehension fit on one line; make autoformat in gate pass 6 collapsed it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The entrypoint refills the shared code volume only when the release changes, so a volume bootstrapped at 5.3.0 by an image without auth_oidc never received the plugin from a later same-release image; galaxyserver's production deploy failed at the presence check although the image ships it (probe: /opt/moodle-source/public/auth/oidc exists). 04_oidc.yml now asserts the image ships the plugin, naming the image when it does not, and syncs it into the volume as the runtime user through a staging directory, only when diff -rq finds a difference, before the existing presence check. The OIDC upgrade.php run no longer hides a failed plugin install behind failed_when: false, matching the MCP and objectfs plugin tasks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Development worktrees carry empty .claude/.cc-writes directories in role source trees; the one-call unchanged check gave up on any directory without files, so the docs tooling copy fell back to per-file stat and copy round trips (about 56 s on the echoserver production deploy). file_checksums now also reports directories (isdir, mode), and the check asks for every leaf directory in the same remote call: it settles only when each leaf exists as a real directory and, when directory_mode is set, carries exactly that mode. With directory_mode unset the stock copy would not touch an existing directory either (file module with mode None). A missing directory, a mode mismatch, a file or symlink in its place, or a symbolic directory_mode with leaves still runs the regular copy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The runtime image pins lived in meta/distros.yml, which neither the docker
updater, the image mirror nor the vulnerability scan reads, so a pin would
only ever move by hand. The new role sys-os carries them in its
meta/services.yml under the distro id; utils/distros.py renders the
workspace matrix from there with the stdlib block reader, and
meta/distros.yml keeps the distro attributes only.
A new release now arrives as an updater pull request. follow: latest holds
debian, ubuntu and fedora to the release their latest tag points at, by
Docker Hub digest: Ubuntu moves between LTS releases only (26.10 shares the
digest of rolling, not latest) and Fedora skips rawhide (46 shares the
digest of rawhide, latest is 45). Arch stays rolling; CentOS stream tags
carry no number the updater can order.
dockerhub_repo and is_dockerhub move to utils/docker/image/ref.py beside
split_registry_and_name, so utils/update/follow.py can use them without an
import cycle and utils/update/docker.py stays under the size cap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>