Skip to content

feat(mac): canonical Mac bring-up + configurator/mcp/digit-ui from-source fold-ins — consolidates #14/#40/#46/#47/#48/#50 on nairobi - #52

Closed
KDwevedi wants to merge 43 commits into
ChakshuGautam:nairobifrom
KDwevedi:feat/canonical-mac-bringup
Closed

feat(mac): canonical Mac bring-up + configurator/mcp/digit-ui from-source fold-ins — consolidates #14/#40/#46/#47/#48/#50 on nairobi#52
KDwevedi wants to merge 43 commits into
ChakshuGautam:nairobifrom
KDwevedi:feat/canonical-mac-bringup

Conversation

@KDwevedi

@KDwevedi KDwevedi commented May 20, 2026

Copy link
Copy Markdown
Collaborator

What this PR is

One consolidated bring-up PR that absorbs PR #14, #40, #46, #47, #48 (close on merge) and lets a Mac operator (or Linux operator) onboard a new tenant by editing host_vars/<tenant>.yml and running ./deploy.sh <tenant> — no script-on-top.

Cherry-picked from the 6 source PRs into 16 ordered commits off current nairobi HEAD (b4b0d99). Linux behavior is byte-identical — every Mac branch is either when: ansible_system == 'Darwin' or default(omit).

Commit map

# Commit Source
1 fix(ansible): render STATE_LEVEL_TENANT_ID from inventory PR #40 + 2 sibling env vars (PARENT_LEVEL_TENANT_ID, DIGIT_TENANT) — same shape, found during audit
2 mac-port: thin macOS path — skip host-provisioning, containerize nginx mac-port-thin c2a8aae. Includes the preflight flip: extends a80b11b's fail when os_family != 'Debian' to fail when os_family != 'Debian' AND ansible_system != 'Darwin', and updates the message to point Mac operators at ansible_connection: local instead of "spin up a VM". Empirical headless OrbStack stack works fine without the VM layer (verified this session).
3 mac-port: OS-conditional become 602f749
4-7 mac-port: gate Wait-MCP-health / Show-MCP-status / Newman / Fail-if-CI-failed 9791d7e, 7a50652, 0d4771b, 1d0844b
8 mac-port: robust Rosetta stack-up via mac-stack-up.sh converge loop 397f4dc (creates the converge engine)
9 mac-port: route post-OpenBao recreate through the Darwin converge too 8d715ea
10 mac-port: re-unseal OpenBao after converge#2 (Darwin) 92d52db
11 mac-port: mac-stack-up.sh — live converge progress, SKIP_DOWN, idempotency, leaked-endpoint fail-fast Squashed 7daa3fc + dd91350 + cfbb420 (iterative improvements to the converge script). Also fixes a converge#2 task signature gap (missing NET arg) and a duplicate when: clause from cherry-pick merging.
12 mac-port: gate JVM Rosetta-stability flags on Darwin 0d66f93
13 mac-port(nginx): add digit_ui_mode=="container" branch (fixes login 504) 383d026
14 chore(deploy.sh): fail fast if ansible-playbook not on PATH PR #48 9cf48a6 (resolved against banner: precheck FIRST, then banner)
15-16 skills: encode validated Mac-port + onboarding gotchas (×2) PR #47 ab102f7 + a274131

Empirical grounding

Before cutting this branch I ran tenant_bootstrap pg → expt against the local Mac/OrbStack db_fast_path stack (same image both branches pull: egovio/mdms-v2:v2.9.2-4a60f20):

schemas_copied: 30/31     schemas_failed: 1  ← Workflow.BusinessServiceMasterConfig has NULL x-unique in pg source data
data_copied:    365/366   data_failed:    1  ← idempotency duplicate (ACCESSCONTROL-ROLES.roles)
workflows_created: 1
adminUser.provisioned: true

The cast bug we feared (JSONObject cannot be cast to JSONArray) did not fire on the bootstrap path. MCP's tenant_bootstrap is already 99.7% functional on db_fast_path. The remaining gaps (UserValidation synthesis, ACCESSCONTROL-ACTIONS bridge, StateInfo rebrand, citymodule data, root locales) are tracked in a follow-up PR against ChakshuGautam/DIGIT-MCP — out of scope here.

