Commit Graph
10 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5 f47ea0e5bb fix(discourse): bootstrap only for what the bootstrap consumes
Three notifiers pointed at one handler, and that handler always ran
./launcher rebuild. A rotated database password therefore cost a full image
build with bundle install and asset precompile: 961.82s in pass 1 and 13m16s
in pass 2 of job 103878400427, on a run that then missed its wall-clock
budget by 201s.

The launcher draws the line itself. set_template_info reads app.yml on every
call and turns env: into -e flags that run_start hands to docker run, while
templates:, params: and hooks: are consumed during bootstrap only. An env:
change needs a new container, not a new image. run_start takes the docker run
branch only when no container exists, which is why the start path still stops
and removes it first.

The digest task is the discriminator: it records a sha256 over every top-level
key except env:, so a credential-only change leaves it untouched and the
bootstrap handler is never notified. A hash rather than the content because
that section carries the OIDC client secret and the administrator password.

The guard against running both paths lives in a fact cleared per role run. A
register from inside the included file would have outlived the play and, in a
later one, silently skipped the recreate while the config change never reached
the container.

from_yaml adds no failure mode: the launcher parses the same file with
YAML.load, so anything it rejects was already a broken deployment. start
without an image is structurally impossible, since a running container
implies its image and a missing one notifies the bootstrap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 15:28:38 +02:00
kevinveenbirkenbachandClaude Opus 5 7162b7bf4c Merge fork/feature/svc-net-tor into feature/ai-agent-employees
834 commits from the Tor onion-service branch against 299 of ours, from merge
base 7b646bdece: 127 conflicted files, 3506 files changed.

