Creating a build pipeline for OpenMRS 3.0

As the OpenMRS 3.0 Framework is beginning to get piloted and we expect adoption only to grow, we need to migrate from an ad hoc deployment at to a more automated CI-based process that can be hosted within the OpenMRS infrastructure (e.g., something like,,

Adapting the proposed RefApp 3.x folder layout to define everything needed to deploy an instance of the current version of OpenMRS 3 (running at would be a great start and would have some immediate benefits:

  • Provide the recipe for automated CI builds so we can have qa, staging, and demo environments for OpenMRS 3.0 work
  • Harden our convention for defining deployments (both for OpenMRS 2.x & 3.x)
  • Make it easier for people to try out OpenMRS 3.0 for themselves (either through a hosted environment or by deploying locally)

Some of the pieces I think we need are:

  • Pulling settings (host location, port, etc.) into environment variables
  • A repo for OpenMRS 3.0 Framework with GitHub action(s) to build docker containers and deploy them to OpenMRS on Docker Hub
  • ansible-docker-compose file(s) that define the hosted environments for our infrastructure
  • CI plans to deploy those environments

I assume micro frontends (with its myriad of esm modules) would be similar to the RefApp (with its myriad of OpenMRS modules) – i.e., changes to individual esm repos wouldn’t trigger a new deployment to qa; rather, a commit to the OpenMRS 3.x repository (like a change to its import map configuration) would trigger a new build & deployment. Then CI could be used to manually promote any one of those builds to staging/demo sites.

I know there are more pieces, but I wanted to get the conversation started here. Thoughts about other steps/pieces we’ll need?

/cc @mksrom @jdick @grace @mksd @bistenes @dkayiwa @ibacher @cintiadr


Thanks for kicking this off Burke!

A few immediate thoughts:

  1. Separating App releases from Production environment releases: I think we need to think of these as two different though related release pipelines, similar to how we think of Modules vs RefApp release right now. We kind of have this already: in npm, MF apps/widgets get labelled with “next” or “latest” depending on whether they’re production ready or in draft.

I want to see us focus, for the next few weeks, on the other release process: releasing regular improvements to our production/demo site.

I.e. every 1-2 weeks we should update the Demo/production site with the most recent published version of widgets. This frequent cadence culture will help us get used to getting value out the door, promptly, together.

  1. Demo Site: Let’s keep it simple to start with and just have 2 environments: our dev/QA environment, and our demo/production environment. I know big big teams often have a staging environment in between but at our current size and scope it adds unnecessary complexity (esp. since our production product is a demo site, not a patient or physician facing product). We have a staging environment in the OCLOMRS project and honestly it’s rarely used, and adds an additional layer that our PMs have to QA which is rather irritating at this stage.

If we find there’s lots of issues like merge conflicts between these two, then we can consider adding a staging environment. But let’s reduce complexity ad much as we can for now.

Thanks @burke, definitely needing to loop in @achachiez.

Agreed. As I was writing the post, I was already thinking there’s little need for a staging environment at this point. Starting with a qa & demo makes sense. Actually, if we follow the path of RefApp CI, the next next environment we’ll likely want would be uat (i.e., user acceptance testing = an environment for testing of features/changes that is safe from automated/unexpected changes but not yet ready for the demo site). But let’s start by focusing on an automated qa first (an automated version of what openmrs-spa is doing now).

@mksrom / @dkayiwa / @ibacher,

When I checkout the 3.x branch of openmrs-distro-referenceapplication and run mvn clean package, it fails with this error:

[ERROR] Failed to execute goal on project openmrs-distro-package: Could
not resolve dependencies for project
org.openmrs:openmrs-distro-package:pom:1.0.0-SNAPSHOT: Failed to
collect dependencies at org.openmrs.web:openmrs-webapp:war:2.3.3:
Failed to read artifact descriptor for
org.openmrs.web:openmrs-webapp:war:2.3.3: Could not transfer artifact
org.openmrs.web:openmrs-webapp:pom:2.3.3 from/to
maven-default-http-blocker ( Blocked mirror for
repositories: [openmrs-repo
default, releases+snapshots), archetype
default, releases+snapshots), openmrs-repo-thirdparty
default, releases+snapshots)]
1 Like

You’ve got the latest Maven apparently. The URLs on that repo will need to be changed to https instead of http or you can downgrade to Maven 3.6.3 (this is a breaking, security-related change in Maven 3.8.1).

Yeah, it seems like we might want an environment that we can run some automated and manual tests against that isn’t the development environment, for much the same reasons it makes sense to have a demo environment.

Is there any preference as to which servers these will be deployed on? Currently both demo and qa-refapp run on balaka.

Ahh great point.

One caveat: @ibacher in the case of 3.0, the Demo is equivalent to our Production application :stuck_out_tongue: So we’ll have to be kinda careful how we use that word in the 3.0 context.

Thanks @ibacher. I replaced a couple “http:” with “https:” in package/pom.xml and in my ~/.m2/settings.xml and mvn clean package succeeded. :slight_smile:

Is it safe (backwards-compatible) to update the package/pom.xml to use https (if I update the pom.xml to use https, will it still work for people using older versions of mvn)?

Yeah, it’s totally safe, as long as it isn’t pointing to the repo (we use that for SASS in some of the UI modules; see here).

@burke any updates? Are we any closer to having two environments, dev vs production/demo, for 3.0?

@mksrom is the spa module supposed to be included in pom.xml? When I try to bring it up, it looks like resources are expected to be served from /openmrs/spa/ and I assume that path is designed to be handled by the spa module, right?

