Skip to content

Migrate climatemappedafrica builds to Docker Bake - #1510

Open
kelvinkipruto wants to merge 4 commits into
mainfrom
feat/climatemappedafrica-docker-bake
Open

Migrate climatemappedafrica builds to Docker Bake#1510
kelvinkipruto wants to merge 4 commits into
mainfrom
feat/climatemappedafrica-docker-bake

Conversation

@kelvinkipruto

Copy link
Copy Markdown
Contributor

Description

Migrate climatemappedafrica from the legacy shared root Dockerfile to the repository's per-app Dockerfile and Docker Bake build architecture, and fix three pre-existing bugs uncovered along the way that were silently blocking its build.

Fixes the Climate Mapped Africa | Deploy | DEV workflow.

Why

climatemappedafrica's deploy workflow has been failing for a while, most recently on missing @types/express and ajv dependencies (partially addressed in #1509). Rather than keep patching the legacy shared Dockerfile's two-step base-deps/app-deps install — which has repeatedly lost dependency resolution for things like peer-injected packages — this moves the app onto the same turbo-prune-based Bake architecture already proven for pesayetu/roboshield/techlabblog/trustlab.

Validating the migration against a real database (rather than assuming success) surfaced three additional, genuinely pre-existing bugs that would have blocked this build under any tooling:

  1. @commons-ui/payload's barrel export is Payload-v3-only. The package is pinned to catalog:payload-v3 (for trustlab), and its barrel (src/index.js) re-exports ./fields, which imports the v3-only subpath payload/shared. Any Payload v2 app importing anything from the barrel — even just { RichText } — forces webpack to resolve the whole barrel and fails with Module not found: Can't resolve 'payload/shared'. This affected climatemappedafrica directly and the shared hurumap-next package (which climatemappedafrica transitively depends on).
  2. getFooter/getNavBar assumed the Settings global is always fully populated. They destructured several fields (description, title, connect, newsletter, analytics, tutorialEnabled) with no fallback. Next.js's getStaticProps rejects undefined outright ("cannot be serialized as JSON"), so any of these being unset in a lightly-configured Settings global crashes the build, not just renders empty.
  3. imageFromMedia didn't handle a missing image. It read media.alt unconditionally — if no logo were configured, this threw Cannot read properties of undefined (reading 'alt') rather than the intended "no image" state.

None of these three are related to Docker or Bake — the legacy Dockerfile would hit the exact same failures once ajv/@types/express were fixed.

What changed

