Commit Graph
14682 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 4.8 25f286d2bf feat(swarm): per-service internal ports and entity-named services
Give each reverse-proxied app a swarm-resolvable upstream: add services.<entity>.ports.internal.http to the role metadata and rename the main compose service from the generic 'application' to the entity name, so its short DNS alias on the shared overlay is unique. Thread the service name through compose, vars (*_SERVICE_NAME), volume mounts and the oauth2 origin host. Consolidate the split image/ports blocks in bridgy-fed and postmarks into the entity block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:49:18 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c3f01a8640 feat(proxy): mode-aware proxy_pass SPOT lookup
Centralise the nginx proxy_pass directive in one source of truth (utils/networks/proxy.py + plugins/lookup/proxy_pass.py). Compose keeps the literal 127.0.0.1:<local> upstream; swarm resolves the service over the shared overlay at request time (variable upstream + http resolver) so a rescheduling backend never blocks openresty startup. Converts the html/media/upload/ws location partials, the keycloak oauth2 and login-iframe endpoints, and the logout endpoint to the lookup; adds the swarm resolver and the front-proxy service_key plumbing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:49:18 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c53c77af15 fix(openresty-swarm): bind-mount nginx.conf so post-deploy rewrites take effect
sys-svc-webserver-core rewrites nginx.conf AFTER openresty first deploys. An
immutable swarm `config` froze the bootstrap content (openresty kept serving
the `listen 8080; return 200` stub and never bound 80/443). A bind mount
reflects the live host file on `service update --force`, so the real vhost
config loads. openresty is manager-pinned, so the host file is always present
where the task lands.

Runtime-confirmed (swarm, attempt 28/29): openresty served the real config and
bound 80/443, vs the bootstrap-only stub under type: config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:49:18 +02:00
kevinveenbirkenbachandClaude Opus 4.8 baae3c377e fix(openresty-swarm): out-of-band overlay creation + stop-first host-mode redeploy
- Load openresty via the sys-svc-compose network/routine.yml helper (same
  pattern as the db/ollama roles) so the shared `openresty` overlay is created
  attachable + swarm-scoped before the stack deploys; emit the top-level
  networks block via compose_networks.
- Add an opt-in `order:` to the swarm update_config and set it to `stop-first`
  for openresty: it publishes 80/443 in host mode, so a default rolling update
  deadlocks (the new task cannot bind ports the old task still holds).
- Relax the network-include lint to accept a single compose_networks lookup in
  a host-mode template (openresty is mode-conditional: host in compose, overlay
  in swarm).
- Whitelist openresty's path_join network/routine.yml include in
  .ansible-lint-ignore (same dynamic-include pattern as the db/ollama roles).

Runtime-confirmed (swarm, attempt 32): openresty created its overlay, attached
consumers, reached backends, and redeployed cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:49:18 +02:00
kevinveenbirkenbachandClaude Opus 4.8 7376d5bd4d feat(overlay): web_facing consumer + proxy_resolvable collect for openresty net
Extend the registry-driven overlay renderer so the central openresty proxy
reaches every web-facing backend over a shared swarm overlay:

- Add consumer kind `web_facing` (matches web-app-*/web-svc-*) so every
  web-facing app attaches its primary service to the openresty overlay in
  swarm mode; openresty resolves them by swarm service DNS.
- openresty's overlay declares `collect_proxy_resolvable`, harvesting the
  keycloak beacon's `auth.<domain>` alias onto its own attachment so each
  per-app sso-proxy resolves the auth host to openresty.
- Fix the default-net name collision: a shared_net provider rendering its own
  compose must not also emit `name:` on its default overlay (the external net
  already carries that name), which made `docker stack deploy` fail with
  "network <x> already exists".