The MFs are served via the Nginx server. not the spa module.

The issue you encounter has been reported to me already but only appear for some and not others, maybe OS dependent. which OS do you run?

Immediate workaround:

Could you replace:


npx openmrs build --spa-path /ui --api-url /openmrs --target $1

and rebuild again?

Longer explanation:

My idea was to keep the packaging of the application agnostic of the way it’s deployed.

However, the build of the MF project needs to be informed of some deployment-specific variables. And those vars are:

  • apiUrl
  • spaPath

Such variables must be passed to the npx openmrs build command, meaning that a package of the distro would become deployment specific!!

To get around this, I am setting the value to be environment variables instead:

And substitute the vars upon running the project:


Problem is that in some environments, --spa-path \${SPA_PATH} substitutes the variable with it’s current value (which is null on the developer machine) and therefore ends up being the default /openmrs/spa.

1 Like

Thanks @mksrom. I’m running on macOS Big Sur. Hardcoding the values worked. So did this:

npx openmrs build --spa-path \${SPA_PATH:-/ui} --api-url \${API_URL:-/openmrs} --target $1

What do you think about using default values? :slight_smile:

Are there requirements for the host (maven version, npm/npx version)? We’ll want to make sure our CI environment meets those requirements.

Not really.

I made sure that the most recent Maven works (by using HTTPS repos).

As for npm, on the machine I’ve tried it on, npm -v was 6.14.4

And any recent Docker and Docker Compose version will do.

My initial goal of this Distro Ref App 3.x project was to suggest a standard packaging for OpenMRS 3, not so much how to run such package. So the Docker project attached is really optional and there to show an example of how we can consume the packaging of OpenMRS.

With that in mind, I want the output ZIP file of the distro to be deployment independent. So that anyone can choose to deploy it the way he/her wants. Maybe with containers, maybe bare servers or anything.

Those 2 vars (apiUrl and spaPath), they make the package deployment dependent! :angry: :smiley:

So that’s the reason why we can’t hard code them, nor use their default value, as we don’t know what their value should be until we know how the app will be deployed.

So basically, that’s the job of the deployment project to provide those values.

In our case, the Docker project provides them in the docker-compose.yml

Does that make sense?

@mksrom my docker container has the correct environment values:

$ docker-compose exec frontend bash -c 'env | grep -iE "SPA|API"'

I think the problem is package/scripts/ is being executed as part of the mvn clean package (i.e., executed during the build within the host environment) before docker is started, so setting these environment variables in the docker-compose doesn’t help the npx command that was run before docker-compose is invoked.

So, either these variables need to also be provided during the build step or we need to perform the build within the docker-compose stack. This worked for me:

SPA_PATH=/ui API_URL=/openmrs mvn clean package
cd run/docker/
docker-compose up -d --build

Well, OpenMRS 3.0 doesn’t yet successfully get past the login, but at least the frontend appears to be getting loaded now. :slight_smile:

Looking through the docker-compose.yml, I don’t think we’ll need the proxy service, since that is already provided by the OpenMRS infrastructure. But we will still need to move the contents of distro/ and properties/ folders into a docker image.

Ideally they are supposed to be kept as environment variables during the build, so they can be substituted only at run time.

But anyway, we’ve made some refactoring on the project and it doesn’t look like you’re using the latest changes there, as we are now using the OpenMRS SDK to package the distro.

In this new version, the apiUrl is hardcoded at build time though:

Which again, is incorrect as we do not know those values for certain when packaging the app. Only know them upon running it.

In the case of deploying from CI to our infrastructure, the build environment and run time environments are different (build on CI, run via docker-compose on infrastructure host machines).

In the case of infrastructure deployment, we should not alter the host environment; rather, environment setting should be pulled from a configuration file (e.g., .env) just as Docker does.

And, from what I see for 3.0, these variables are being used both at build time (running npx as part of the maven packaging process) and for run time.

Ah. So, you’re saying that – unlike the current state of the 3.x branch of refapp distro – the npx command in the script that runs during mvn clean package should not need to supply these settings at all, since they should be determined within the app from environment variables at run time, correct?

Exactly that. That’s why initially I wanted them to just be set to the strings "$API_URL" and "$SPA_PATH". And provide their actual values only when running the Docker Compose project. See

But that’s not the case anymore. Right now, they are just hardcoded at build time. And that’s not correct

So, it looks like we only need to get the contents of the distro/ folder into container(s) so a docker-compose.yml can download everything needed from Docker Hub (including these resources) and provide the resources to the openmrs and frontend services.

@ibacher did @mksrom say that you had already done something along this line? I don’t know if creating a docker image from scratch to simply copy the distro files and provide them for the other containers would be considered a best practice in Docker-land… or if we just want to copy these files into the openmrs & frontend Docker containers (which would make those containers less re-usable).

So what I did is a bit messy, but hopefully manageable. Basically if you look here, there are two Docker images, one of which is expected to use volumes and one (the Dockerfile-embedded one) that just embeds the relevant files in the same place. (There’s a similar structure for the ui image that serves the frontend).

I think it looks like we just need to add a docker-compose-embedded.yml file and we could have something that can be deployed to the OMRS infrastructure.

Adding one more consideration: right now, gets updated more or less “live” by updating an importmap located in DigitalOcean’s CDN. If we convert to the Docker images, the importmap is only setup to serve particular versions of the frontends, since the build generates the import map.

@florianrappl Is there any easy way we can use the standard build / assemble commands but continue leveraging the existing importmap?