Skip to main content

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:fast builds 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 IDEnv var
VITE_MEDPLUM_BASE_URLVITE_MEDPLUM_BASE_URL
VITE_MEDPLUM_PROJECT_IDVITE_MEDPLUM_PROJECT_ID
VITE_MEDPLUM_GOOGLE_CLIENT_IDVITE_MEDPLUM_GOOGLE_CLIENT_ID
VITE_RECAPTCHA_SITE_KEYVITE_RECAPTCHA_SITE_KEY
VITE_MICROSOFT_CLIENT_IDVITE_MICROSOFT_CLIENT_ID
VITE_MICROSOFT_TENANT_IDVITE_MICROSOFT_TENANT_ID
VITE_MEDPLUM_CLIENT_APP_IDVITE_MEDPLUM_CLIENT_APP_ID
VITE_CHAT_API_URLVITE_CHAT_API_URL

Deploy

  1. Detects build output at memberportal/dist (falls back to memberportal/build).
  2. Copies to ${MEMBER_RELEASES}/${RELEASE_NAME}/.
  3. Sets jenkins:www-data ownership and 755 permissions.
  4. Verifies index.html exists in the release.
  5. Atomically updates current symlink.

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 IDEnv var
MEDPLUM_BASE_URLMEDPLUM_BASE_URL
VITE_MEDPLUM_CLIENT_IDVITE_MEDPLUM_CLIENT_ID
VITE_GOOGLE_CLIENT_IDVITE_GOOGLE_CLIENT_ID
VITE_RECAPTCHA_SITE_KEYVITE_RECAPTCHA_SITE_KEY
VITE_API_BASE_URLVITE_API_BASE_URL
VITE_PATIENT_PORTAL_URLVITE_PATIENT_PORTAL_URL
VITE_EPIC_CLIENT_IDVITE_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 IDPurpose
MEDPLUM_BASE_URLMedplum server URL
MEDPLUM_CLOUD_BASE_URLCloud Medplum URL
MEDPLUM_CLIENT_IDOAuth client ID
MEDPLUM_FHIR_URLFHIR R4 base URL
MEDPLUM_PROJECT_IDMedplum project ID
EPIC_CLIENT_ID / EPIC_REDIRECT_URIEpic EHR OAuth
HEALOW_CLIENT_ID / HEALOW_REDIRECT_URIHealow EHR OAuth
MEDITECH_CLIENT_ID / MEDITECH_CLIENT_SECRET / MEDITECH_REDIRECT_URIMEDITECH EHR OAuth
GOOGLE_CLIENT_IDGoogle OAuth
RECAPTCHA_SITE_KEYreCAPTCHA
FRONTEND_URLAllowed CORS origin
FOOMEDICAL_URLLegacy URL reference (still present in pipeline)
EMBED_API_URL / EMBED_ADMIN_KEYEmbedding service credentials
DEXCOM_CLIENT_ID / DEXCOM_CLIENT_SECRET / DEXCOM_REDIRECT_URI / DEXCOM_BASE_URLDexcom integration
VITE_GROQ_API_KEYAI (Groq) API key
EHR_TOKEN_ENCRYPTION_KEYEncryption key for stored EHR tokens
CHARTCHAT_DEFAULT_PASSWORDDefault admin password

Then runs npm run build (TypeScript → dist/).

Deploy

  1. Creates ${SERVER_RELEASES}/${RELEASE_NAME}/.
  2. Copies dist/, package*.json, and .env into the release (medplum.config.json is not copied — server reads config from AWS SSM at startup).
  3. Copies node_modules/ from the previous release into the new release directory for speed, then runs npm install to apply any changes.
  4. Overwrites node_modules/@medplum/definitions/dist/ with the locally built definitions (contains custom FHIR types not published to npm: PatientEhrConnection, SSOIntegration, etc.).
  5. Atomically updates current symlink.

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:

  • errored or stopped → fails immediately (no point waiting), dumps last 50 PM2 log lines.
  • online but HTTP not ready → keeps waiting.
  • HTTP 2xx/3xx → success, pipeline continues.

On rollback:

  1. Finds the most recent previous release in ${SERVER_RELEASES}.
  2. Repoints current symlink to it.
  3. Runs pm2 restart chartchat-server.
  4. 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:

  1. Open Jenkins → Manage Jenkins → Credentials
  2. Select the appropriate domain (usually Global)
  3. Add or update the Secret text entry with the matching credential ID

Required Jenkins Setup​

ItemDetails
Jenkins pluginBitbucket plugin (for bitbucketPush() trigger)
Jenkins credentialbitbucket-credential — username/password or SSH key for repo access
sudoers entryjenkins ALL=(ubuntu) NOPASSWD: /home/ubuntu/.nvm/versions/node/v24.11.1/bin/pm2
Node.jsAvailable on the Jenkins agent — must match the version used in the project
Bitbucket webhookMust 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>.