Pre-existing bug fixes

  • Add a ./RichText export to commons-ui-payload's package.json that bypasses the broken fields barrel, and switch every Payload v2 consumer (climatemappedafrica, hurumap-next) to import from it directly. trustlab's LexicalRichText usage is untouched — it's on Payload v3, so the barrel already resolves fine there. civicsignalblog has the same broken import in 3 files but is out of scope here (flagged separately).
  • Add the missing ajv devDependency to climatemappedafrica (same class of fix as Fix legacy Payload build failures for climatemappedafrica/codeforafrica/charterafrica #1509's for codeforafrica/charterafrica).
  • Guard the undefined-prone Settings fields in getFooter/getNavBar with ?? null, and rewrite imageFromMedia to return null outright when no media is provided — the existing Footer/Navigation components already guard for a falsy logo ({logoProps && (...)}, {...logo}), so this is a no-op for working cases.

climatemappedafrica Docker Bake migration

  • Add a pruned, multi-stage Dockerfile at docker/apps/climatemappedafrica/Dockerfile. Unlike the other migrated apps, this one doesn't use Next.js output: "standalone", so the runner stage copies the full node_modules and .next folder rather than a pruned standalone bundle — matching the legacy Dockerfile's own approach, just via turbo prune instead of the base-deps/app-deps split. The custom server (server.ts, tsc-compiled to dist/server.js) connects to MongoDB via payload.init() before spawning next build, so this build genuinely needs a reachable database, not just a well-formed URL.
  • Add the climatemappedafrica target to docker-bake.hcl and the shared apps group, inheriting _payload-app-runner directly.
  • Standardize on DATABASE_URL for the Payload DB connection instead of MONGO_URL, matching roboshield/trustlab, across the app code, .env files, and the whole build pipeline.
  • Reuse the published ui-builder-base/ui-runner-base images in CI.
  • Switch local make climatemappedafrica and Compose usage to the Bake-built image.
  • Mark climatemappedafrica migrated in docker/README.md.
  • Leave the legacy root Dockerfile stage and climatemappedafrica-deploy-dev.yml workflow untouched for now, matching the rollback-safety precedent from the other migrations — to be disabled once this pipeline is validated in production.

CI/CD

  • Add a reusable Bake workflow (_build-climatemappedafrica.yml) and a new orchestration workflow (climatemappedafrica.yml): build + deploy to DEV on every push, no version-check gate — there's no PROD Dokku app for this one yet, matching the legacy workflow it replaces.
  • Add climatemappedafrica to the pr-build.yml image-validation workflow and scripts/pr-build-targets.mjs's target detection. PR builds use the real CLIMATEMAPPEDAFRICA_MONGO_URL secret (mapped to DATABASE_URL) rather than a dummy value, since the build needs a reachable database to succeed.
  • No new GitHub secrets required — existing CLIMATEMAPPEDAFRICA_MONGO_URL/CLIMATEMAPPEDAFRICA_PAYLOAD_SECRET carry over, just remapped to DATABASE_URL/PAYLOAD_SECRET build secrets.

Validation

  • docker buildx bake climatemappedafrica against a real local MongoDB instance — full turbo prune → install → next build (compile, lint, static export of all 3 pages) → payload build all pass.
  • docker run smoke test — the built image boots, Payload connects, Next.js starts, and / returns HTTP 200.
  • pnpm test:scripts — 10/10 pass, including the updated pr-build-targets detection tests.
  • ESLint passes on all changed files.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Screenshots

N/A

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation

packages/commons-ui-payload is pinned to Payload v3 (catalog:payload-v3)
for trustlab's benefit. Its barrel export (src/index.js) re-exports
./fields, which imports the v3-only subpath `payload/shared`. Any
Payload v2 app importing anything from the barrel — even just
`{ RichText }` — forces webpack to resolve the whole barrel, including
the broken v3-only fields module, and fails with "Module not found:
Can't resolve 'payload/shared'".

Add a dedicated `./RichText` export path that bypasses the `fields`
module entirely, and switch every Payload v2 consumer (climatemappedafrica
and the shared hurumap-next package) to import from it directly instead
of the barrel. trustlab's LexicalRichText usage is untouched — it's on
Payload v3, so the barrel already resolves fine there.

civicsignalblog has the same broken import in 3 files but is out of
scope here; flagged separately.
@payloadcms/bundler-webpack peer-requires ajv, which nothing else in
climatemappedafrica's dependency tree pulls in directly — same class
of issue already fixed for codeforafrica/charterafrica in #1509.
Needed for `payload build` to resolve it when installed in isolation
(both the legacy Dockerfile's per-app install and the new turbo-pruned
Bake build install climatemappedafrica separately from its siblings).
Next.js requires getStaticProps' return value to be JSON-serializable —
`undefined` is rejected outright ("Reason: undefined cannot be
serialized as JSON"). getFooter/getNavBar destructured several fields
(description, title, connect, newsletter, analytics, tutorialEnabled)
directly off Payload's Settings global with no fallback, so an
unconfigured field crashed the build rather than just rendering empty.

Also rewrite imageFromMedia to return null outright when no media is
provided, instead of an object with every field undefined — the
existing Footer/Navigation components already guard for a falsy
logo/drawerLogo (`{logoProps && (...)}`, `{...logo}`), so this is a
no-op for consumers and matches the "no image configured" case more
correctly than piecemeal per-field fallbacks.

Surfaced while validating the Docker Bake migration against an
unseeded local database — any of these fields being empty in a real,
lightly-configured Settings global would hit the exact same crash.
climatemappedafrica was still built from the legacy shared root
Dockerfile via a single docker/build-push-action workflow with no
version-check gate. Move it onto the same per-app Dockerfile + Docker
Bake architecture as pesayetu/roboshield/techlabblog/trustlab.

Standardize on DATABASE_URL for the Payload DB connection instead of
MONGO_URL, matching roboshield/trustlab, across the app code, .env
files, and the whole build pipeline — no MONGO_URL left anywhere in
the new path.

### What changed

- Add a pruned, multi-stage Dockerfile at
  docker/apps/climatemappedafrica/Dockerfile. Unlike the other migrated
  apps, this one doesn't use Next.js `output: "standalone"`, so the
  runner stage copies the full node_modules and .next folder rather
  than a pruned standalone bundle — matching the legacy Dockerfile's
  own approach, just via turbo prune instead of the base-deps/app-deps
  split. The custom server (server.ts, tsc-compiled to dist/server.js)
  connects to MongoDB via payload.init() before spawning `next build`,
  so this build genuinely needs a reachable database, not just a
  well-formed URL.
- Add the climatemappedafrica target to docker-bake.hcl and the shared
  apps group, inheriting _payload-app-runner directly (no app-specific
  secrets needed beyond that and next_public app URL/project root args).
- Reuse the published ui-builder-base/ui-runner-base images in CI.
- Package the full app, plus Dokku health-check metadata.
- Switch local `make climatemappedafrica` and Compose usage to the
  Bake-built image.
- Mark climatemappedafrica migrated in docker/README.md.
- Leave the legacy root Dockerfile stage and
  climatemappedafrica-deploy-dev.yml workflow untouched for now — same
  rollback-safety precedent as the other migrations, to be disabled
  once this pipeline is validated in production.

### CI/CD

- Add a reusable Bake workflow (_build-climatemappedafrica.yml) and a
  new orchestration workflow (climatemappedafrica.yml): build + deploy
  to DEV on every push, no version-check gate — there's no PROD Dokku
  app for this one yet, matching the legacy workflow it replaces.
- Add climatemappedafrica to the pr-build.yml image-validation workflow
  and scripts/pr-build-targets.mjs's target detection. PR builds use
  the real CLIMATEMAPPEDAFRICA_MONGO_URL secret (mapped to
  DATABASE_URL) rather than a dummy value, since the build needs a
  reachable database to succeed.
- No new GitHub secrets required; existing CLIMATEMAPPEDAFRICA_MONGO_URL/
  CLIMATEMAPPEDAFRICA_PAYLOAD_SECRET carry over, just remapped to
  DATABASE_URL/PAYLOAD_SECRET build secrets.

### Validation

- `docker buildx bake climatemappedafrica` against a real local
  MongoDB instance — full turbo-prune -> install -> next build (compile,
  lint, static export of all 3 pages) -> payload build all pass.
- `docker run` smoke test — the built image boots, Payload connects,
  Next.js starts, and `/` returns HTTP 200.
- `pnpm test:scripts` passes (10/10), including the updated
  pr-build-targets detection tests.
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.

1 participant