fix(approach-c): full E2E success on marcelle - byte-identical templates + core-only recreate
This session completed Approach C end-to-end on marcelle (status=COMPLETED,
mkdocs untouched, idempotent on re-run). Four fixes landed:
1. template-engine.ts: dropped nginx/conf.d/*.hbs (default, api, services)
from renderAllTemplates AND renderAllTemplatesInMemory. The new
prod-style docker-compose.yml.hbs does NOT mount conf.d/ into the
nginx container ("Note: conf.d is NOT mounted (configs are generated
at startup from templates)" — nginx confs are baked into the nginx
Docker image). Writing them was a no-op orphan that showed up as 3
"modified" lines in preview unnecessarily.
Same reason removed nginx/nginx.conf from staticFiles.
2. templates/configs/{pangolin/resources.yml,prometheus/prometheus.yml,
grafana/datasources/datasources.yml}.hbs: synced byte-identical to
canonical changemaker.lite/configs/*. These ARE mounted into pangolin
tunnel + prometheus + grafana respectively. Preview now reports
"unchanged" for them on install.sh tenants.
3. templates/docker-compose.yml.hbs: dropped the CCP-tenant header
comment, making the template now BYTE-IDENTICAL (58907 bytes) to
canonical changemaker.lite/docker-compose.prod.yml. Even a 1-byte
comment difference caused docker compose to compute new config hashes
for every service, triggering full-stack recreates (including
ccp-agent — the Phase 6 self-destruct trap from upgrade.sh).
4. upgrade.service.ts:runReleaseUpgrade — composeUp now restricted to
core app services [api, admin, media-api, nginx] (same set as
image-upgrade.sh). Unscoped composeUp would recreate ccp-agent
mid-apply and orphan the runner. Until Approach C inherits the
deferred-ccp-agent-restart pattern from upgrade.sh, this restriction
keeps the apply path safe. Limitation: brand-new services in a
release won't auto-deploy via Approach C alone — operator must
follow with Approach A (full upgrade.sh) to pick them up.
E2E verification on marcelle:
- Apply: status=COMPLETED, duration<10s.
- mkdocs.yml md5 unchanged (38810d9df8b4258ad46a6739232cf88a).
- mkdocs/docs file count unchanged (242).
- docker-compose.yml now byte-identical to canonical (58907 bytes).
- app + api public sites: 200 both.
- Re-preview: ALL 10 files show "unchanged" — true idempotency.
Phase 6 acceptance gate met. Approach C now fully operational on the
install.sh fleet.
Bunker Admin
This commit is contained in:
@@ -243,12 +243,15 @@ export async function renderAllTemplates(context: TemplateContext, outputDir: st
|
||||
|
||||
const templatesDir = path.resolve(__dirname, '../..', 'templates');
|
||||
|
||||
// Templates that produce on-disk files actually consumed by tenant containers.
|
||||
// nginx/conf.d/* templates removed 2026-05-23: the new (prod-style)
|
||||
// docker-compose.yml does NOT mount conf.d/ into the nginx container
|
||||
// ("Note: conf.d is NOT mounted (configs are generated at startup from
|
||||
// templates)" — nginx confs are baked into the nginx Docker image).
|
||||
// Writing them was a no-op orphan.
|
||||
const templateFiles = [
|
||||
{ template: 'docker-compose.yml.hbs', output: 'docker-compose.yml' },
|
||||
{ template: 'env.hbs', output: '.env' },
|
||||
{ template: 'nginx/conf.d/default.conf.hbs', output: 'nginx/conf.d/default.conf' },
|
||||
{ template: 'nginx/conf.d/api.conf.hbs', output: 'nginx/conf.d/api.conf' },
|
||||
{ template: 'nginx/conf.d/services.conf.hbs', output: 'nginx/conf.d/services.conf' },
|
||||
{ template: 'configs/pangolin/resources.yml.hbs', output: 'configs/pangolin/resources.yml' },
|
||||
{ template: 'configs/prometheus/prometheus.yml.hbs', output: 'configs/prometheus/prometheus.yml' },
|
||||
{ template: 'configs/grafana/datasources/datasources.yml.hbs', output: 'configs/grafana/datasources/datasources.yml' },
|
||||
@@ -272,9 +275,10 @@ export async function renderAllTemplates(context: TemplateContext, outputDir: st
|
||||
logger.debug(`Rendered ${template} → ${outputPath}`);
|
||||
}
|
||||
|
||||
// Copy static files (no templating needed)
|
||||
// Copy static files (no templating needed). nginx/nginx.conf removed
|
||||
// 2026-05-23 — same reason as nginx/conf.d/* in templateFiles above
|
||||
// (nginx image bakes its own; on-disk file is an orphan).
|
||||
const staticFiles = [
|
||||
'nginx/nginx.conf',
|
||||
'configs/prometheus/alerts.yml',
|
||||
'configs/alertmanager/alertmanager.yml',
|
||||
'configs/grafana/dashboards/dashboards.yml',
|
||||
@@ -311,12 +315,15 @@ export async function renderAllTemplatesInMemory(
|
||||
const templatesDir = path.resolve(__dirname, '../..', 'templates');
|
||||
const result: Array<{ relativePath: string; content: string }> = [];
|
||||
|
||||
// Templates that produce on-disk files actually consumed by tenant containers.
|
||||
// nginx/conf.d/* templates removed 2026-05-23: the new (prod-style)
|
||||
// docker-compose.yml does NOT mount conf.d/ into the nginx container
|
||||
// ("Note: conf.d is NOT mounted (configs are generated at startup from
|
||||
// templates)" — nginx confs are baked into the nginx Docker image).
|
||||
// Writing them was a no-op orphan.
|
||||
const templateFiles = [
|
||||
{ template: 'docker-compose.yml.hbs', output: 'docker-compose.yml' },
|
||||
{ template: 'env.hbs', output: '.env' },
|
||||
{ template: 'nginx/conf.d/default.conf.hbs', output: 'nginx/conf.d/default.conf' },
|
||||
{ template: 'nginx/conf.d/api.conf.hbs', output: 'nginx/conf.d/api.conf' },
|
||||
{ template: 'nginx/conf.d/services.conf.hbs', output: 'nginx/conf.d/services.conf' },
|
||||
{ template: 'configs/pangolin/resources.yml.hbs', output: 'configs/pangolin/resources.yml' },
|
||||
{ template: 'configs/prometheus/prometheus.yml.hbs', output: 'configs/prometheus/prometheus.yml' },
|
||||
{ template: 'configs/grafana/datasources/datasources.yml.hbs', output: 'configs/grafana/datasources/datasources.yml' },
|
||||
@@ -334,9 +341,9 @@ export async function renderAllTemplatesInMemory(
|
||||
result.push({ relativePath: output, content: rendered });
|
||||
}
|
||||
|
||||
// Read static files into memory
|
||||
// Read static files into memory. nginx/nginx.conf removed 2026-05-23 —
|
||||
// same orphan reason as in renderAllTemplates above.
|
||||
const staticFiles = [
|
||||
'nginx/nginx.conf',
|
||||
'configs/prometheus/alerts.yml',
|
||||
'configs/alertmanager/alertmanager.yml',
|
||||
'configs/grafana/dashboards/dashboards.yml',
|
||||
|
||||
@@ -651,14 +651,24 @@ async function runReleaseUpgrade(
|
||||
});
|
||||
await driver.composePull(instance.basePath, instance.composeProject);
|
||||
|
||||
// Phase 4: recreate services
|
||||
// Phase 4: recreate services. Restricted to core app services (api,
|
||||
// admin, media-api, nginx) — same set as scripts/image-upgrade.sh.
|
||||
// Calling unscoped composeUp would recreate ccp-agent too, severing the
|
||||
// CCP↔agent connection mid-apply and orphaning the script (same trap as
|
||||
// upgrade.sh Phase 6 self-destruct fixed in v2.10.2). Until we mirror
|
||||
// upgrade.sh's deferred-ccp-agent-restart pattern here, stick to the
|
||||
// explicit service list. Limitation: Approach C will not pick up
|
||||
// brand-new services in a release until either (a) operator runs full
|
||||
// upgrade.sh (Approach A) afterwards, or (b) this is upgraded to the
|
||||
// deferred-restart pattern.
|
||||
const coreServices = ['api', 'admin', 'media-api', 'nginx'];
|
||||
await updateStatus({
|
||||
currentPhase: 4,
|
||||
phaseName: 'Recreate Services',
|
||||
percentage: 80,
|
||||
progressMessage: 'Recreating services with new orchestration...',
|
||||
progressMessage: `Recreating core services (${coreServices.join(', ')})...`,
|
||||
});
|
||||
await driver.composeUp(instance.basePath, instance.composeProject);
|
||||
await driver.composeUp(instance.basePath, instance.composeProject, coreServices);
|
||||
|
||||
// Phase 5: verify (best-effort; soft warnings only)
|
||||
await updateStatus({
|
||||
|
||||
Reference in New Issue
Block a user