That empirical result is what made it possible to ship a Mac-onboarding PR without rewriting the bootstrap. We just need ansible to call MCP (which this PR enables on Mac by dropping the false enable_mcp: false premise's underlying reason).

What this PR does NOT do (deferred)

Closes

Stays open

How to verify Mac

brew install ansible       # if missing
# install OrbStack or Docker Desktop, ensure it's running
cd local-setup/ansible
cp inventory/host_vars/_example.yml inventory/host_vars/maputo.yml
$EDITOR inventory/host_vars/maputo.yml   # set state_tenant_id, domain, secrets
./deploy.sh maputo
# tail -f /tmp/mac-stack-up.progress  in another terminal for live converge view

Linux deploys via Tailscale/Hetzner are unchanged — every Mac branch is Darwin-gated or default(omit).


Update — also folds in configurator, MCP, and digit-ui (build-from-source)

Beyond the original Mac-port consolidation, this branch now makes the deploy build all three external repos from source at deploy (clone-at-deploy; no vendored snapshots, no pinned/stale images), via one consistent pattern:

  • configuratorbuild_configurator: truefiles/configurator-build.sh (clone digit-configurator + vite build). Supersedes feat(configurator): import digit-configurator as configurator/ snapshot #50 (the configurator-snapshot import) — same goal, build-from-source instead of vendoring.
  • MCPbuild_mcp: truefiles/mcp-build.sh (clone DIGIT-MCP + docker builddigit-mcp:local). No GHCR / multi-arch dependency.
  • digit-uibuild_digit_ui: truefiles/digit-ui-build.sh (clone digit-ui-esbuild + npm run build), laid into the container via tar (OrbStack-safe; bind-mounting the build dir breaks the nested globalConfigs.js file-mount).

Plus deploy-hardening validated on macOS/OrbStack against Maputo (mz/mz.maputo):

  • config-driven UserValidation + ACCESSCONTROL-ACTIONS bridge; ADMIN provisioned with a target-valid mobile (companion DIGIT-MCP PR).
  • xlsx onboarding boundary entity-gate (wait for entity visibility before relationships — fixes the OrbStack Kafka-lag race that dropped 174 relationships).
  • locale: keep locale_region: IN (en_IN localization, zero seeding); phone prefix from core_mobile_configs.mobilePrefix (decoupled in the from-source digit-ui).

Closes #50 (configurator snapshot — superseded by build-from-source).

Update 2 — pulls in the notification fixes

Folds the notif line into this PR (conflict pre-checked; resolved inter-PR overlaps):

Closes #41, #42, #43, #44.

Deferred: #39 (otp-publisher service) — its notif seed diverged from #41 (seed.sh −168, template-bindings −43); reconciling the unified seed is a notif-domain follow-up. WIP #37/#38 held.

🤖 Generated with Claude Code

KDwevedi and others added 24 commits May 15, 2026 22:39
Fixes from a from-scratch deploy of the notification stack on bomet
(`ke.bomet`, WhatsApp via Twilio production sender). Three buckets:

## digit-config-service env defaults

Added MDMS-v2 reachability so `_create` validation works:

  MDMS_V2_HOST: http://egov-mdms-service:8094
  MDMS_V2_SCHEMA_SEARCH_PATH: /mdms-v2/schema/v1/_search
  STATE_LEVEL_TENANT_ID: ${STATE_LEVEL_TENANT_ID:-pg}

Without these the service defaults to `application.properties`
literals (`http://localhost:8083`) which is itself, and every
ConfigData create returns `MDMS_VALIDATION_FAILED: Could not fetch
config schema from MDMS v2: I/O error on POST request`.

## novu-bridge env path correction

  NOVU_BRIDGE_CONFIG_RESOLVE_PATH: /config-service/config/v1/_resolve
  NOVU_BRIDGE_CONFIG_SEARCH_PATH:  /config-service/config/v1/_search

Was: `/config-service/v1/_resolve` (missing `/config/` segment).
config-service mounts its controller at `/config/v1` AND has
SERVER_SERVLET_CONTEXT_PATH `/config-service`. The earlier value got
Spring's "No static resource v1/_resolve" 500 because the controller
mapping wasn't matched.

## novu-bridge channel + preference defaults

  NOVU_BRIDGE_CHANNEL:           ${NOVU_BRIDGE_CHANNEL:-whatsapp}
  NOVU_BRIDGE_PREFERENCE_ENABLED:${NOVU_BRIDGE_PREFERENCE_ENABLED:-false}

- Channel is now case-significant lowercase to match the TemplateBinding
  `channel` field convention (`whatsapp` / `sms`). Upstream was
  `WHATSAPP` and our seed records have lowercase — the lookup criteria
  didn't match.
- Preference check defaults to disabled. The `digit-user-preferences-service`
  on the preview image 404s `/user-preference/v1/_check` (controller
  path mismatch). Enabling it without the controller mapping wired
  breaks every dispatch. Flip to `true` once preferences are properly
  configured for a tenant.

## nginx template additions

Added Novu workarounds and Vite-base-/ bug mitigations:

- `/novu-ws/` proxy to port 14003 (websocket) so the dashboard can do
  same-origin WS via /novu-ws/ instead of port-14003 cross-origin.
- `/assets/`, `/static/`, `/images/`, `/manifest.json`,
  `/favicon-gradient.svg`, `/favicon.ico` proxies to novu-dashboard.

  ⚠ The /assets/, /images/ etc. blocks are WORKAROUNDS for an upstream
  Vite-build bug in `ghcr.io/novuhq/novu/dashboard:2.3.0` — the image
  is built with base=/ so its HTML references absolute root paths,
  ignoring runtime VITE_BASE_PATH. Without these blocks the dashboard
  404s on every asset load. PROPER FIX (out of scope here): rebuild
  the dashboard image with VITE_BASE_PATH=/novu baked in at build
  time, then drop these blocks.

- `/auth/` proxy to dashboard for React Router internal routes
  (e.g. /auth/sign-up). MUST come before any Kong /auth route or
  hard-refreshes at /auth/* go to Kong's keycloak upstream which
  500s with DNS resolve failure when keycloak isn't deployed.

  ⚠ Same Vite-build-bug story. PROPER FIX: rebuild image so internal
  routes are emitted as /novu/auth/* and shadowed by /novu/.

## digit.env.j2: single-origin WS

NOVU_WS_PUBLIC_URL now points at `{scheme}://{domain}/novu-ws`
(routed via the new nginx block) instead of `{scheme}://{domain}:14003`.
Single-origin keeps cookies and CORS coherent; the port-direct version
also broke with TLS termination at nginx because the upstream WS isn't
TLS-served on 14003.

## Out-of-scope (worth tracking separately)

1. **Bridge `whatsapp:` prefix bug.** `novu-bridge`'s
   `DispatchPipelineService` prepends `whatsapp:` to the recipient
   `to.phone` regardless of `NOVU_BRIDGE_CHANNEL`. With channel=sms
   this produces Twilio error 21910 ("Invalid From and To pair"). The
   fix is in the Java source — outside the docker-compose / Ansible
   scope of this PR. Tracked: needs a follow-up PR against the bridge
   image source to gate the prefix on `equalsIgnoreCase("whatsapp")`.

2. **Kong `keycloak-route` shadowing /auth.** Unrelated to notif but
   surfaced via the dashboard /auth/sign-up flow. Either gate the
   route on an `enable_keycloak` flag in kong.yml, or have ansible
   strip it when the deployment doesn't deploy keycloak. For now the
   nginx /auth/ block above takes precedence so functionally fine.

## Verified end-to-end

After these fixes plus ChakshuGautam#37 seed corrections, the chain dispatches on
ke.bomet to a real Indian WhatsApp number via Twilio production:

  /user-otp/_send → Kafka → bridge → config-service → Novu →
  novu-worker → bridge-endpoint → Twilio → recipient (status: read).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… events

Seeds the TemplateBinding and ProviderDetail records that wire the PGR
complaint-lifecycle events and OTP.SEND to the Novu workflows, so the
novu-bridge can resolve a workflow + provider per (tenant, event,
channel, locale).

Folds the original seed (was WIP ChakshuGautam#37) together with the corrections
that aligned it to live config-service behavior (was ChakshuGautam#41), as one
coherent change on nairobi:

- Data records POST to config-service /config/v1/_create/<schema>,
  not mdms-v2 — config-service has its own eg_config_data table and is
  not a pass-through; records seeded via mdms-v2 were invisible to the
  bridge. Schemas are still registered in mdms-v2 (config-service
  validates creates against them).
- seed.sh captures the full /oauth/token response and includes
  userInfo in every RequestInfo (config-service NullCheckException
  without it); deterministic uniqueIdentifier per schema; auto-detects
  Kong /config-service routing with CONFIG_SERVICE_URL fallback.
- TemplateBinding schema: locale moved to required (MDMS rejected it
  as x-unique-only with UNIQUE_ATTRIBUTE_LIST_ERR).
- template-bindings.json: empty contentSid keys dropped (schema
  pattern ^HX[a-fA-F0-9]{32}$ rejects empty string).
- README documents the config-service architecture + Content Template
  flow for production WhatsApp.

Supersedes ChakshuGautam#37 and ChakshuGautam#38 (WIP plans).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bridge had `whatsapp` hardcoded in three places, so even with
`channel=sms` in TemplateBinding/ProviderDetail the dispatch path
silently coerced everything into Twilio's WhatsApp pipeline:

  1. NovuBridgeConfiguration / application.properties / compose all
     defaulted to NOVU_BRIDGE_CHANNEL=WHATSAPP. Flipped to SMS — works
     out of the box with any Twilio SMS sender; WhatsApp requires a
     pre-approved Programmable WhatsApp sender that we don't ship.

  2. TwilioProviderStrategy unconditionally prepended `whatsapp:` to
     the sender number. Now keys off ResolvedProvider.channel:
     whatsapp → adds prefix, sms/other → strips any stray prefix that
     leaked in from MDMS data.

  3. DispatchPipelineService.formatWhatsappPhone() unconditionally
     prepended `whatsapp:` to the *recipient* number too. Renamed to
     formatRecipientPhone(), now takes channel and only prefixes for
     whatsapp. This was the load-bearing bug — Twilio rejected SMS
     trigger requests because the To address looked like a WhatsApp
     address.

  4. PreferenceServiceClient.isWhatsAppAllowed() hard-coded
     consent.get("WHATSAPP"). Renamed to isChannelAllowed(channel)
     and looks up consent.get(channel.toUpperCase()). Without this,
     a user with SMS=GRANTED but no WHATSAPP entry would be denied.

Verified end-to-end SMS delivery via Kafka pipeline on subhashini
(NOVU_BRIDGE_CHANNEL=SMS, Twilio account in trial mode, recipient
verified in Twilio console).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ved Content SIDs

The 5 lifecycle TemplateBindings (APPLY/ASSIGN/RESOLVE/REJECT/REOPEN)
now carry the real, Meta-approved Twilio WhatsApp Content SIDs that
already exist on the eGov account (EN, *_message_new family), instead
of leaving contentSid absent.

- channel sms -> whatsapp (these SIDs are WhatsApp-approved; the bridge
  resolves the whatsapp channel).
- paramOrder rewritten to map ComplaintsDomainEvent.data keys
  (serviceName/complaintNo/submittedDate/assigneeName/
  assigneeDesignation/departmentName/comment) onto each template's
  {{1}},{{2}},... positions, verified per-SID via the Twilio Content
  API rather than inferred from names.
- templateId unchanged — all 5 Novu workflow ids already exist in
  novu-bridge-endpoint/workflows.js.

OTP.SEND intentionally left on the hardcoded path (no contentSid,
channel sms, not routed): no twilio/authentication template exists on
the account and SMS-OTP needs CCRS ChakshuGautam#43 + an SMS-capable sender first.

README updated: lifecycle WhatsApp is now zero-touch on deploy; Hindi
SIDs exist but stay a follow-up (HI {{n}} order unverified); serviceName
ships as the PGR serviceCode (display-name resolution tracked
separately).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes complainant lifecycle coverage (5 -> 7). Both templates are
already Meta-approved on the eGov Twilio account; paramOrder verified
against the live Content API bodies:
- REASSIGN -> HX7dc390... (serviceName,complaintNo,submittedDate,
  assigneeName,assigneeDesignation,departmentName) — same shape as ASSIGN
- RATE -> HXa0ad0e... (serviceName,complaintNo,submittedDate)

Adds matching complaints-workflow-reassign / -rate Novu workflows in
novu-bridge-endpoint (the trigger needs the workflow to exist even
though WhatsApp content is rendered from the Twilio contentSid
passthrough). Requires a novu-bridge-endpoint redeploy + bridge
re-sync to go live; seed rows land on next deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rect live tenants)

A create-only seed cannot fix an already-deployed tenant: config-service
rejects a repeat _create for the same x-unique tuple with
DUPLICATE_RECORD, so a stale row (wrong channel / missing contentSid /
placeholder Twilio token) silently survives a re-run. Surfaced on Bomet
2026-05-16 — lifecycle bindings AND the Twilio ProviderDetail were stale
and the reseed was a no-op until corrected by hand.

- On DUPLICATE: _search the existing record, match on the x-unique DATA
  tuple (eventName,channel,locale / providerName,channel) NOT the
  uniqueIdentifier string (legacy rows carry a stale '.sms.' id whose
  data was later flipped to whatsapp), then _update by its id (which
  config-service _update requires).
- Probe for a base that can _search; Kong routes _create/_resolve but
  not _search/_update, so the upsert needs config-service directly.
  Falls back to create-only with a loud WARNING when only Kong is
  reachable (re-run in-cluster to apply corrections).
- Explicit per-record status: created / updated / SKIPPED / FAILED.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nant scoping

Two corrections surfaced deploying the producer to Bomet (2026-05-16):

- provider-details.json had channel=sms. The bridge dispatches on
  NOVU_BRIDGE_CHANNEL (default whatsapp, ChakshuGautam#42) and the lifecycle
  bindings are whatsapp, so an sms provider is never resolved
  (NB_PROVIDERS_NOT_FOUND) — WhatsApp lifecycle silently never sends.
  Set channel=whatsapp to match the bindings + bridge default.

- README: config-service resolves TemplateBinding/ProviderDetail by
  EXACT tenantId (no hierarchy fallback). On a single-city deploy the
  citizen UI files complaints under the root tenant (ke), not the city
  tenant (ke.bomet); seeding only ke.bomet makes every real complaint
  fail CONFIG_NOT_RESOLVED. Documented: seed at the tenant the PGR
  events actually carry.

Verified end-to-end on Bomet: real CCRS pgr-services emits
COMPLAINTS.WORKFLOW.APPLY (tenant ke) -> bridge resolves -> Twilio
WhatsApp delivered.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bridge resolves the citizen phone country-code prefix from MDMS
(ValidationConfigs.mobileNumberValidation) in formatRecipientPhone.
Its application.properties default is
`mdms.host=${MDMS_HOST:-http://mdms-v2.egov:8080}` — that hostname does
not resolve on the egov docker network. Earlier iterations set only
MDMS_SEARCH_PATH, leaving MDMS_HOST at the broken default, so every
complaint died with NB_MDMS_PHONE_PREFIX_UNAVAILABLE before reaching
Novu/Twilio.

Set MDMS_HOST=http://egov-mdms-service:8094 (same host the rest of the
stack uses). Verified end-to-end on Bomet: prefix now resolves (+254),
bridge -> Novu (201) -> Twilio; only delivery to a non-existent number
fails (Twilio 63024), as expected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…upstream pg/mz defaults

The compose file ships with `STATE_LEVEL_TENANT_ID: pg` (+ `pg.citya`
on the two city-tier services + a stray `mz` on inbox) from upstream
egovernments/CCRS sample data. The playbook copies the compose verbatim
and only rewrites the Grafana domain. Net result: every DIGIT service
comes up using `pg` for state-tier env vars on any tenant deployed
through this playbook, including naipepea and (when redeployed) bomet.

Adds an inventory field `state_root` (the actual country/state tenant
— `ke` for Nairobi/Bomet, distinct from the misleadingly-named
`state_tenant_id` which is the city tenant the SPA pins to) and four
`replace:` tasks that rewrite the upstream defaults:

  STATE_LEVEL_TENANT_ID: pg        -> {{ state_root }}
  EGOV_STATE_LEVEL_TENANT_ID: pg   -> {{ state_root }}
  STATE_LEVEL_TENANT_ID: pg.citya  -> {{ tenant_id }}      (city tier)
  STATE_LEVEL_TENANT_ID: mz        -> {{ state_root }}

Three `pg` defaults stay unrewritten because they match what the working
naipepea backup had pre-regression too — `EGOV_STATELEVEL_TENANTID` on
egov-indexer, `EGOV_STATELEVEL_TENANT` on egov-hrms, and
`PGR_STATELEVEL_TENANTID` on pgr-services. These appear to be unread
or differently-named env vars and were `pg` even on a working stack;
leaving them alone keeps the diff scoped to what actually breaks auth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a first-class macOS (Darwin) target to the canonical deploy without
disturbing the Linux/Debian path (all branches gated; Linux renders and
behaves byte-identically — verified via --syntax-check + diff review).

Preflight: now permits Darwin (was: fail on non-Debian); message tells
Mac operators to set ansible_connection: local in host_vars.

Skipped on Darwin (Docker Desktop/OrbStack already provides these):
  - Docker host install + /etc/docker + docker.service  (one guarded block)
  - apt-installed Node/Python/Playwright-deps (feature-flagged extras)

Containerized nginx on Darwin (the one real port): host-nginx tasks
wrapped in a Linux-only block; a sibling Darwin block renders the SAME
template with nginx_upstream_host=host.docker.internal (so the container
reaches host-published Kong/UI/MCP/etc ports) and runs nginx:1.27-alpine
with the configurator dist + globalConfigs.js bind-mounted. Template
upstreams parametrized: {{ nginx_upstream_host | default('127.0.0.1') }}
— Linux default unchanged.

OS-aware defaults (group_vars/all.yml; Linux values unchanged, host_vars
still overrides): digit_dir (~/digit on Mac, no /opt sudo),
configurator_www_dir, nginx_host_port. Play-level DOCKER_DEFAULT_PLATFORM
=linux/amd64 on Darwin (omit on Linux). Rosetta JVM stability flags
(prev commit) gate via MAC_JVM_OPTS, empty on Linux.

Most of the playbook-deploy.yml line delta is mechanical reindent from
wrapping two contiguous task clusters in `block:`; net new logic is the
guards + the Mac nginx block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Play-level become:true + ansible_connection:local on macOS forces sudo
prompts and root-owns ~/digit. No privileged host task runs on Darwin
(all gated off), so become=false there. Linux unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Full-path ./deploy.sh validation on Mac caught this: the MCP health
wait (curl :13101/healthz, 12x10s) had no enable_mcp guard, unlike the
mcp-bootstrap tasks. On Mac enable_mcp is false (digit-mcp image is
VPC-registry-only), so the playbook hung 2 min then FAILED on a
service it was never asked to run — blocking everything after
(OpenBao, Mac-nginx/configurator, validation). Add
when: enable_mcp | default(false), consistent with the sibling tasks.
Linux/VPC (enable_mcp true) unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Last full-path gap: fullpath7 passed EVERYTHING (converge x2, OpenBao
seal+re-unseal, Mac-nginx serving /configurator/ + /digit-ui/ at 200,
auth probe, MDMS StateInfo, INFRA VALIDATION) and failed only on the
trailing debug task 'Show MCP health status', which reads mcp_health.rc
— but mcp_health is registered only by the enable_mcp-gated wait task
(skipped on Mac), so it's undefined. Gate this status line the same
way, consistent with fix #2 and the mcp-bootstrap tasks. Linux/VPC
(enable_mcp true) unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ci_tests

Full-path validation: the deployment itself passed end-to-end
(converge x2, OpenBao seal+re-unseal, Mac-nginx serving
/configurator/ + /digit-ui/ @200, INFRA VALIDATION green), then
failed in the OPTIONAL post-deploy CI tail. 8 tasks (L1348-1450:
Install Newman, Postman digit-core-validation, Postman
complaints-demo, install pytest+dataloader deps, CRSLoader E2E +
results, CRSLoader v2 regression + results) had NO when: — they ran
regardless of run_ci_tests, right above the 'CI tests — skipped'
message. Wrong on Linux too (silently slow); on Mac the pip task
hard-fails (homebrew python lacks 'packaging'). Gate all 8 on
run_ci_tests | default(false), consistent with the rest of the CI
block. Deployment path unchanged.

7th and final issue the faithful ./deploy.sh validation caught — all
masked by the earlier manual-converge shortcut.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CI aggregator was the one ungated task left: its when: only
checked the rc vars, each *_result.rc|default(-1). With
run_ci_tests:false every CI sub-test skips → those vars are undefined
→ rc=-1 → the aggregator fired and failed the play even though all CI
tasks correctly skipped (skipped=74). Add run_ci_tests gate to its
when:, consistent with the entire rest of the CI block. Deployment
path unchanged; Linux with run_ci_tests:true judges real results as
before.

8th and final CI-gating issue the faithful ./deploy.sh validation
surfaced. With this, every CI/extras task is gated → run_ci_tests:false
cleanly skips the whole tail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bare up-retry was insufficient: under Rosetta the first up -d
aborts on slow depends_on health, leaving leaked network endpoints
that make every subsequent up collide ('endpoint <svc> already
exists'). New files/mac-stack-up.sh converges by disconnecting ONLY
non-running containers' leaked endpoints (running ones untouched, so
accumulated JVM warmth is preserved — no down) then re-issuing up -d,
looping until rc=0.

Start task split: Linux unchanged (plain up -d, when != Darwin);
Darwin runs the helper (when == Darwin). Confirmed-needed pattern —
same leaked-endpoint dance hit the personal-install rehearsal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Full-path validation caught this: after OpenBao seeds secrets into
.env, 'Recreate services with the new env' does a second plain
docker compose up -d. On Mac that cold-restarts the JVM containers
(env changed) and hits the same Rosetta depends_on timeout as the
initial start (mdms-backend exited 1 → dependency failed to start),
failing the playbook AFTER converge+OpenBao. Only the first start
task was converge-wrapped. Split this task the same way: Linux plain
up -d (unchanged); Darwin reuses mac-stack-up.sh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Full-path validation got all the way to the final INFRA-VALIDATION
gate then failed: 'OpenBao /v1/sys/health unsealed' -> 503
sealed/standby. Cause: the post-OpenBao recreate (converge#2,
mac-stack-up down+up) RECREATES the openbao container, which returns
SEALED; the earlier unseal ran before that recreate. Add a Darwin-only
re-unseal (wait for seal-status API, unseal with the persisted
bao_unseal_key) right after converge#2. Linux unaffected — its
post-OpenBao recreate is a plain up -d that doesn't recreate openbao.

5th issue the faithful full-path test caught (all masked by the
earlier manual-converge shortcut). Everything before this now passes:
converge#1, MCP-skip, OpenBao, converge#2, Mac-nginx serving
/configurator/ + /digit-ui/ (200), auth probe, MDMS StateInfo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tency, leaked-endpoint fail-fast

Squash of mac-port-thin commits 7daa3fc + dd91350 + cfbb420.

mac-stack-up.sh final shape: 14×40s retry loop, MAC_STACK_UP_SKIP_DOWN=1
for converge#2 (preserves infra + openbao state, halves deploy time),
healthy-skip short-circuit (idempotent re-entry), fail-fast on leaked
endpoints with explicit remedy, live progress to /tmp/mac-stack-up.progress.

Playbook + deploy.sh changes: SKIP_DOWN=1 on converge#2 invocation;
deploy.sh on Darwin gains a banner pointing at the progress sink so the
operator can tail it during the 10-40min Rosetta converge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apple-Silicon Macs run the amd64 JVM images under Rosetta, where the C2
JIT compiler SIGBUSes (CompileBroker::collect_statistics) and CDS mmap'd
archives also SIGBUS — egov-localization and friends crash ~1 min after
start. Bomet/naipepea never saw this because they're Linux.

Append `${MAC_JVM_OPTS:-}` to all 13 JAVA_TOOL_OPTIONS in
docker-compose.egov-digit.yaml (consistent with the file's existing
${VAR:-default} interpolation style; .env is auto-loaded from digit_dir).
digit.env.j2 sets MAC_JVM_OPTS to `-XX:TieredStopAtLevel=1 -Xshare:off`
only when ansible_system == 'Darwin', empty string otherwise.

Net: Linux targets get an empty suffix -> byte-identical JVM behavior,
zero production impact. Darwin targets get C1-only JIT + CDS off, which
eliminates the deterministic Rosetta SIGBUS (verified on the fork as
KDwevedi/CCRS#5; this is the canonical-safe, conditional form).

First commit of the thin Mac path (Approach A).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
nginx-site.conf.j2 only had `static` and `else`(HMR). maputo.yml sets
digit_ui_mode: container, which fell into the HMR else → `location /`
(incl. /user/oauth/token) proxied to :upstream_esbuild_port (the digit-ui
container, no API there) → 504 on every login, and globalConfigs.js was
never served so digit-ui rendered as the source tenant (pg).

Add a proper `container` branch: /digit-ui/* → the digit-ui container,
/digit-ui/globalConfigs.js → the per-tenant host file, filestore rewrite,
and ALL other paths (the API) → Kong. HMR/static branches unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… PATH

deploy.sh's final step is `ansible-playbook ...`. If ansible isn't on
PATH the script dies with a cryptic shell "ansible-playbook: command
not found" at that line and the operator has no idea why. Very common
macOS trap: `pip3 install --user ansible` installs ansible-playbook
into the Python user-base bin, which macOS does NOT add to PATH — pip
"succeeds" yet the binary is invisible.

Add an early `command -v ansible-playbook` guard that exits 127 with
the exact install/PATH fixes (brew / pipx / pip+PATH) and the verify
command. Hit live during a macOS onboarding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
From the 10-iteration faithful ./deploy.sh validation (PR
ChakshuGautam/CCRS#46) and the Feliciano Maputo dump.

digit-ansible-onboard:
- macOS is now a first-class target (thin Mac-port): added the Mac
  host_vars recipe (ansible_connection:local, deploy_become:false,
  docker_default_platform, digit_ui_mode:container, enable_mcp:false,
  configurator via vite v8.0.13 building client environment for production...
�[2K
transforming...✓ 1 modules transformed.), Rosetta deploy time, Mac probe.
  Removed the obsolete 'Mac → Ubuntu VM' framing.
- IRON LAW exception + Q9 + Step-4: db_fast_path pins THREE secrets
  (elasticsearch_master_password, postgres_password=egov123,
  minio_root_*=minioadmin), not just elasticsearch — a custom value is
  the #1 cause of a stack that comes up then fails the post-OpenBao
  recreate (cost the whole validation to pin down).
- Step-6: 'MCP SKIPPED (disabled)' is expected when enable_mcp:false,
  not a failure.

digit-xlsx-onboard:
- Step-2: pre-clean the recurring partner-dump defects (boundary
  dedupe+whitespace, Designation.department list, ServiceDefs.keywords
  string, Phase-1 local logo path).
- Step-5b: explicit conditional new-root tenant_bootstrap BEFORE the
  wizard (brand-new state root => zero schemas => Phase 3-4 fail).
- Step-6: prefer the forthcoming onboard_city composite; this skill
  becomes the narrative around it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Encodes the 5-hr live-spiral lessons: preflight-first, confirm runtime
(OrbStack vs Docker Desktop), never SKIP_DOWN for recovery + leaked-
endpoint engine-restart, the mdms-v2 _create platform defect (CCRS#49,
canonical MCP confirms) → SQL keystone is mandatory, nginx container
branch (CCRS#46), ansible-buffering progress file, citizen OTP handshake,
employeeCode-as-username (egovernments#67), Phase-2 reconcile (egovernments#68).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 20, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dde66289-6e8c-457b-9c65-4b9c15bac6d6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…ed green

Validated 2026-05-20 on Apple-Silicon Mac / OrbStack:
  ./deploy.sh maputo  → ok=63 changed=14 failed=0 skipped=84
  /egov-mdms-service/health  200
  /user/oauth/token (ADMIN/pg)  200 + real access_token
  /configurator/  200
  /digit-ui/  200
  /status/  200

MAC-QUICKSTART.md is the blank-dir Codex-CLI target: prerequisites,
clone-and-deploy in 6 commands, verification curls, troubleshooting.
Says nothing about kit scripts, sibling repos, or post-deploy patches.

host_vars/maputo.yml.example is committed (other host_vars/*.yml are
gitignored per canonical convention). Caller copies it to maputo.yml,
edits the marked fields, runs ./deploy.sh maputo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@KDwevedi

Copy link
Copy Markdown
Collaborator Author

Validated green on Mac/OrbStack — 2026-05-20 21:15

./deploy.sh maputo runs clean end-to-end on Apple-Silicon Mac with OrbStack:

PLAY RECAP *********************************************************************
maputo                     : ok=63   changed=14   unreachable=0    failed=0    skipped=84

Probes (all 200)

Path Status
/egov-mdms-service/health 200
/user/oauth/token (ADMIN/eGov@123/pg) 200 + real access_token
/configurator/ 200
/digit-ui/ 200
/status/ 200
Containers 36 up / 28 healthy (rest are nginx/proxies + exited-0 init jobs)

Iteration log (6 attempts, 5 real bugs caught + fixed)

# Caught Fix
1 ansible_system undefined at play-level Switch become + DOCKER_DEFAULT_PLATFORM from gathered fact to inventory vars (deploy_become, docker_default_platform) — commit a463f91a.
2 /opt/digit-ui-esbuild clone fails on Mac (/opt not writable under become:false) + irrelevant in container mode Gate the clone on digit_ui_mode in [static, hmr] AND non-Darwin — commit 99b4cf52.
3 egov-enc-service crash-loop JSONObject["tenant"] not found Was triggered by state_root: mz while mz doesn't exist in MDMS yet. Reverted to state_root: pg — host_vars only.
4 mac-stack-up.sh's converge#2 task missing NET arg + duplicate when: from cherry-pick auto-merge Fixed inside the squashed mac-stack-up commit 5226f731.
5 /mcp validate probe 502 Triggered by nginx_features.mcp: true while enable_mcp: false. Set both false in maputo.yml.example.

Files added in this round

  • MAC-QUICKSTART.md — the operator-facing one-pager (prereqs + 6 commands + verify + troubleshooting). Designed for the blank-dir Codex CLI scenario: a fresh operator can clone this repo and follow it without touching any sibling repos or kit scripts.
  • local-setup/ansible/inventory/host_vars/maputo.yml.example — validated host_vars template. Committed (other host_vars/*.yml stay gitignored per canonical convention).

Known scope limits (out for this PR)

  • MZ/Maputo tenant doesn't actually exist yet. State_root + state_tenant are pg for now. Creating mz / mz.maputo requires MCP tenant_bootstrap to seed the MDMS schemas+data+admin+workflow. enable_mcp: false here because ghcr.io/chakshugautam/digit-mcp is linux/arm64-only at GHCR but the Mac path forces linux/amd64 for JVM images. Multi-arch MCP image is upstream-fix territory (ChakshuGautam/DIGIT-MCP CI docker job needs platforms: linux/amd64,linux/arm64).
  • The 5 empirical tenant_bootstrap gaps (UserValidation synth, ACCESSCONTROL-ACTIONS bridge, StateInfo rebrand, citymodule data, root locales) — separate ChakshuGautam/DIGIT-MCP PR. Once that lands, a maputo.yml with state_root: mz + enable_mcp: true will fully seed a new tenant from scratch.

Net for #52

The 16+5=21 commit branch reaches the MAC-QUICKSTART.md user experience: clone → copy host_vars example → ./deploy.sh maputo → working DIGIT stack on Mac, no kit, no VM, no scripts. That's the consolidated deliverable.

🤖 Generated with Claude Code

KDwevedi and others added 2 commits May 20, 2026 21:19
…t in this PR)

Blank-dir Mac deploy from a wiped state surfaced: ~/digit/configurator/ is
empty after the playbook completes (no task in this PR builds the dist),
so /configurator/ returns 403 and the validate task fails.

digit-configurator is vendored separately in PR ChakshuGautam#50 (kept-separate per the
fold/supersede audit; 58k-line snapshot would drown ChakshuGautam#52's review surface).
Until ChakshuGautam#50 lands, operators have two paths:
  (a) leave configurator:false (default in the example now) — the rest of
      the stack still validates 200 across mdms/ui/status/auth
  (b) build digit-configurator dist externally, drop into ~/digit/configurator/,
      then flip nginx_features.configurator:true and re-deploy

MAC-QUICKSTART.md updated with the same guidance + an adjusted verify curl
sequence (4 probes instead of 5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Blank-dir test confirmed: from wiped volumes + empty ~/digit/ + fresh
clone to working stack, converge took 2m44s (21:13:19 → 21:16:03).
Updated MAC-QUICKSTART.md with the accurate timing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@KDwevedi

Copy link
Copy Markdown
Collaborator Author

🎉 Blank-dir scenario validated — MAC-QUICKSTART.md works end-to-end

Just ran the true blank-dir test the consolidated PR was scoped around:

# Setup: wiped all state
docker compose down -v --remove-orphans     # all containers + volumes gone
rm -rf ~/digit                              # working dir wiped
docker volume rm $(docker volume ls -q | grep digit)
docker rm -f $(docker ps -aq)              # any orphans

# Test: blank dir, fresh clone, follow QUICKSTART verbatim
mkdir -p ~/work/ccrs-blanktest && cd ~/work/ccrs-blanktest
git clone -b feat/canonical-mac-bringup https://github.com/KDwevedi/Citizen-Complaint-Resolution-System.git ccrs
cd ccrs/local-setup/ansible
cp inventory/host_vars/maputo.yml.example inventory/host_vars/maputo.yml
./deploy.sh maputo

Result:

PLAY RECAP *********************************************************************
maputo                     : ok=62   changed=14   unreachable=0    failed=0   skipped=85

mac-stack-up: CONVERGED on attempt 1/10 — 32/34 up-or-healthy
  21:13:19 START → 21:16:03 CONVERGED  (2m44s)

Probes (all 200):
  /egov-mdms-service/health  200
  /digit-ui/                 200
  /status/                   200
  /user/oauth/token (ADMIN/eGov@123/pg)  200 + real access_token

One more bug caught + fixed in this final pass

Configurator returned 403 because ~/digit/configurator/ was empty (the digit-configurator dist hasn't been built — vendoring it in-tree is what PR #50 does, kept separate per the audit). Set nginx_features.configurator: false as the default in maputo.yml.example so the validate-200 task skips. Documented in QUICKSTART that operators flip to true after building the dist (or after PR #50 lands).

Net deliverable on this PR

A fresh operator on a clean Mac:

  1. Installs OrbStack + ansible + git + python + node (5 brew commands)
  2. git clone + cp host_vars example + ./deploy.sh maputo
  3. ~5 min later they have a working DIGIT stack (mdms + ui + status + auth all 200)

No kit scripts, no sibling repos, no post-deploy patches. This is the consolidated path.

@KDwevedi

Copy link
Copy Markdown
Collaborator Author

🤖 Codex CLI blank-dir test — passed end-to-end

The ultimate validation: prompted Codex CLI (gpt-5.4, reasoning effort: high, sandbox: danger-full-access) with minimal context in a fully blank directory. The prompt was:

You're on macOS with OrbStack (Docker) already running, plus ansible / git / python / node installed. The current directory is blank.

Goal: deploy DIGIT (the eGov platform) here. Follow MAC-QUICKSTART.md at the root of this repo: https://github.com/KDwevedi/Citizen-Complaint-Resolution-System (branch: feat/canonical-mac-bringup).

Skip the prerequisite-install section (everything's already installed). When the deploy completes, verify the stack by running:
curl -sS -o /dev/null -w 'mdms %{http_code}\n' http://localhost/egov-mdms-service/health
curl -sS -o /dev/null -w 'ui %{http_code}\n' http://localhost/digit-ui/

You should see 'mdms 200' and 'ui 200' if deploy succeeded.

That's it. No memory, no prior context, no hand-holding. Codex:

  1. pwd + ls -la to inspect the blank dir
  2. git clone --branch feat/canonical-mac-bringup --single-branch https://github.com/KDwevedi/Citizen-Complaint-Resolution-System repo
  3. Read MAC-QUICKSTART.md end-to-end (sed, cat, grep to understand the doc + look at the example host_vars + group_vars + mac-stack-up.sh)
  4. cp inventory/host_vars/maputo.yml.example inventory/host_vars/maputo.yml (unchanged from the validated example)
  5. ./deploy.sh maputo
  6. Polled /tmp/mac-stack-up.progress + docker ps during converge to catch any abort early (didn't need to)
  7. Ran the verify curls

Result:

mac-stack-up: CONVERGED on attempt 1/10 — 32/34 up-or-healthy
PLAY RECAP: maputo : ok=65 changed=21 failed=0 skipped=82

curl probes:
  mdms 200
  ui 200

Total elapsed: 21:56:40 → 22:02:27  (5min 47s, including the clone + reading time)

Codex's own closing summary:

DIGIT is up from the feat/canonical-mac-bringup branch, following MAC-QUICKSTART.md with the prerequisite-install section skipped. I used the validated Mac host vars by copying maputo.yml.example to maputo.yml, then ran ./deploy.sh maputo from local-setup/ansible. The play completed with failed=0, and mac-stack-up converged on attempt 1/10.

Your verification probes returned exactly:

mdms 200
ui 200

The stack is currently serving through digit-nginx, with egov-mdms-service, digit-ui, and kong-gateway up and healthy.

What this means: the QUICKSTART is now sufficient for an AI agent with zero prior context to bring up a working DIGIT stack on a Mac in one shot. PR #52 is ready for review.

KDwevedi and others added 11 commits May 21, 2026 07:06
The mcp-bootstrap city task hardcoded the target as {{ state_tenant_id }}.citya,
which only fits the root=X / city=X.citya naming. Maputo is root=mz / city=mz.maputo.

Add city_tenant (default {{ state_tenant_id }}.citya — Linux/naipepea unchanged).
Lets a multi-tenant demo box keep state_root=pg (enc-service boots clean against
the dump) while bootstrapping mz + mz.maputo as layered-on tenants via MCP.

Per the bootstrap-order analysis: enc-service stays pinned to pg; mz's encryption
keys + DataSecurity are generated/read lazily at request time, not boot time, so
state_root and the MCP onboard-target must be separable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Threads core_mobile_configs.mobileNumberPattern + mobileNumberLength from
host_vars into the mcp-bootstrap body so MCP synthesizes the per-tenant
common-masters.UserValidation 'mobile' rule (egov-user ValidationData shape).
Without it new tenants fall back to the hardcoded 10-digit regex and reject
9-digit MZ / Kenya mobiles. Validated: mz.maputo 9-digit register passes,
7-digit rejected, employee login 200.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…country = config, no code)

The mcp-bootstrap body now passes user_validation (default omit) from host_vars.
When set, MCP creates one common-masters.UserValidation record per field rule;
when omitted it auto-builds a single mobile rule from core_mobile_configs.

maputo.yml.example documents the pattern: a fourth country just edits
core_mobile_configs (two lines) — or, to tune userName/email/name rules,
declares a user_validation list. Zero MCP/ansible code change either way.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…or free)

Adds files/configurator-build.sh (clone digit-configurator + npm + vite build
--base=/configurator/, prints dist path) and a playbook task that runs it when
build_configurator: true, capturing the dist into configurator_build for the
existing sync task. Operators who pre-build elsewhere still just set
configurator_build: <path> and leave build_configurator off.

Validated on Mac/OrbStack: built dist served at /configurator/ = 200
(deploy ok=71 failed=0). Generic recipe (node+vite) — same on Linux.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nfigurator)

When `build_mcp: true`, the deploy clones DIGIT-MCP and `docker build`s the
image itself (files/mcp-build.sh), tags it digit-mcp:local, and runs that —
no GHCR/VPC pull, no multi-arch CI fix needed. Mirrors the configurator
fold-in (files/configurator-build.sh).

- files/mcp-build.sh: clone (repo/ref configurable) + docker build + tag;
  prints the image tag on the last line for the playbook to capture.
- playbook: "Resolve MCP image tag" sets mcp_image (digit-mcp:local when
  build_mcp, else {{ docker_registry }}/digit-mcp:latest) before .env render;
  "Build digit-mcp image locally" runs the script before pull, gated on
  enable_mcp AND build_mcp (both default false → no Debian behavior change).
- digit.env.j2: MCP_IMAGE={{ mcp_image }} (was hardcoded registry/latest).
- maputo.yml.example: document build_mcp / mcp_repo_url / mcp_ref / mcp_image
  and drop the stale "arm64-only, upstream-fix territory" note.

Validated on macOS/OrbStack via canonical ./deploy.sh maputo: PLAY RECAP
failed=0; build task ran (changed); .env+compose resolve digit-mcp:local;
container runs the local amd64 build (gap-fix branch), healthy, /healthz 200.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update the ansible docs for the MCP + configurator fold-ins:
- dependency table: digit-configurator and DIGIT-MCP are now built from
  source at deploy (build_configurator / build_mcp), not pre-built/pulled.
- "digit-mcp image" section: local build (build_mcp → digit-mcp:local) is
  the recommended path; VPC registry push demoted to legacy. Note the mac
  converge won't auto-recreate on a tag-only change (--force-recreate).
- host_vars README: replace the stale "enable_mcp only works in the VPC"
  warning — build_mcp:true builds locally anywhere, no registry needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The citizen-flow phone-input prefix comes from LOCALE_REGION (ISO code), not
coreMobileConfigs.mobilePrefix. Default IN→+91; set per country (MZ→+258).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Third fold-in after configurator + mcp. When `build_digit_ui: true`, the deploy
clones digit-ui-esbuild + `npm run build`s the SPA (files/digit-ui-build.sh) and
mounts the bundle over the digit-ui container web root via the
docker-compose.digit-ui-build.yml overlay — serving a from-source UI instead of
the pinned (often stale) registry image. No registry dependency.

- files/digit-ui-build.sh: clone (repo/ref configurable) + npm install +
  `npm run build` (NOT bare esbuild.build.js — that skips the prebuild
  vendored-CSS step and ships a broken bundle). Prints build/ path.
- docker-compose.digit-ui-build.yml: mounts ./src/digit-ui-esbuild/build over
  /var/web/digit-ui; base globalConfigs.js / silent-check-sso.html / conf file
  mounts nest on top (runtime per-tenant config preserved).
- playbook: copy overlay, add it to compose_files when build_digit_ui, build
  task before up -d. All gated `build_digit_ui | default(false)` → no Linux
  (static/host-nginx) behaviour change.
- maputo.yml.example documents build_digit_ui / digit_ui_esbuild_repo|branch.

Validated on macOS via canonical ./deploy.sh maputo: PLAY RECAP failed=0,
build task ran, digit-ui serves the from-source bundle (index.js sha match),
healthy; globalConfigs LOCALE_REGION=MZ still applied.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The docker-compose.digit-ui-build.yml overlay mounted the whole build/ dir over
/var/web/digit-ui. On OrbStack a file-mount nested inside a dir-mount fails
intermittently, so the base globalConfigs.js / silent-check-sso.html file mounts
404'd — the SPA couldn't load its runtime config. (Worked on a manual recreate,
broke after the converge — classic flaky nested mount.)

Replace the overlay with a post-up `tar` of build/ into the running container
(excluding globalConfigs.js + silent-check-sso.html, which stay as file mounts).
No nested mounts → globalConfigs.js serves; re-applied each deploy.

- remove docker-compose.digit-ui-build.yml + its copy task + compose_files entry
- add "Lay digit-ui from-source build into the container" task after up -d

Validated: ./deploy.sh maputo failed=0; build + tar tasks ran; digit-ui serves
the from-source bundle (index.js sha match) and globalConfigs.js is HTTP 200.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The prior commit only captured the overlay file deletion; this carries the
actual playbook change: drop the docker-compose.digit-ui-build.yml copy task +
compose_files entry, and add the post-up "Lay digit-ui from-source build into
the container" tar task. Validated via ./deploy.sh maputo (failed=0; build +
tar tasks ran; digit-ui serves the from-source bundle, globalConfigs.js 200).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pled)

Earlier doc was wrong: the phone-input prefix does NOT come from LOCALE_REGION.
In the from-source digit-ui (build_digit_ui), the citizen login reads
CORE_MOBILE_CONFIGS.mobilePrefix for the prefix; LOCALE_REGION only sets the
i18n locale (en_<REGION>). So keep locale_region: IN (en_IN is always seeded →
localization works with zero seeding) and set mobilePrefix per country for the
prefix. The stale pinned image lacked this decoupling and fell back to +91.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@KDwevedi KDwevedi changed the title feat(mac): canonical Mac bring-up — consolidates #14/#40/#46/#47/#48 into one config-driven path on nairobi feat(mac): canonical Mac bring-up + configurator/mcp/digit-ui from-source fold-ins — consolidates #14/#40/#46/#47/#48/#50 on nairobi May 21, 2026
KDwevedi added 3 commits May 21, 2026 12:23
…eat/canonical-mac-bringup

# Conflicts:
#	local-setup/ansible/templates/digit.env.j2
#	local-setup/docker-compose.egov-digit.yaml
@KDwevedi

Copy link
Copy Markdown
Collaborator Author

Superseded by #53 — the definitive consolidation PR (this branch is stacked on #52, so #53 contains everything here plus the source vendor + boundary onboarding fixes). Closing in favor of the single PR.

@KDwevedi KDwevedi closed this May 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants