Read-only mirror of https://github.com/ehealthsuisse/ch-emr-test-kit — eHealth Suisse. Issues & pull requests at the source.
  • Shell 80.8%
  • HTML 11.1%
  • Dockerfile 8.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Christian Peter Dyreborg Jonigkeit 3a8eb6581b feat(config): default IG_URLS to the CH EMR implementation guide
Populate the example environment file with a working IG_URLS entry
pointing at the CH EMR IG (http://fhir.ch/ig/ch-emr/ImplementationGuide/ch.fhir.ig.ch-emr),
so a fresh copy of .env.example loads the IG this test kit targets
out of the box instead of leaving IG_URLS empty.
2026-08-04 14:51:17 +02:00
.github/workflows ci: build and publish the image for amd64 and arm64 2026-06-23 14:00:30 +02:00
fhir-server feat(branding): rebrand the built-in tester for eHealth Suisse 2026-06-30 10:22:33 +02:00
tests feat(server): install IG example instances by default 2026-06-25 09:03:16 +02:00
.env.example feat(config): default IG_URLS to the CH EMR implementation guide 2026-08-04 14:51:17 +02:00
.gitignore build: add docker-compose stack, image build script and env template 2026-06-15 15:20:48 +02:00
build.sh chore: point image and name references at ehealthsuisse/ch-emr-test-kit 2026-06-29 13:21:48 +02:00
docker-compose.build.yml chore: point image and name references at ehealthsuisse/ch-emr-test-kit 2026-06-29 13:21:48 +02:00
docker-compose.yml feat(branding): rebrand the built-in tester for eHealth Suisse 2026-06-30 10:22:33 +02:00
LICENSE chore: add Apache 2.0 license 2026-06-15 09:12:34 +02:00
README.md feat(branding): rebrand the built-in tester for eHealth Suisse 2026-06-30 10:22:33 +02:00

CH EMR Test Kit

A disposable, versioned HAPI FHIR server with the built-in hapi.fhir.org-style tester (a clone of hapi.fhir.org) for unrestricted CRUD. Implementation Guides are loaded into the server from a URL at container startup, and the tester is branded for eHealth Suisse.

The profile-restricted, per-IG CRUD UI (dynamic forms generated from each profile's StructureDefinition) lives on the feat/ig-profile-ui branch, which builds on top of this one.

Architecture

Service Image Port Purpose
db postgres:16-alpine Persistence
fhir ghcr.io/ehealthsuisse/ch-emr-test-kit:<tag> (built FROM hapiproject/hapi) 8080 FHIR REST API + generic tester

Base-image note: the FHIR image is built FROM hapiproject/hapi:<version>, which is a distroless/Debian image — not Alpine. To make it Alpine, switch to a from-source multi-stage build.

Images & versioning

The FHIR image is published to the GitHub Container Registry by CI:

  • ghcr.io/ehealthsuisse/ch-emr-test-kit:latest — rolling, from main.
  • ghcr.io/ehealthsuisse/ch-emr-test-kit:<version> — immutable, from git tags (1.0.0, 1.0, 1); plus hapi-<HAPI_VERSION> and sha-<sha> tags.

By default docker compose up pulls the published image (latest, or pin IMAGE_TAG). Local builds (see below) use a separate :local tag scheme, so they never collide with the published versioned tags or latest.

Quick start (pull the published image)

cp .env.example .env          # then edit IG_URLS, IMAGE_TAG, ports
docker compose up -d          # pulls ghcr.io/ehealthsuisse/ch-emr-test-kit:${IMAGE_TAG:-latest}

Pin a release with IMAGE_TAG=1.0.0, and docker compose pull to refresh latest. (The GHCR package may require docker login ghcr.io if it isn't public.)

Build from source (opt-in)

docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build
# or, equivalently, just build the image:
./build.sh

This builds ch-emr-test-kit:${LOCAL_TAG:-local} (HAPI version from HAPI_VERSION) instead of pulling.

Loading Implementation Guides

Set IG_URLS (a startup parameter, comma-separated). Each item is either a package .tgz URL, or explicit coordinates name@version=url:

IG_URLS=ch.fhir.ig.ch-emr@1.0.0-ballot=https://hl7ch.github.io/ch-emr/package.tgz
# multiple:
IG_URLS=https://example.org/a/package.tgz,my.ig@1.0.0=https://example.org/b/package.tgz

The container entrypoint expands IG_URLS into the native HAPI hapi.fhir.implementationguides.* settings. Those native HAPI_FHIR_IMPLEMENTATIONGUIDES_* environment variables also work directly if you prefer to bypass IG_URLS.

Dependencies

Each loaded IG's declared dependency packages are fetched and installed transitively (fetchDependencies), so the base IGs it builds on — and profiles it references via the structuredefinition-imposeProfile extension — are present for snapshot generation and $validate. For example, loading CH EMR also installs ch-core, ch-term, ch-ips, ch-emed, the HL7 extensions and hl7.fhir.uv.ips (the source of CH EMR's imposed IPS profiles), and validating a resource against a CH EMR profile then also enforces the imposed IPS profile.

A few HL7 "infrastructure" packages cannot be ingested by HAPI's package installer and would abort startup (hl7.terminology.r4HAPI-1764; hl7.fhir.uv.xver-r5.r4 → over-long resource ids). These are excluded by default (they provide terminology/cross-version helpers, not profiles). Adjust with IG_DEPENDENCY_EXCLUDES (comma-separated regexes on package id; set empty to attempt everything).

Examples

By default the IGs' example instances (the resources in each package's example folder — e.g. CH EMR's example Patients, Compositions and Medications) are also created on the server, so there is sample data to browse and validate immediately. Set INSTALL_EXAMPLES=false to install only conformance resources, or EXAMPLE_FOLDERS (comma-separated, default example) to point at other package subfolders. This maps to HAPI's per-IG implementationguides.<id>.additionalResourceFolders.

Branding the built-in tester

The tester's logo, name and welcome/sample text default to eHealth Suisse and are overridable via a container mount. The image ships defaults under /branding:

/branding/custom/logo.svg      # logo in the tester banner (SVG; .jpg also works)
/branding/custom/welcome.html  # welcome / sample text on the home page
/branding/name.txt             # server name shown in the navbar

The banner template is patched (in the image build) to load content/custom/logo.svg, falling back to content/custom/logo.jpg and then the built-in sample logo.

To customise, mount a host directory (same layout) over /branding — uncomment the volumes block on the fhir service in docker-compose.yml and set BRANDING_DIR, or with plain Docker:

docker run -v /path/to/branding:/branding:ro …

The name can also be set with HAPI_FHIR_TESTER_HOME_NAME; the content path with HAPI_FHIR_CUSTOM_CONTENT_PATH (defaults to the mounted /branding).

Terminology

Hibernate Search (Lucene) is enabled in fhir-server/application.yaml so the terminology service can expand ValueSets ($expand) and validate coded values. The index directory defaults to /tmp/lucenefiles (override with HSEARCH_DIR). Value sets are pre-expanded at IG install time, so codes are available without loading external terminologies like SNOMED (only sets that filter an unloaded system, such as full SNOMED subsets, can't be expanded).

Testing

Integration tests load the CH EMR IG and assert that the IG loads and profile validation rejects an invalid resource. The test logic is provider-agnostic; there are two entrypoints.

Locally, inside a multipass VM (podman):

./tests/multipass.sh          # creates the VM if missing, installs podman, runs everything

In CI (.github/workflows/ci.yml) the same tests/provision.sh + tests/run-tests.sh run directly on the runner with podman.

To run the assertions against an already-running stack:

./tests/provision.sh          # build + up + wait (COMPOSE=podman-compose|docker compose)
./tests/run-tests.sh          # FHIR_BASE configurable

Repository layout

build.sh                 # build the FHIR image locally (:local tag)
docker-compose.yml       # db + fhir (pulls the published image)
docker-compose.build.yml # override: build fhir from source instead of pulling
fhir-server/             # Dockerfile, entrypoint (IG_URLS expansion), application.yaml
tests/                   # provision.sh, run-tests.sh, multipass.sh
.github/workflows/ci.yml # tests + publish image to GHCR