CI/CD Pipeline
ChartChat uses a Jenkins declarative pipeline defined in Jenkinsfile-memberportal at the repo root.
Trigger​
Every push to the main branch triggers the pipeline. A change detection stage inspects the diff and only builds components that changed. Shared package changes (e.g. packages/core) trigger all three components to rebuild.
Bitbucket webhook → Jenkins using the bitbucketPush() trigger.
Pipeline Stages​
Setup → Pull Code → Detect Changes → Install Root → Build Shared
→ [parallel] Member Portal (Install → Build → Deploy)
+ Admin Portal (Install → Build → Deploy)
→ Install Server → Build Server → Deploy Server
→ Restart Server → Health Check Server
→ Cleanup Old Releases
Member Portal and Admin Portal build in parallel. Each component only runs if its files changed (or shared packages changed).
Stage 1 — Setup​
Sets RELEASE_NAME = "release-${BUILD_NUMBER}" which is used as the release directory name for all three components.
Stage 2 — Pull Code​
Checks out the main branch from Bitbucket using the bitbucket-credential Jenkins credential. Runs git clean -fd to remove any leftover files from the previous build.
Stage 3 — Install Root Dependencies​
npm install
Installs all workspace dependencies at the monorepo root. This makes shared packages available to all components.
Stage 4 — Build Shared Packages​
npm run build:fast
cd packages/generator && npm run fhirtypes && npm run jsonschema
build:fastbuilds all shared packages (core, react, fhirtypes, definitions, etc.) via Turborepo.- The generator step regenerates TypeScript types and JSON schemas from the FHIR definitions. This must run after shared packages are built so that downstream packages use the latest types.
Stage 5 — Member Portal​
Install
Uses hash-based caching — compares md5sum of memberportal/package.json against a file at /tmp/chartchat-memberportal-lock-hash. If the hash matches and node_modules/ exists, npm install is skipped entirely.
cd memberportal && npm install # skipped if package.json unchanged
Build
Environment variables are injected from Jenkins credentials store. A .env file is written to memberportal/ before npm run build:
| Credential ID | Env var |
|---|---|
VITE_MEDPLUM_BASE_URL | VITE_MEDPLUM_BASE_URL |
VITE_MEDPLUM_PROJECT_ID | VITE_MEDPLUM_PROJECT_ID |
VITE_MEDPLUM_GOOGLE_CLIENT_ID | VITE_MEDPLUM_GOOGLE_CLIENT_ID |
VITE_RECAPTCHA_SITE_KEY | VITE_RECAPTCHA_SITE_KEY |
VITE_MICROSOFT_CLIENT_ID | VITE_MICROSOFT_CLIENT_ID |
VITE_MICROSOFT_TENANT_ID | VITE_MICROSOFT_TENANT_ID |
VITE_MEDPLUM_CLIENT_APP_ID | VITE_MEDPLUM_CLIENT_APP_ID |
VITE_CHAT_API_URL | VITE_CHAT_API_URL |
Deploy
- Detects build output at
memberportal/dist(falls back tomemberportal/build). - Copies to
${MEMBER_RELEASES}/${RELEASE_NAME}/. - Sets
jenkins:www-dataownership and755permissions. - Verifies
index.htmlexists in the release. - Atomically updates
currentsymlink.
On failure, reverts current to the previous release.
Stage 6 — Admin Portal​
Install
Same hash-based caching as Member Portal — hash of packages/chartchat-admin/package.json stored at /tmp/chartchat-adminportal-lock-hash.
cd packages/chartchat-admin && npm install # skipped if package.json unchanged
Build
Credentials injected and written to packages/chartchat-admin/.env:
| Credential ID | Env var |
|---|---|
MEDPLUM_BASE_URL | MEDPLUM_BASE_URL |
VITE_MEDPLUM_CLIENT_ID | VITE_MEDPLUM_CLIENT_ID |
VITE_GOOGLE_CLIENT_ID | VITE_GOOGLE_CLIENT_ID |
VITE_RECAPTCHA_SITE_KEY | VITE_RECAPTCHA_SITE_KEY |
VITE_API_BASE_URL | VITE_API_BASE_URL |
VITE_PATIENT_PORTAL_URL | VITE_PATIENT_PORTAL_URL |
VITE_EPIC_CLIENT_ID | VITE_EPIC_CLIENT_ID |
VITE_MEDPLUM_REGISTER_ENABLED=true is hardcoded in the pipeline.
Deploy — same pattern as Member Portal, reading from packages/chartchat-admin/dist.
Stage 7 — Server​
Install
Same hash-based caching — hash of packages/server/package.json stored at /tmp/chartchat-server-lock-hash.
cd packages/server && npm install # skipped if package.json unchanged
Build
Credentials injected and written to packages/server/.env:
| Credential ID | Purpose |
|---|---|
MEDPLUM_BASE_URL | Medplum server URL |
MEDPLUM_CLOUD_BASE_URL | Cloud Medplum URL |
MEDPLUM_CLIENT_ID | OAuth client ID |
MEDPLUM_FHIR_URL | FHIR R4 base URL |
MEDPLUM_PROJECT_ID | Medplum project ID |
EPIC_CLIENT_ID / EPIC_REDIRECT_URI | Epic EHR OAuth |
HEALOW_CLIENT_ID / HEALOW_REDIRECT_URI | Healow EHR OAuth |
MEDITECH_CLIENT_ID / MEDITECH_CLIENT_SECRET / MEDITECH_REDIRECT_URI | MEDITECH EHR OAuth |
GOOGLE_CLIENT_ID | Google OAuth |
RECAPTCHA_SITE_KEY | reCAPTCHA |
FRONTEND_URL | Allowed CORS origin |
FOOMEDICAL_URL | Legacy URL reference (still present in pipeline) |
EMBED_API_URL / EMBED_ADMIN_KEY | Embedding service credentials |
DEXCOM_CLIENT_ID / DEXCOM_CLIENT_SECRET / DEXCOM_REDIRECT_URI / DEXCOM_BASE_URL | Dexcom integration |
VITE_GROQ_API_KEY | AI (Groq) API key |
EHR_TOKEN_ENCRYPTION_KEY | Encryption key for stored EHR tokens |
CHARTCHAT_DEFAULT_PASSWORD | Default admin password |
Then runs npm run build (TypeScript → dist/).
Deploy
- Creates
${SERVER_RELEASES}/${RELEASE_NAME}/. - Copies
dist/,package*.json, and.envinto the release (medplum.config.jsonis not copied — server reads config from AWS SSM at startup). - Copies
node_modules/from the previous release into the new release directory for speed, then runsnpm installto apply any changes. - Overwrites
node_modules/@medplum/definitions/dist/with the locally built definitions (contains custom FHIR types not published to npm:PatientEhrConnection,SSOIntegration, etc.). - Atomically updates
currentsymlink.
Stage 8 — Restart Server​
pm2 delete chartchat-server 2>/dev/null || true # always delete first — restart won't pick up new CWD
pm2 start node --name chartchat-server --cwd "${SERVER_CURRENT}" -- dist/index.js aws:us-east-2:/chartchat/uat/
pm2 save
pm2 list
The server reads all configuration from AWS SSM Parameter Store at startup via the aws:us-east-2:/chartchat/uat/ argument. No medplum.config.json or local config file is needed.
See Deployment → PM2 for why delete + start is required.
Stage 9 — Health Check​
Polls http://localhost:8103/healthcheck every 10 seconds, up to 24 times (4 minutes total).
Each attempt first checks PM2 process state:
erroredorstopped→ fails immediately (no point waiting), dumps last 50 PM2 log lines.onlinebut HTTP not ready → keeps waiting.- HTTP 2xx/3xx → success, pipeline continues.
On rollback:
- Finds the most recent previous release in
${SERVER_RELEASES}. - Repoints
currentsymlink to it. - Runs
pm2 restart chartchat-server. - Fails the build.
Stage 10 — Cleanup Old Releases​
Runs for all three component release directories. Keeps the 5 most recent releases and deletes the rest:
cd "$DIR"
ls -t | tail -n +6 | xargs -r rm -rf
Jenkins Credentials​
All secrets are stored in Jenkins' Credentials Store as Secret text entries. The credential ID must match exactly what is referenced in withCredentials([...]) blocks in the Jenkinsfile.
To add or rotate a credential:
- Open Jenkins → Manage Jenkins → Credentials
- Select the appropriate domain (usually
Global) - Add or update the
Secret textentry with the matching credential ID
Required Jenkins Setup​
| Item | Details |
|---|---|
| Jenkins plugin | Bitbucket plugin (for bitbucketPush() trigger) |
| Jenkins credential | bitbucket-credential — username/password or SSH key for repo access |
| sudoers entry | jenkins ALL=(ubuntu) NOPASSWD: /home/ubuntu/.nvm/versions/node/v24.11.1/bin/pm2 |
| Node.js | Available on the Jenkins agent — must match the version used in the project |
| Bitbucket webhook | Must be configured to notify Jenkins on push to main |
Notifications​
Build log:
- Success:
DEPLOYED | Release: release-N | Components: <list of built components> - Failure:
FAILED | Release: release-N | Logs: <build_url>/console
Email (sent to neel.pegasusone@gmail.com):
- On failure: Subject
Build Failed: <job> #<build>with release name, job, build number, and log URL. - On recovery (first green build after a failure): Subject
Build Recovered: <job> #<build>.