Runtime-confirmed: openresty reached keycloak over the overlay (302, no more
connection-refused) once consumers attached.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:49:18 +02:00
kevinveenbirkenbachandClaude Opus 4.7 cabbdea764 fix(install): 15-min hard timeout per ansible-galaxy collection install
Wrap each `ansible-galaxy collection install` invocation (Galaxy and
the Git fallback) with `timeout --foreground 15m` so a stalled upstream
fetch can no longer hang the whole `act` run indefinitely. Without this
guard, an attempt was observed to spin for ~51 minutes inside the
Discover-apps step before being killed externally.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 19:49:17 +02:00
infinito-nexus-updater[bot] e12c0ee995 update(repos): bump git refs to latest semver tags 2026-06-17 14:36:23 +00:00
infinito-nexus-updater[bot] 5d0ec4254a update(images): update Docker image versions 2026-06-17 14:13:34 +00:00
kevinveenbirkenbach 1549ac284d Merge pull request #320 from infinito-nexus/update/docker-image-versions-20260617-e8d0046
update(images): update Docker image versions
2026-06-17 15:42:19 +02:00
kevinveenbirkenbach cfc859cb7f Update services.yml 2026-06-17 15:41:59 +02:00
kevinveenbirkenbach 3fb0e393c3 Merge pull request #316 from kevinveenbirkenbach/fix/ci-deploy-server-failures
CI deploy/test fixes + SeaweedFS S3 object-store integration + addon requirements
2026-06-17 15:40:15 +02:00
kevinveenbirkenbachandClaude Opus 4.8 142a4b4241 fix(cache): self-heal package-cache-frontend CA missing keyUsage
OpenSSL 3.x rejects a trust-anchor CA without keyUsage, but cert-gen.sh only regenerated on missing/expiring certs, so a CA created before the keyUsage addext was added (10y validity) never refreshed and broke all MITM-proxied HTTPS verification in the dev cache. Add a keyUsage guard so a deficient CA is regenerated even while still time-valid.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:47 +02:00
kevinveenbirkenbachandClaude Opus 4.8 9f65b558dc docs(026): unified addon syntax spec + acceptance check-off
Per-file meta/addons/<id>.yml convention throughout; 18 end-to-end-verified acceptance criteria checked off, 37 held (matrix-wiring backlog, audit/update automation, deploy-gated per-role).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:47 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4e807e64c2 docs(027): regenerate integration matrix from data module (req 026)
Split the generator data into integration_matrix_data.py and regenerate 027-integration-matrix.md (82x82, 523 wired + 96 upstream edges). Artifact is generated, not hand-edited.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:47 +02:00
kevinveenbirkenbachandClaude Opus 4.8 7800205732 feat(desk): migrate browser/gnome extensions to meta/addons (req 026)
desk-chromium, desk-firefox, desk-gnome declare extensions as mechanism: extension (no bridges); templates derive the install lists from addons; desk-gnome variant axis drives enable/disable via the addon enabled flag. Playwright exemption documented (Decision 11).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:47 +02:00
kevinveenbirkenbachandClaude Opus 4.8 5e375d5668 docs(mattermost): document volume-based plugins under addon contract (req 026)
Mattermost manages plugins at runtime via named volumes; no meta/addons declared. README documents the exemption.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:47 +02:00
kevinveenbirkenbachandClaude Opus 4.8 28c64dd4c5 feat(pretix): migrate oidc plugin to meta/addons (req 026)
pretix-oidc declared as mechanism: plugin with the 2.3.1 pin as a string version and bridges [sso].

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:46 +02:00
kevinveenbirkenbachandClaude Opus 4.8 42f272240b feat(discourse): migrate plugins to meta/addons (req 026)
docker_manager, discourse-activity-pub, discourse-akismet, discourse-ldap-auth (bridges ldap) declared as mechanism: plugin; config.yml.j2 + vars/main.yml read the new addons path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:46 +02:00
kevinveenbirkenbachandClaude Opus 4.8 f7603cc0c2 feat(matrix): migrate mautrix bridges to meta/addons (req 026)
Six mautrix network bridges declared as mechanism: bridge, enabled: false, with DB creds referenced from meta/schema.yml. MATRIX_BRIDGES derived from addons; vars/bridges.yml, filter_plugins/bridge_filters.py and its unit test removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:46 +02:00
kevinveenbirkenbachandClaude Opus 4.8 62ecbafecb feat(joomla): migrate keycloak plugin to meta/addons (req 026)
plg_system_keycloak declared as mechanism: plugin, source: built, bridges [sso]; env.j2 reads the addon config.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:46 +02:00
kevinveenbirkenbachandClaude Opus 4.8 6b7a579850 feat(xwiki): migrate plugins to meta/addons (req 026)
oidc-authenticator (bridges sso), ldap-authenticator (bridges ldap), matomo (bridges matomo); version pins preserved as strings; xwiki_filters derives install items from addons.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:46 +02:00
kevinveenbirkenbachandClaude Opus 4.8 65545e52e8 feat(mediawiki): migrate extensions to meta/addons (req 026)
OpenIDConnect (bridges sso) and PluggableAuth declared as mechanism: extension; extension list derived from addons in vars/main.yml.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:46 +02:00
kevinveenbirkenbachandClaude Opus 4.8 226d5b5366 feat(wordpress): migrate plugins to meta/addons (req 026)
plugin/mu_plugin addons declared; daggerhart OIDC + rbac-mapper bridge sso, wp-discourse bridges discourse. Settings absorbed from vars/oidc.yml + vars/discourse.yml (deleted); wp-discourse task uses include_vars for the runtime vault key.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:45 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4be72582eb feat(nextcloud): cross-role integration addons (req 026 matrix backlog)
Declares integration_* addons (discourse/github/gitlab/google/mastodon/openai/openproject/peertube) per the 027 integration matrix. Disabled by default; not yet end-to-end wired/tested (acceptance criteria 265-272 remain open).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:45 +02:00
kevinveenbirkenbachandClaude Opus 4.8 b2fddf70a6 feat(nextcloud): migrate plugins to meta/addons (req 026)
Each Nextcloud app declared as a per-file addon; sociallogin bridges sso, user_ldap bridges ldap, bbb/onlyoffice/richdocuments/spreed/whiteboard/xwiki bridge their partner services. Per-plugin runtime config absorbed from vars/plugins/* (now deleted); install/configure tasks read the addon config via include_vars for lazy var rendering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:45 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4dd2c9cf76 feat(odoo): migrate core modules to meta/addons (req 026)
Seven core modules declared as mechanism: module, required: true, group: core; module lists derived from addons in vars/main.yml.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:45 +02:00
kevinveenbirkenbachandClaude Opus 4.8 07da73d88c feat(friendica): migrate ldapauth to meta/addons (req 026)
ldapauth addon bridges [ldap]; ldap+sso service blocks stay in meta/services.yml; tasks/04_addons.yml reads the new addons path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:45 +02:00
kevinveenbirkenbachandClaude Opus 4.8 e317f08ac9 feat(addons): unified meta/addons contract (req 026)
Per-file meta/addons/<id>.yml loader, schema + bridge-parity lints, shared update SPOT, unit/external tests, and layout.md/plugins README docs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:45 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c4c8672b5a fix(test-e2e-playwright): count native-login success as admin-persona auth proof
No-SSO roles authenticate via their own username/password form. The admin persona only treated a completed Keycloak round-trip as proof of reaching an authenticated surface, so a role like Snipe-IT — whose only logout control sits inside a collapsed account dropdown the visible-marker poll never sees — failed the gate even after a successful native login.

Track native-login success symmetrically: a real password form was found and submitted (loginAttempted), the form is gone afterwards, and the page parked on the role's canonical domain. The loginAttempted guard prevents a form-less public landing on the canonical domain from passing the gate without authentication. Also strip the verbose mid-code comments from the persona helper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:30:45 +02:00
kevinveenbirkenbach 76a2d905b6 Merge branch 'fix/ci-deploy-server-failures' into feature/addon-refactoring 2026-06-17 11:30:44 +02:00
kevinveenbirkenbachandClaude Opus 4.8 98d1b23db7 fix(test-e2e-playwright): submit native admin login via Enter; stop matching account as an auth marker
OpenProject's /login renders a header Sign-in toggle plus a duplicate pulldown form, so a generic Sign-in button click hits the toggle and never submits the credentials. Press Enter on the password field to post the enclosing form (the button click stays as a fallback). Also drop the bare account token from the admin auth marker so an unauthenticated Create-a-new-account link no longer masks a failed login as no-logout-control.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:44 +02:00
kevinveenbirkenbachandClaude Opus 4.8 e04a7c8092 fix(web-app-postmarks): block the administrator persona when SSO is disabled
Postmarks admin auth is the SSO trusted-header bridge only; with SSO off there is no native admin login for the persona to drive, so the no-SSO variant failed to reach an authenticated surface. Gate PERSONA_ADMINISTRATOR_BLOCKED on services.sso.enabled.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:44 +02:00
kevinveenbirkenbachandClaude Opus 4.8 03835bcd85 fix(web-app-openproject): activate the seeded admin so native login is accepted
In the no-SSO variant the admin persona drives native login, but it was rejected ('Invalid user or password') even though the seeded credentials match. Set the user status to active in ensure_admin_user.rb so a freshly-created admin can authenticate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:44 +02:00
kevinveenbirkenbachandClaude Opus 4.8 6d87fe3057 fix(web-app-matrix): run the SeaweedFS avatar test only on the ansible flavor
Synapse S3 media offload (synapse-s3-storage-provider) is wired only in the ansible flavor; the compose flavor stores media on local disk, so the matrix bucket never grows and the avatar seaweedfs check failed on variant-2 (compose). Expose MATRIX_FLAVOR and skip the test unless the flavor is ansible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:44 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c353afc714 fix(web-app-baserow): block the administrator persona when SSO is disabled
Baserow's admin is SSO-gated; with SSO off there is no provisioned native frontend admin (first run forces /signup), so the persona can never authenticate and the no-SSO variant timed out. Gate PERSONA_ADMINISTRATOR_BLOCKED on services.sso.enabled, mirroring the biber gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:44 +02:00
kevinveenbirkenbachandClaude Opus 4.8 1d2b1e0113 fix(web-app-mailu): retry user-create/password so a transient DB or admin restart does not fail the deploy
On busy runners the mailu admin/MariaDB container can be OOM-killed or briefly refuse connections mid user-loop (flask mailu user -> ConnectionRefused rc=1 / SIGKILL rc=137), which failed co-deployed roles (opentalk, fusiondirectory). Add retries/delay/until to the user-create and password-change commands so a transient outage is waited out; 'already exists' still short-circuits as success.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:44 +02:00
kevinveenbirkenbachandClaude Opus 4.8 abee277216 chore(web-app-matrix): track matrix-docker-ansible-deploy from spantaleev upstream
Switch the MDAD upstream from the kevinveenbirkenbach fork back to the canonical spantaleev/matrix-docker-ansible-deploy repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:43 +02:00
kevinveenbirkenbachandClaude Opus 4.8 94cba80c56 docs(claude): forbid raw docker exec; reach the live stack via make targets
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:43 +02:00
kevinveenbirkenbachandClaude Opus 4.8 e514e38b08 fix(web-app-matrix): relax Synapse rc_login so the SSO e2e suite does not exhaust the burst
Synapse's default rc_login (burst 3, ~0.17/s) is too strict for the SSO e2e suite, where consecutive persona sign-ins drain the burst and leave later sign-ins cycling consent <-> M_LIMIT_EXCEEDED until the test is torn down (Matrix avatar never uploaded). Set matrix_synapse_rc_login with a higher burst via the MDAD ansible flavor vars.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:43 +02:00
kevinveenbirkenbachandClaude Opus 4.8 e3b3c739bb fix(web-app-postmarks): build the admin URL from the origin to avoid a /admin/admin 404
After the proxy SSO handoff the page URL is already /admin, so url().replace(/\$/,'')+'/admin' produced /admin/admin (HTTP 404) and the logout probe found no control. Resolve /admin against the origin with new URL() so it always lands on <origin>/admin, whose navbar carries the native logout link.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:43 +02:00
kevinveenbirkenbachandClaude Opus 4.8 bb8536a680 fix(web-app-akaunting): wait for the Vue login form before submitting native credentials
The native-login block was skipped when the non-waiting isVisible check ran before the Vue form mounted, so the admin never authenticated and the company-settings logo upload never happened (bucket stayed empty). Wait for the email field + Vue hydration, fill and submit, then confirm the URL leaves /auth/login.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:43 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c220a5a674 fix(web-app-baserow): move the injected floating logout fallback to bottom-left
When no header/nav anchor exists the logout affordance is appended to document.body as a fixed-position control. Move it from top-right to bottom-left so it no longer overlaps Baserow's top-right '+ Add new' action, and drop a duplicate display:inline-flex.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:43 +02:00
kevinveenbirkenbachandClaude Opus 4.8 477dac8157 fix(web-app-yourls): expose ADMIN_NATIVE_PASSWORD for the no-SSO native admin login
YOURLS' native admin authenticates against the role-local credentials.administrator_password, not the global user password. Export it as ADMIN_NATIVE_PASSWORD (env keys kept in ASC order) so the admin persona's native-login fallback can sign in at /admin/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:43 +02:00
kevinveenbirkenbachandClaude Opus 4.8 66c70d2f52 fix(web-app-openproject): make the no-SSO admin native login work; skip biber without an account
ensure_admin_user.rb sets the admin password only when the user is new or the current password does not match (avoids re-validating against former-password rules on every redeploy) and clears force_password_change so native login does not hit the change-password interstitial. playwright.env.j2 blocks the biber persona in the no-SSO variant where biber has no account.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:42 +02:00
kevinveenbirkenbachandClaude Opus 4.8 740c384622 fix(web-app-decidim): point ActiveStorage direct uploads at the public S3 endpoint
ActiveStorage presigned the browser PUT against the internal AWS_ENDPOINT (http://seaweedfs:8333), which the browser cannot reach and CSP disallows, so avatar uploads never landed in the bucket (objects 161->161). Use the public S3 endpoint (objstore public_url minus the bucket path) and allow the SeaweedFS S3 host in connect-src.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:42 +02:00
kevinveenbirkenbachandClaude Opus 4.8 b8e6e8ea38 fix(web-app-erpnext): render the Keycloak SSO button on /login under gunicorn --preload
gunicorn runs --preload, so workers fork before the Social Login Key is applied and serve /login from a stale provider cache that clear_cache() (a separate one-shot process) cannot refresh. apply_oidc_settings.py now reports whether the key changed; 03_oidc.yml restarts the backend only on a real change (no steady-state bounce). The e2e helper asserts the SSO button is visible and clicks it instead of falling back to the 500ing oauth2_logins callback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:42 +02:00
kevinveenbirkenbachandClaude Opus 4.8 6cf8e3cb30 fix(test-e2e-playwright): add a no-SSO native-login fallback to the admin persona
When SSO is disabled the OIDC step is a no-op, so drive the role's own username/password form with :visible selectors (skips hidden anti-autofill decoys such as snipe-it's password_fake) and a path-walk over /login,/admin/,/admin. The landing-page probe waits 10s; per-path probes 5s so a slow form at / is not abandoned. Fixes no-SSO admin journeys (snipe-it, yourls, openproject).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:42 +02:00
kevinveenbirkenbachandClaude Opus 4.8 41f12cb42c fix(test-e2e-playwright): seaweedfs check requires a NEW object key by default; opt-in mtime for single-slot consumers
Background workers (PeerTube transcode, Pixelfed media jobs) rewrite existing objects during the poll window, so accepting any changed mtime made those consumers false-green. Default is now a new key only (the original count-growth semantics); single-slot consumers that overwrite one fixed key in place (fider tenant logo) pass expectInPlaceOverwrite to also accept a changed mtime.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:42 +02:00
kevinveenbirkenbachandClaude Opus 4.8 52112cbf86 style(lint): satisfy ruff in the rails-runner lint (PIE810 tuple startswith, drop redundant nested if)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:42 +02:00
kevinveenbirkenbachandClaude Opus 4.8 46aa32faef test(lint): forbid inline Ruby in Ansible 'rails runner' tasks — require an external .rb fed via a file lookup (nocheck: rails-runner-inline)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 11:30:42 +02:00