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>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-09-14 15:28:38 +02:00
1 parent a605c0aa79
commit f47ea0e5bb
4 files changed
+38 -5

No files matched your search

+17 -1
View File
@@ -1,5 +1,21 @@
---
- name: "🔨 Bootstrap the stack host image"
include_tasks: stack_host.yml
vars:
DISCOURSE_LAUNCHER_ACTION: rebuild
when: IS_STACK_HOST | bool
listen: rebuild-discourse
- name: "📝 Record that the bootstrap already recreated the container"
ansible.builtin.set_fact:
DISCOURSE_REBUILT: true
listen: rebuild-discourse
- name: "🖥️ Handle stack host"
include_tasks: stack_host.yml
when: IS_STACK_HOST | bool
vars:
DISCOURSE_LAUNCHER_ACTION: start
when:
- IS_STACK_HOST | bool
- not DISCOURSE_REBUILT | bool
listen: recreate-discourse
@@ -25,9 +25,9 @@
- DISCOURSE_DATABASE_IS_SHARED | bool
- DEPLOYMENT_MODE == 'compose'
- name: 🔨 rebuild discourse
- name: "🔨 {{ DISCOURSE_LAUNCHER_ACTION }} discourse"
shell: >
./launcher rebuild {{ DISCOURSE_CONTAINER }}
./launcher {{ DISCOURSE_LAUNCHER_ACTION }} {{ DISCOURSE_CONTAINER }}
{% if DOCKER_IN_CONTAINER | bool %}--skip-prereqs{% endif %}
args:
executable: /bin/bash
+17 -2
View File
@@ -1,4 +1,10 @@
---
# Exception: set_fact outlives the play, so the guard the recreate handler
# reads has to be cleared per role run or a stale true silently skips it.
- name: "🧹 Clear the bootstrap marker of an earlier play"
ansible.builtin.set_fact:
DISCOURSE_REBUILT: false
- name: ⬇️ Pull Discourse docker repository
git:
repo: "{{ DISCOURSE_REPOSITORY_URL }}"
@@ -10,7 +16,7 @@
retries: 12
delay: 30
until: discourse_repo_pull is succeeded
notify: recreate-discourse
notify: rebuild-discourse
become: true
- name: 🔧 set chmod 700 for '{{ DISCOURSE_CONTAINERS_DIR }}'
@@ -26,6 +32,15 @@
mode: "0640"
notify: recreate-discourse
# Exception: launcher reads env: on every docker run but consumes templates,
# params and hooks only during bootstrap, so only their digest may rebuild.
- name: "🔑 Record the digest of everything the bootstrap consumes"
stack_host_copy:
content: "{{ lookup('template', 'config.yml.j2') | from_yaml | dict2items | rejectattr('key', 'eq', 'env') | items2dict | to_json | hash('sha256') }}"
dest: "{{ DISCOURSE_BOOTSTRAP_DIGEST_DEST }}"
mode: "0640"
notify: rebuild-discourse
- name: "✅ Verify that '{{ DISCOURSE_CONTAINER }}' is running"
ansible.builtin.command:
argv:
@@ -40,7 +55,7 @@
register: docker_ps
changed_when: docker_ps.stdout.strip() == ""
failed_when: docker_ps.rc != 0
notify: recreate-discourse
notify: rebuild-discourse
- name: 📂 Load discourse handlers explicitly
include_tasks: utils/load/handlers.yml
+2
View File
@@ -30,6 +30,8 @@ DISCOURSE_CONTAINERS_DIR: "{{ [ DISCOURSE_REPOSITORY_DIR, 'containers' ] | path_
DISCOURSE_APPLICATION_YML_DEST: "{{ [ DISCOURSE_CONTAINERS_DIR, DISCOURSE_CONTAINER ~ '.yml' ] | path_join }}"
DISCOURSE_BOOTSTRAP_DIGEST_DEST: "{{ [ DISCOURSE_CONTAINERS_DIR, DISCOURSE_CONTAINER ~ '.bootstrap.sha256' ] | path_join }}"
DISCOURSE_LDAP_ENABLED: "{{ lookup('config', application_id, 'services.ldap.enabled') }}"
DISCOURSE_LITELLM_ENABLED: "{{ lookup('config', application_id, 'services.litellm.enabled') | bool }}"