Skip to content

CI/CD & Build Pipeline

Blue Dots separates building application images from deploying them. The application repositories build and publish container images; the bluedots-automation repository pins specific image versions and deploys them to a cluster. Understanding this split is the key to the whole delivery pipeline.

Application CI in app repos (open PR, GitHub Actions quality gates, merge to trunk, Docker matrix builds) feeds the image registry (GHCR sha-short tags, pinned per environment in global-images.yaml), which Delivery/CD in bluedots-automation deploys via install.sh with Helm to AWS EKS — common-services, then signals, then aggregator

The diagram shows two distinct “CI/CD” surfaces, bridged by the image registry (yellow):

  1. Application CI (blue) — lives in each app repo. It validates and builds images.
  2. Delivery / CD (green) — lives in bluedots-automation. It provisions infrastructure and deploys pinned images with Helm.

Each application repo runs GitHub Actions on every PR and on pushes:

  • Quality gates — pnpm -w lint, typecheck, test, build, and (for aggregator-dpg) pnpm dep-check. Branch protection requires the CI check to pass before merge.
  • Image build & publish — images are built and published to GitHub Container Registry (GHCR) under ghcr.io/blue-dots-economy/…. Two triggers exist today:
    • A release tag — the promotion path. One tag cuts every service’s image at once, across the fleet. In current practice this is always a sprint release candidate, 20*-s*-rc*, e.g. 202608-s1-rc1 (<YYYYMM>-s<sprint>-rc<candidate>; a fix found in -rc1 ships as -rc2, so every candidate is its own image). The workflows also match a semver tag (v*.*.*), but that scheme isn’t in active use. aggregator-dpg additionally supports per-app tags (web-v*.*.*, api-v*.*.*, worker-v*.*.*) to release one of its three services independently.
    • A push to main/develop/feature — still builds and publishes a moving sha-<short> tag in most repos today (notification-service is the exception: tags only). This path is being removed fleet-wide, so a release tag will be the only build trigger going forward — don’t build new tooling around the branch-triggered tags.

Representative images (Signals + Aggregator):

ServiceImage
Signals APIghcr.io/blue-dots-economy/signals-dpg/api
Signals UIghcr.io/blue-dots-economy/signals-dpg/ui
Signals searchghcr.io/blue-dots-economy/signals-search
Aggregator APIghcr.io/blue-dots-economy/aggregator-dpg/api
Aggregator webghcr.io/blue-dots-economy/aggregator-dpg/web
Aggregator workerghcr.io/blue-dots-economy/aggregator-dpg/worker
Keycloak server (custom)ghcr.io/blue-dots-economy/keycloak-server — built in bluedots-automation (dockerfiles/keycloak/)
Keycloak login theme (per network)built in aggregator-dpg from config/<network>/keycloak.env

Third-party platform images (Postgres, Redis, cert-manager, Kong) come from their own registries and are pinned the same way.

The bluedots-automation repo does not build images — it selects them. Each environment has its own opentofu/aws/<env>/global-images.yaml, the single source of truth for image repository, tag and pullPolicy across every service:

# opentofu/aws/<env>/global-images.yaml (excerpt)
api:
image:
repository: ghcr.io/blue-dots-economy/signals-dpg/api
tag: "sha-46c05dd" # dev: riding a branch build
pullPolicy: Always
web:
image:
repository: ghcr.io/blue-dots-economy/aggregator-dpg/web
tag: "202609-s1-rc3" # prod: pinned to a release tag
pullPolicy: Always

Because the file is per-environment, each deployment pins its own tags independently — dev can ride sha-… builds while a production environment stays on a known-good release tag. Promoting a release = updating a tag here and re-running the deploy. The keys map directly to subchart names in the umbrella Helm charts.

Deployment is driven by opentofu/aws/<env>/install.sh — one script for both cloud bootstrap and Helm deploy (there is no Makefile). Every deploy_* function runs helm upgrade --install … --wait, layering the chart’s own values.yaml, then the generated overlays, then global-images.yaml and global-resources.yaml.

Terminal window
cd opentofu/aws/<env>
export GHCR_PAT=ghp_xxxxxxxxxxxx # read:packages — needed to pull private images
# static checks (install nothing)
bash install.sh lint # helm lint all charts
bash install.sh dry_run # helm --dry-run against the cluster
# full, ordered deploy
bash install.sh deploy_all_services

deploy_all_services chains, in strict order:

deploy_all_services runs in strict order: 1 preflight, 2 create_namespaces_and_secrets (3 namespaces + ghcr-pull secret), 3 deploy_monitoring, 4 deploy_common_services (Kong CRDs + platform), 5 deploy_keycloak, 6 deploy_signals, 7 deploy_aggregator

See the Deployment guide for the full step-by-step and validation, and Infrastructure & Deployment Architecture for what each layer is.

For automation (e.g. a future pipeline), the OpenTofu apply functions honour AUTO_APPROVE=1 for non-interactive terragrunt apply, and GHCR_PAT can be supplied via the environment instead of an interactive prompt:

Terminal window
AUTO_APPROVE=1 GHCR_PAT=ghp_xxx bash install.sh apply_tf_eks
GHCR_PAT=ghp_xxx bash install.sh create_namespaces_and_secrets

Each live deployment has its own long-lived branch carrying that deployment’s config — network schema, image tags, public hostnames, and its own opentofu/aws/<env>/ directory. Work flows up a trunk chain before landing in a deployment branch.

Trunk (promotion chain): <feature-branch> → feature → develop → main.

  • main — canonical branch; cut new deployment branches from here.
  • develop — pre-release integration.
  • feature — collects in-progress work (often the newest trunk branch).
  1. Open a PR in an app repo → CI runs lint/typecheck/test/build; reviewers approve; merge.
  2. When ready to ship, cut a sprint release-candidate tag (e.g. 202608-s1-rc1) → that tag triggers the build, publishing ghcr.io/blue-dots-economy/<service>:<tag> to GHCR for every service at once.
  3. In bluedots-automation, on the target environment’s branch, update the relevant tag(s) in opentofu/aws/<env>/global-images.yaml to the new release tag.
  4. Run bash install.sh deploy_<service> (or deploy_all_services) — Helm rolls out the new image with --wait.
  5. Validate (helm list -A, pod health, ingress, TLS) — see the Deployment guide.

Rollback is the inverse: set the tag back to the previous known-good release tag and re-run the deploy.