The adapter flush notifies `compose up --force-recreate`, which recreates
every service in the project including the Jenkins controller. The contract
probe then ran 12 seconds after that recreate, while Jenkins was still
booting, so the upstream answered 401 and the probe reported the adapter as
refusing its own credential.
Extract the readiness wait so it can run twice and repeat it after the
flush. Bind the adapter to the controller with service_healthy as well:
that is what keeps gitea's sidecar out of this race, and swarm ignores the
condition, so the wait carries that mode.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The 180s start period expired while Matomo was still installing, so the
supervisor killed and restarted it on a 300s cadence and the role never
reached a healthy container.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The three credential tasks exec into the container the moment the service
reports up, which races the directory's own startup and fails the run.
Retry until the exec succeeds instead.
The first task also ran slappasswd with the bind password as an argument
and the second carried the resulting hash, both in clear text in the task
result, so both now honour MASK_CREDENTIALS_IN_LOGS.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Iterating on one app meant restarting the whole stack or reaching for raw
docker. The target mirrors compose-app-logs and goes through the
repository's own container wrapper.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
REDIS_EXEC_SHELL passed -e REDIS_PASSWORD=<value>, which puts the secret
into every rendered command and into the process list inside the
container. The container already carries REDIS_PASSWORD from its own env
file, sourced from the same credential, so the flag added exposure and
nothing else; an authenticated redis-cli ping from inside proves it.
REDIS_ACL_PASSWORD keeps its explicit -e: it is a per-consumer secret the
container does not hold.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
matomo, moodle and nextcloud keep the database password inside a config
file on their data volume. When the deploy rotates credentials, the
container gets the new password while its persisted config still carries
the old one, and the app fails to authenticate against its own database.
Patch the persisted file before the health gate reads it. The value goes
through the environment rather than the command line, and sed_escape
guards the delimiter, backslash and ampersand that would otherwise corrupt
the file the replacement is meant to repair.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The controller serves its MCP plugin endpoint to anonymous callers: the
deploy probe measured a 200, and the response carried the base64 of
'anonymous' in its reporting endpoint. The JCasC authorization strategy
grants anonymous nothing, so the plugin does not hang off it.
Front it with the svc-ai-mcp-adapter instance. Clients present mcp_bearer
to compose service jenkinsmcp and get a 401 otherwise; the adapter
presents the controller's own API token upstream over HTTP Basic.
Narrow the allowlist to the 15 read-only tools and record the 19 the
plugin serves as upstream_serves. The four write tools were advertised
while mutating_tools_enabled stayed false, so the adapter would have
refused them at call time: a dead surface a green deploy hides.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gitea-mcp sidecar holds no credential of its own and expects each
client to supply a Gitea token per request, so it answers initialize and
tools/list to anyone: the deploy probe measured a 200 disclosing the full
tool inventory. No sidecar setting closes that, because serving the
unauthenticated is how the upstream is built.
Front it with the svc-ai-mcp-adapter instance instead. Clients now present
mcp_bearer to compose service giteamcp and get a 401 otherwise; the
adapter presents the minted Gitea token upstream to giteamcpupstream,
which keeps the sidecar image but drops it to the role's own network.
Pin the 24 read-only tools the sidecar serves, captured from the running
instance, so the served surface cannot widen unnoticed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Compose interpolates a bare $ in an inline environment value, so a tool
whose input schema carries a "$schema" key reaches the adapter with that
key renamed to the empty string. The tools mapping then hashes differently
than meta/mcp.yml pinned, assert_no_drift refuses to serve, and the
container crash-loops with a symptom three steps from its cause.
Only web-app-jenkins carries a $ today; the other twelve render the
contract identically, so the same capture would have broken any of them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sys-svc-mcp-reconcile sets application_id from a loop variable, so the
lookup received the literal {{ mcp_client.id }}. An unrendered id matches
no allowed_consumers, which rejected every provider as
consumer_not_allowed and emptied each client's registry without raising.
Pull the resolution into resolve_consumer_id() so the branch is reachable
from a test, and cover the three cases: a deferred id is rendered, a plain
id is kept, a missing id aborts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The restore cycle stops every subject of the host, replays into the database
services alone, and starts the rest again at the very end. Any exit in
between skips that last step, so the host keeps its subjects down - and
nothing recorded it.
The next deploy then failed on a symptom of the teardown rather than on
whatever ended the drill. It cost two runs: once 'rake db:migrate' against a
database the cycle had emptied but not yet replayed, once
Redis::CannotConnectError because the cycle never restarted redis. Both times
the message pointed away from the cause, and fixing what it named would have
fixed nothing.
A trap armed before the teardown now records the teardown and the exit code,
and mode=update refuses to run while that record stands. purge=true and
mode=reinstall pass, because both rebuild the state the drill destroyed; a
completed cycle clears the record itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The id leads, the variant follows the name it belongs to, and covered_by,
weight and the new clone column close the row. ✂️ said only that a row was cut
while two different reasons hide behind it: 🔰 points at the earlier row that
already embeds this one, 🐑 says the row shares its service set with an earlier
role that stands in for it. 27 rows of a full plan carry 🐑 and were until now
indistinguishable from the rest.
_COLUMNS is the order and the headers again. tor and triggered used to sit
outside it as special cases, so a reordering had to be made in two places and
the tests indexed by hand; they now ask _COLUMNS for a column's position.
The chunk column loses its ⏭️ marker, which named the block the calling job was
running. Without it cells() has no use for `current` and --chunk no effect, so
both go, along with the workflow's argument.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
swarm-zombie filtered the act matrix on mode:swarm but left the workflow's
modes input at its default, so discovery drew rows from every mode and
assigned each one by its position in the candidate list. For svc-db-postgres
that put variant 0 on compose and variant 1 on swarm, while the matrix filter
asked for variant 0 on swarm - a pair that assignment never produces.
act then had no matrix entry for the deploy job, ran discover alone and exited
0. The drill never happened, which is what roundtrip.sh's completion-marker
guard reports:
swarm-zombie exited 0 but the drill never completed for svc-db-postgres
Two filters were deciding the same thing from different ends: ACT_MATRIX picks
from the finished rows, ACT_INPUTS decides which rows exist at all. Pinning
modes=swarm makes every row of the whitelist a swarm row, so variant 0 is one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7.0.1 stops the --empty pre-clean from listing an extension's own objects for
a one-by-one DROP that postgres refuses. Without it a discourse generation is
unreplayable: the role declares the vector extension, its functions belong to
the application role rather than to a superuser, and the pre-clean's owner
filter therefore selected them.
That aborted the restore under ON_ERROR_STOP, which is where the backup drill
stood after the dump itself started working again.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Containerised lint and deploy runs write .ansible/ in the worktree: fetched
collections, modules, roles and the lock ansible-lint holds for the duration
of a run. It showed up as untracked in every git status, and its lock is
written by the container's user, so a run that dies leaves a root-owned file
behind that the next status still reports.
.dockerignore is generated from this file, so it moves along.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
a2789f7bb was proven by a deploy run and nothing else. The pair here states
both halves of the rule: the engine holding the dump's volume does not block
its own replay, and a real consumer beside it still aborts - naming that
consumer and not the engine.
The second case is what keeps the first honest. On its own the first would
also pass if the guard were widened until it stopped refusing anything.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
assert_no_consumers refuses a replay while the compose project still runs
containers, because a booting consumer recreates the schema the pre-clean
just dropped. The restore cycle brings exactly one container up on purpose:
the engine the dump reaches through docker exec. The cluster path already
excluded it; the per-dump path did not.
For an application whose database carries its own name - discourse's database
is 'discourse' inside project 'discourse' - the running container is
discourse-database, so the engine reported itself as the blocker and the
drill could never replay a single-database dump.
The engine is resolved from the dump's volume, which the replay does anyway
a few lines later, so an engine that is down fails the same way as before,
only earlier.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wait_service_terminated guards two opposite moments with one rule. The call
before the forced backup only has to wait out a run still in flight, while
the calls after a systemctl start read the verdict on the code under test.
Both treated 'failed' as fatal, so a unit left failed by an earlier deploy
ended the drill in the pre-flight - after 0.13s, reporting a state three
hours old as if it were this run's result.
That made the drill blind from its first failure onward: it could never start
another backup, so every defect behind the first one stayed unreachable.
The pre-flight now clears such a state and carries on. After a start of its
own, 'failed' stays fatal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7.0.0 resolves a databases.csv instance from one engine-name set instead of
an inline regex, which closes two gaps 6.0.0 left. A container named exactly
after its engine resolves again without a declaration, and the central
MariaDB is dumped under swarm for the first time: its task reads
mariadb_mariadb.1.<id>, which matched neither the static name passed through
--database-containers nor any token the suffix match knew.
It also stops recording an application container as a database. Refusing its
dump alone would have written the volume to the manifest as
database: true, dumped: false, which is the shape cab5e2523 makes the restore
drill fail on - the fix for one defect would have tripped the check for
another.
Both pins move together because the version that writes a generation and the
version that reads it must agree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
d130f5559 moved the swarm converge wait onto compose_mode_force-or-
DEPLOYMENT_MODE and left the compose health wait beside it reading
DEPLOYMENT_MODE raw. A role pinned with compose_mode_force: compose on a
swarm cluster then satisfied neither condition - the compose branch saw
'swarm' and the swarm branch saw 'compose' - so it got no post-deploy
readiness wait at all. web-app-matrix in its ansible flavour is the live
case.
The database lookup read DEPLOYMENT_MODE raw for the same reason, handing a
pinned role a swarm service name and a resolve-container-id address for a
stack that docker compose had created.
The override is scoped to the dedicated database only. A dedicated engine
runs inside the consumer's own stack, so the consumer's pin governs it; a
central engine is its own stack under the cluster mode, and letting a
consumer's pin reach it would rename a service swarm really did create.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
json.loads(path.read_text(...)) plus its marker ran to 155 characters, so ruff
format split the inner call across three lines and left the comment sitting
after the closing parenthesis, where the lint no longer reads it as belonging
to the file read. Splitting the read out of the parse keeps the call and its
marker together on one line within the limit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6.0.0 forwards PGPASSWORD into the container, so an engine whose pg_hba
demands a password on TCP loopback is dumped instead of refusing every
attempt, and it stops resolving an application container onto its own
dedicated engine's databases.csv row, where a dump used to start and write a
file that looked like a backup and held none of the data.
Both pins move together because the version that writes a generation and the
version that reads it must agree: pyproject.toml covers the control host
running utils/recovery, the role covers the target host that writes.
6.0.0 also requires a database container whose name carries no database
token to be declared through --database-containers. Every container this
repository produces is already covered: the central engines are declared via
backup.database_routine, dedicated engines are named <entity>-database, and
bigbluebutton names its own bigbluebutton-postgres-1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
i8cifail died twice on the same run, each failure hiding the next.
The retrigger recovers a source run's dispatch inputs from a job log, because the title truncates at 512 bytes and REST answers inputs: null for a finished workflow_dispatch. That reader picked the first depth-1 job outright. A skipped job never uploads a log blob, so its log endpoint answers BlobNotFound as a bare HTTP 404 -- and a force-cancelled run can leave every depth-1 job skipped, which is why deeper jobs are now read too rather than assumed inputs-free. Candidates are taken shallowest first, so the orchestrator's own inputs still win over what a caller forwarded. gh 2.97 also refuses to emit a log holding terminal escape sequences and exits 1; the log downloader read that as no log and silently wrote none. A missed fetch is now tolerated instead of aborting, so the guard in dispatched_priority reports an expired log in words rather than the command dying on gh: HTTP 404.
Behind that sat the resolver: it decoded whole priority entries, but an entry may pin axes (web-app-zammad#1@compose+tor), which the display-name codec does not parse. Every pinned entry aborted as corrupt. split_axes -- already the seam decode_list uses -- separates name from suffix, so only the name is resolved and the truncation guard stays sharp. The pin rides along into the retrigger: the entry never ran, so the run holds no evidence its axes were wrong.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The test climbed to the repository root itself. Moving the file one directory would have left that index pointing somewhere else, and the test would then fail with FileNotFoundError instead of saying anything about compose.py. Its own package already defines PROJECT_ROOT and every sibling test module imports it from there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The api probe asked for the bare root, and funkwhale does not serve '/' locally: SPAFallbackMiddleware fetches the page back over the instance's own public URL. In swarm that closed a cycle - api healthy needs front up, front's nginx needs to resolve api:5000 at parse time, and swarm withholds the service DNS record until a task is ready. The failing run shows all three links: the api traceback ending in 'HTTPError: 502 for https://.../index.html', openresty logging 'funkwhale_front could not be resolved', and front dying on '[emerg] host not found in upstream "api:5000"'. No timeout can break that; the task just reached FailingStreak 3 and was replaced by one that repeated it.
api/v1/instance/nodeinfo/2.0 is answered inside the container. Read from funkwhale/api:2.0.9 itself: config/urls mounts ^api/ -> v1/ -> ^instance/ onto instance/urls.py's ^nodeinfo/2.0/?$, so the path is exact and the optional slash rules out an APPEND_SLASH redirect; NodeInfo20 declares empty permission_classes and authentication_classes; and EXCLUDED_PATHS = ['/api', ...] makes should_fallback_to_spa() return False for it, so the SPA round trip cannot be reached from here at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The task name broke the 80-char convention; naming the consumer alone says as much, since the enclosing role already names the application.
The manifest read opts out of the shared read cache rather than joining it. utils.cache.files pulls in PROJECT_ROOT and the gitignore reader, which would break the import-free rule utils/recovery/manifest.py exists under, and a process-wide cache is wrong for this file in any case: a backup run writes a new generation that the same process then reads back, so the cached copy would be the stale one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Output of 'make autoformat': ruff collapses expressions that were wrapped across lines without need, shfmt and the yaml formatters drop surplus blank lines. No semantic change - the whole diff is 10 insertions against 39 deletions, and every hunk joins lines that already said the same thing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ruff B905 flagged zip(comments, spans) without strict=. spans is built by a comprehension over comments, so the two can only ever be the same length; strict=True can never fire and records that, while a future edit that breaks the pairing now fails loudly instead of silently truncating the shorter side.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SuiteCRM 8 renders its logout as <a class="dropdown-item global-links-sublink">Logout</a> inside <scrm-logout-ui>, and that anchor has no href, no role and no ngbdropdownitem - unlike every sibling in the same user menu (Edit Profile, Employees, Admin, About), which all carry one. An anchor without href gets no implicit link role, so getByRole('link'|'menuitem'|'button') cannot see it and neither can any href-based selector. Every existing candidate therefore missed a control that was on the page.
Read out of the failing run's Playwright trace: the dropdown did open - the Logout node is present in the frame snapshots - and the helper still clicked its 26 menu triggers without ever matching it. The earlier sibling fix widened the dropdown render wait, which this branch already carries via resolveTimeout(1_500); the wait was not the problem.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
baudolo dumps only what databases.csv names, and the entry was seeded from the central branch alone. An application running its own engine - Discourse with svc-db-postgres absent from the group - therefore never reached the seed: databases.csv stayed empty, baudolo warned that it was continuing without dumps, and the volume was stored as a raw PGDATA tree that restores a torn cluster.
Whether a database must be backed up is a property of there being one, not of it being central, so both branches now include the same helper with their own gate: centrally the host from the provider group seeds, dedicated the stack host itself. The coordinates come from the same lookup either way.
Verified without a deploy that the row this writes is the row baudolo looks for: the seed records the consumer entity as the instance, and baudolo derives the same key from the container name (discourse-database -> discourse). A mismatch there would have filled databases.csv and still produced no dump.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The payload check accepted a files/ tree OR an sql dump, so a database whose dump never happened and which was copied as raw engine files passed it. That is exactly what occurred: Discourse's postgres landed as PG_VERSION/base/pg_wal and the drill reported green, while restoring that replays a torn cluster rather than a dump.
The generation's manifest is now the authority on which volumes held a database and which of those carry a dump; generations written before baudolo recorded that are judged by the engine's on-disk footprint instead. Demonstrated against both: the manifest branch catches a volume the footprint heuristic cannot see, and leaves a non-database volume alone.
seed/marker.py reads dumps and therefore needs baudolo, but ran under a bare python3, which Ansible's PATH resolves to the sealed project venv while sys-pip-install installs into the system interpreter - so it died at import and the entire restore half of the drill never ran. It now uses BKP_TEST_PYTHON, the interpreter the role installed into.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A recent refactor imported baudolo into utils.recovery for three layout strings, and cli.administration.recover.__main__ imports full unconditionally - so the whole recovery CLI, --help included, died at import on every host without baudolo. That is most of them: recover device|nfs|secrets runs on swarm nodes, the NFS server and bare rescue hosts, while only the backup node carries svc-bkp-volume-2-local. utils/recovery had been deliberately stdlib-only for exactly that reason.
baudolo 5.0.0 writes a manifest into every generation stating the layout it used, so the names are now asked of the generation being read rather than of the installed package. A tree written by any version is walked with the names that version used, and utils.recovery.layout answers only for generations older than the manifest, which cannot describe themselves. That also retires two of core's guesses: the '<entity>_database' naming convention now yields to the engine the run actually recorded.
A repo whose manifest cannot be read is skipped rather than guessed at, but no longer takes the other repos with it - recovering two of three beats recovering none because one manifest is corrupt. The exit code reports the gap.
The pin lint compared two declared version strings and was therefore green while a different distribution was installed. It now checks the installed distribution and the layout literals against baudolo's own BackupPaths.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The per-entry group serialized independent role deploys and kept jobs
queued instead of cancellable. Commented out with a TODO so the group key
can be reworked before it comes back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
compose strips outer quotes when it loads an env file; docker stack deploy does not, so a hand-written quote reaches the container as part of the value. The dotenv_quote filter exists for exactly that split - compose-style escaping of $, backslash and double quote in one mode, verbatim passthrough in the other - and five values were still quoting themselves.
LDAP_PROVIDER_FILTER also had '={username}' sitting outside the quotes, so the filter would have seen half the value; it is now concatenated before the pipe. DB_DUMP_PATH and MAIL_AUTO_EMBED_METHOD are literal constants without special characters and need no quoting at all, so their quotes are simply gone - a dump path arriving as "'/usr/bin'" would not have found mysqldump.
The repository lint only rejects double quotes (KEY="..."), so none of these was visible to it. No single-quoted value remains under roles/.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
zammad: the probe sat on the 'zammad' entity while the compose template applied it to the railsserver service by literal name. Both entities carry name: zammad-railsserver, so they are one container - but 'zammad' declares the app/proxy port 8080 and railsserver declares 3000, which is what Rails binds. The probe therefore addressed a port nothing listens on inside that container. The declaration moves to railsserver and the template looks up service_name, so entry and consumer can no longer drift.
The flavor becomes connect: a bare socket open instead of an HTTP request over /dev/tcp, with a 15m start_period instead of the 30s the tcp flavor inherits. The role's own readiness task budgets five minutes for the same boot, so the old probe declared the container dead while the code beside it was still waiting.
seaweedfs: flavor curl without a path probes the root, which the S3 gateway does not answer. tasks/00_core.yml has queried http://127.0.0.1:8333/status with until rc == 0 since it was written, and api_port equals ports.internal.http, so the probe now asks what the role already knows to be the ready signal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The target pins apps, variant and mode into ACT_MATRIX. When the generated matrix holds no such row, act matches nothing, deploys nothing and exits 0 - a job that reports green while drilling nothing, which is how the mariadb zombie ran since e846063f5.
cli.meta.ci.validate already answers exactly that question for a selection token, down to naming the alternative ('offered: compose', 'declares 2 variant(s)'), so the target asks it before tearing a cluster down rather than reimplementing the check in the recipe.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
clone and covered_by both name a stand-in row: another row carries this one's coverage, so the sweep may drop it. Both were decided over every row and then applied to a filtered set, so a run that selected a subset deferred to a stand-in the filter had already removed - the row was cut in favour of a deploy that never happens. --whitelist svc-db-mariadb yielded no row at all, and the swarm-zombie job passed green while drilling nothing.
The sort's last-resort tie-breaker was drawn per process, so discovery and the plan renderer could name different assignments for the same run. INFINITO_DISCOVERY_SEED derives it from the row's own identity instead; the workflow feeds github.run_id, so the rotation still happens, once per run rather than once per process.
Measured against the previous order: on the mode-only filter both cut sets are identical (swarm 220 rows, 44 cut; compose 243 rows, 54 cut), so the full sweep is unaffected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
seed/token.py sat on sys.path beside the scripts that ran it, so any import chain reaching tokenize picked up the drill's seeder instead of the stdlib's token module - dataclasses -> inspect -> tokenize -> token is enough to trigger it. marker.py names what the file does and collides with nothing.
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>
Two collectors run over a .j2 template, so the closing '#}' of a Jinja
block was also a standalone '#' comment to the line-based one. The
header rule protects the opening '{#' but not a closer 22 lines down,
so the fragment got reported, deleted, and the template ended up inside
an unterminated comment: it rendered to nothing while every parser
stayed happy.
_drop_nested drops any comment whose line span is a proper subset of
another's, so the enclosing comment always wins.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The linter only saw the working-tree diff, so 3529 invalid comments in
528 files never broke a build. It now enumerates git ls-files, and the
policy holds for the whole repository.
Two changes ride along, both required to make that scope survivable:
_css_comments never closed a /* */ block on the line that opened it, so
every single-line CSS comment swallowed the following code as its body.
The reported spans were unusable for anything but counting.
A single comment line directly above a key in defaults/*.yml,
vars/*.yml or group_vars/ now counts as that variable's doc, the way a
docstring counts above a function. Two lines are a block again and stay
flagged, and the carve-out holds nowhere else.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The rule only fired one way: a stack role with named volumes had to carry
the flag, but a role without any volume could carry it unnoticed. That
entry pulls svc-bkp-volume-2-local into the role's dependency graph and
schedules a backup run over a role with nothing to back up.
_scan() now classifies every invokable role instead of skipping the ones
that need no flag, and returns both halves. The nocheck opt-out still
exempts a role from both directions.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
df1c65c made --repo-name mandatory because its old default was the literal 'backup-docker-to-local' while the help promised the git folder name. This role never passed the flag and lived off that default, so every backup run under 4.0.0 died in argparse with exit 2, which systemd reported as status=2 and which aborted the deploy of the role and everything behind it.
The value is not free: cli/administration/recover/full.py looks for volume generations under exactly that directory name, and it is the same string the removed default produced, so backups already on hosts stay discoverable. The header documents that binding, because nothing else makes it visible.
Found by the first real deploy. The gap it came through is that no test renders BKP_VOLUME_2_LOCAL_EXEC against the actual binary - test_baudolo_snapshot.py exercises the launcher with invented argv, and baudolo's own suites do not know this repo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
svc-ai-ollama 0.32.13->0.32.14, svc-bkp-volume-2-local's drill scratch alpine 3.21->3.24, penpot's frontend/backend/exporter 2.17.0->2.17.1 in lockstep because they speak one internal interface, prometheus alertmanager v0.33.1->v0.34.0, seaweedfs 4.41->4.42, semaphore v2.19.7->v2.19.8, snipe-it v8.7.0->v8.7.1-alpine.
Declared only - none of these images was pulled or started. alertmanager is the sole minor bump and the one whose configuration could break; the alpine jump spans three releases but carries only the drill's find -delete and ls -A, which the next drill run exercises anyway.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
run_one.yml stats roles/<role>/files/test/test.sh and fails when it is absent, but the runner guide and the nfs-server TODO both said files/test.sh. The TODO was the costlier of the two: following it would have produced a script the asset check never finds. It now also names templates/test.env.j2, without which discover_cli_roles never picks the role up at all and the test stays silently unrun.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4.0.0 renamed --dump-only-sql and rejects the old spelling with exit 2, so the timer's assembled exec string would have failed on every run against the version the previous commit pins. The launcher fixtures and the role README carry the same rename; nothing else in the repo still says it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
utils/recovery restated three things baudolo owns: the generation layout (suffixes, sql/ and files/ directories), the \connect parser for cluster dumps, and the databases.csv reader. Each was a second implementation of a contract whose author is one pip install away - the connect parser had even received the same quoted-identifier fix twice, once per repo.
The layout constants are now derived from BackupPaths, the inventory comes from dump_inventory, and the CSV goes through read_rows. dump_inventory is stricter than the parser it replaces: it also reads CREATE DATABASE lines, so a database a stream creates but never reconnects to is found - a new test pins that. recover/full.py and the drill's token.py take their globs from the same constants instead of spelling them out.
The dependency this creates is declared: backup-docker-to-local==4.0.0 in the base dependencies, because the deploy CLI must import from the base set alone. The role installs the same version, and a lint holds the two pins together - they drifted apart is exactly the failure this whole commit removes, so it must not come back one level up.
The generation-layout tests now write their fixtures through BackupPaths itself; before, the fixture hand-built the paths with the same assumption the test claimed to verify.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5bb803054 tied the dnsmasq listener to group membership instead of the deploy mode, but left these assertions behind, so the branch carries two red tests. A Tor node runs the same listener in compose and in swarm, so a swarm node now expects the bridge resolver first and the clearnet upstream behind it, and the same node without an upstream expects the bridge alone rather than nothing.
The empty list did not lose its case, only its reason: it belongs to a node that is neither on Tor nor given a clearnet resolver, which now has a test of its own.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>