The two sides are largely orthogonal. Theirs adds svc-net-tor and a `tor` service
across the role tree, plus two sweeping refactors: shipped scripts sorted by
language (files/ruby/, files/python/, files/shell/, scripts/lint/*.sh) and the
unit suite moved under tests/unit/python/. Ours is requirement 036, the native AI
gateway, plus the MCP role work. Most conflicts were both sides appending to the
same service map, resolved as unions.

Resolutions that were not unions:

Credentials moved namespace. Theirs renamed meta/schema.yml to meta/secrets.yml
and re-addressed credentials as secrets.credentials.<name>. Git carried our
litellm_api_key blocks through the rename, but every consumer we added still read
'credentials.<name>', which no longer resolves. All 45 call sites are migrated.

plugins/filter/container_volumes.py is deleted by theirs and relocated to
plugins/lookup/container_volumes.py, where it carries the same tmpfs defect our
f85996b43 had just fixed: a hard-coded {"size": ...} that silently drops a per
mount `mode`. The fix is re-applied in the relocated file, as that commit's merge
note anticipated.

compose.yml keeps only INFINITO_PLAYWRIGHT_REPORTS_BASE_DIR. Theirs also forwards
INFINITO_PLAYWRIGHT_STAGE_BASE_DIR; that line is deliberately dropped, per the
analysis recorded in 45804e9ad -- STAGE has a drift-proof role-vars fallback,
while forwarding it promotes a setdefault-pinnable .env value above that SPOT.

tasks/stages/01_constructor.yml takes their include, ./tasks/utils/setup/group.yml,
because their refactor removed tasks/groups/*-roles.yml, leaving our include
target nonexistent. Our task name is kept, since svc-virt is in the loop.

Makefile swarm-zombie is a union: their validate plus teardown, then our
lab_subnet cleanup, which their side dropped while leaving lab_subnet.sh as the
only orphaned reference in the file.

01_bootstrap.sh keeps our deterministic --ip pinning, which distinguishes an
already-attached node from a real failure, over their `|| true`.

Their language-sorted layout orphaned references our side still held: zammad's
two scripts/apply/*.rb, jenkins' java-with-ca-trust.sh, and the notify topics
their handlers renamed to hyphenated form. All corrected.

roles/sys-svc-compose/files/shell/swarm/stack_ready.sh gains our latched-update
block: paused, rollback_paused, rollback_started and rollback_completed exit 2
rather than polling a state that cannot resolve without another deploy.

config_from_run is unioned to (title, logged, jobs); the production caller in
trigger/__main__.py now passes jobs, without which our suite suppression would
have been inert.

READMEs are regenerated rather than hand-merged: 15 LiteLLM rows and 91 Tor rows
now derive from the merged metadata, which is itself evidence the meta merge is
coherent.

Verified: 5161 unit tests pass. The single failure, test_query.py TestSortSpec,
also fails on fork/feature/svc-net-tor at b51d2f9919, measured in a baseline
worktree -- so this merge introduces no new unit-test failure. Two modules are
excluded from that run because `baudolo`, a dependency their branch introduces,
is not installed on this host.
Not verified: no deploy has run against the merged tree, and make quality has not
been run end to end here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 16:32:30 +02:00
kevinveenbirkenbachandClaude Opus 5 2c5ad70d24 feat(discourse): register the platform gateway as the Discourse AI model
Discourse AI is configured through an LlmModel row rather than settings, so the
addon upserts one via rails: provider open_ai, the gateway's chat-completions
URL, the model the gateway serves, and this role's virtual key. It then binds
that row to ai_default_llm_model and to the bot, because a model that exists
but is bound to nothing serves no surface.

llm_models.url is POSTed verbatim, so DISCOURSE_LITELLM_CHAT_ENDPOINT carries
the full /v1/chat/completions path rather than a base URL. Every other consumer
in this requirement takes a base and appends; getting this one wrong produces a
404 at prompt time, long after a green deploy.

Three things had to be cleared before any of that could work.

Discourse refuses to call a private address unless the host is in
allowed_internal_hosts, and it fails as an opaque provider error rather than as
a blocked request. The script appends the gateway host to that list and leaves
existing entries alone, which matters because the OIDC bootstrap in
config.yml.j2 appends the discovery host to the same pipe-separated setting.
Both sides check membership before adding, so they compose instead of
clobbering each other.

The launcher-managed container sits outside the compose graph and does not join
the gateway network on its own. handlers/stack_host.yml adds
DISCOURSE_LITELLM_NETWORK to its network list and config.yml.j2 passes the
matching --network flag, both only while litellm is enabled.

The bootstrap aborted before the plugin could install at all. after_code cloned
every enabled plugin unconditionally, but a bundled plugin already sits in
$home/plugins, git exits non-zero on the clone, and the whole bootstrap stops.
discourse-ai is bundled in current images, so it now skips plugins whose source
is bundled.

mem_limit goes from 2g to 4g. `rails r` boots a second Rails inside the
container already running unicorn and sidekiq, and at 2g the kernel kills it
mid-run: measured in deploy-036-11 and again in deploy-ai-v2-7, where the task
died with "Killed" and the deploy ended at exit 137. Upstream documents 4 GB as
Discourse's own minimum, so this corrects an under-provisioned limit rather than
working around the task. The variant resource budget still passes.

Verified: the OOM with the exact task and exit code across two independent runs,
the bundled-plugin abort, and that the budget test holds at 4g.
Not verified: the registration has never completed, because every attempt so far
died at the OOM first. The eight-fact read-back assertion and
addons/discourse-ai.spec.js are both entirely unexercised, the spec because the
addon bridge gate had it skipping.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-24 13:23:58 +02:00
kevinveenbirkenbachandClaude Opus 5 a46348b6cf refactor(handlers): bind notify to a listen topic, not to a name
A handler name was doing two jobs at once: the line an operator reads in the
play output, and the identifier a `notify:` binds to. The two pull in opposite
directions -- the log wants "restart the SSH service, with diagnostics", the
binding wants a token that survives quoting, YAML flow style and a shell
round-trip -- and a name that changes for readability silently changes which
handler runs.

Every handler that a notify reached by name now carries a `listen:` with that
exact wording, so the binding is explicit before anything is renamed. The
topics themselves become identifiers: `compose up` reads `compose-up`,
`Restart NFS ganesha` reads `restart-nfs-ganesha`, 22 of them across both
sides of every notify. Where the topic is assembled at runtime, both branches
moved together: the conditional in sys-service carries two literals, and the
LDAP renderer interpolates `import-{{ folder }}-ldif-files`, which is what
makes those handlers reachable at all.

Freed from the binding, the names take the emoji prefix every other task line
already has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 03:38:42 +02:00
kevinveenbirkenbachandClaude Fable 5 94e8671d61 refactor(container): route docker ops through the container CLI
community.docker.docker_* module calls in role tasks bypassed the CA-aware container wrapper and split lifecycle handling across two mechanisms. Migrate lifecycle sites (rm/stop/start/run/build, network create) to the container CLI, keep read-only *_info modules with an inline nocheck, and add a lint that rejects new raw module usage. The mariadb recovery flow gains a stale-container pre-remove so an aborted reset stays re-runnable, discourse teardown keeps its graceful stop, and removals guard on 'No such container' instead of masking every failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 00:00:48 +02:00
kevinveenbirkenbachandClaude Opus 4.8 61b75f7505 feat(web-app-discourse): classify idempotent network errors via docker_network_noop
The five docker network connect/disconnect tolerances were inline stderr substring chains, inconsistent between sites (the handler variants matched case-sensitively without a default). The docker_network_noop filter is the single marker source per operation; disconnect additionally tolerates absent networks/containers, which is the proven swarm failure ('No such container: postgres' when the central DB is a swarm service). Drop the now-orphaned DISCOURSE_ERROR_NOT_CONNECTED.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 22:18:20 +02:00
kevinveenbirkenbachandClaude Opus 4.8 1a10cf843d fix(discourse): tolerate 'already attached to network' on idempotent connect
container network connect is not idempotent; on re-run the engine may report 'already attached to network' in addition to the already-handled 'already exists in network'. Widen the failed_when of both stack_host network-connect handlers (shared-DB connect + overlay connect) to tolerate it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 02:29:28 +02:00
kevinveenbirkenbachandClaude Opus 4.8 7b446940f5 chore(web-app-discourse): autoformat reflow of stack_host overlay loop
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 06:22:26 +02:00
kevinveenbirkenbachandClaude Opus 4.8 76935d1cbe fix(web-app-discourse): give the swarm launcher a node-local bridge network
The discourse launcher is a standalone container (docker run via ./launcher), not a swarm service, so it cannot attach to swarm-scoped overlay networks. In swarm it was left without a reachable network for its shared pg/redis/openresty peers.

The shared network util now creates a node-local bridge instead of an overlay when the role declares networks.local.force_bridge in meta/server.yml, read via the config lookup with a false default so every other caller stays byte-identical. Discourse sets force_bridge: true and connects the rebuilt launcher to the shared overlays (web/pg/redis) so swarm services stay reachable; the launcher config drops the explicit --network flags in swarm.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 00:50:53 +02:00
kevinveenbirkenbachandClaude Opus 4.8 2c4298d00f fix(web-app-discourse): create the role network before the launcher and gate it to the stack host
The standalone Discourse bootstrap (./launcher rebuild) attaches --network={{ DISCOURSE_NETWORK }} (=entity_name 'discourse'), but that overlay was never created in swarm, so the rebuild aborted on every node with 'failed to set up container networking: Could not attach to network discourse: rpc error: NotFound desc = network discourse not found'. Create the role network before the launcher via sys-svc-compose network/create (overlay attachable, run_once + delegate STACK_HOST in swarm; bridge in compose), move the launcher tasks to 04_launcher gated on IS_STACK_HOST, and split the recreate-discourse handlers into stack_host.yml fired by a single IS_STACK_HOST-gated 'Handle stack host' handler that listens on 'recreate discourse', matching the established sys-service/sys-svc-sshd/svc-prx-openresty handler-include pattern. Gate the shared-db network connect/disconnect (05_network) on IS_STACK_HOST too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 23:15:39 +02:00