Deployment
Staging deploys itself; production is deliberately manual. The whole flow is GitOps: what runs in a DC is whatever its Git-tracked Helm values say.
Two halves, one image
The Portal buildflow GitHub Actions workflow in ts-polaris builds two images and stitches them:
lf-portal-next- the Vite production build ofportal/(static files).lf-portal-base-express- the compiledportalBackend/Express server.Dockerfile.portalcopies the SPA build into the Express image, producinglf-portal-full-express- the one artifact that ships. All three land in ECR (us-east-1).
PR gates
Before merge: lint (oxlint + ESLint), unit tests, and the Portal DB Migration Gate, which boots a real MySQL 8 and Redis 7 in CI and validates the Sequelize migrations against them. A branch that is behind master is refused a build unless explicitly forced.
Staging: automatic, with a self-cleaning escape hatch
- master builds tag
master-<timestamp>-<sha>and update the staging DCs to the latest image. - Custom images: any branch can be built as
custom-<user>-...and deployed to chosen staging DCs with a mandatory UTC expiry. At expiry the DC auto-reverts to latest master; a custom ("snapshot") image that lingers raises a portal-team alarm. The team runbook (deploy-custom-image) triggers this and then verifies on Prometheus that the DC is actually running the new tag (kube_pod_container_info).
Production: manual GitOps, four locks deep
- PR the tag. An engineer updates the target DC's
env/<dc>/kubernetes/helm/lf-portal-express/version.yamlwith the new ECR tag; codeowners review and merge. Production tags are never updated automatically. - Deployment window. Production deploys happen in scheduled windows; an out-of-band hotfix must be announced and approved in Slack (
#deployment,#production_issues). - Okta privilege elevation. Nobody holds standing ArgoCD write access to production. The engineer requests temporary elevation for that DC through the Okta Access Requests app in Slack.
- Manual ArgoCD Sync. After the merge, ArgoCD shows the app Out of Sync; the engineer clicks Sync. ArgoCD first runs the presync migration Job (Sequelize migrations against the DC's MySQL, with locking), then performs the rolling update of the pods.
Mental model: CI produces immutable images; Git holds the desired state per DC; ArgoCD reconciles the cluster to Git. Staging shortens the loop by editing Git for you; production keeps every edit human.
Next: Infrastructure - what a DC actually is and who runs it.