The dev.sh workflow
dev.sh lives in the repository root and automates the whole local environment. You must run
it from the repository root. Every path in it is relative to the current working directory, not
to the script: it sources ./e2e/env.sh at load time, and each subcommand cds into that
directory by name. Invoked from anywhere else it fails immediately.
Run it with no arguments for an interactive menu, or pass a subcommand directly.
Starting and stopping
Section titled “Starting and stopping”./dev.sh start # build images, template configs, bring the stack up./dev.sh stop # tear down, removing volumes and networksstart does more than docker compose up. It renders the compose template, generates
certificates and keys, and waits for a bootstrap container to signal that shared volumes are
staged before the dependent services start.
The boot chain
Section titled “The boot chain”Five phases, from the shell to the test runner:
flowchart TB
DEV["<b>./dev.sh start</b><br/>sources <i>e2e/env.sh</i>, enters the module"]
APPLY["<b>bootstrap.sh apply_configs</b><br/>renders docker-compose.yml from the template<br/><i>first template-variable pass</i>"]
CHECK["<b>bootstrap.sh check_requirements</b><br/>Docker running, usable without sudo, Compose v2 present"]
UP["<b>docker compose up --build -d</b><br/>builds the local images, then starts containers"]
DEV --> APPLY --> CHECK --> UP
subgraph gate["file-orchestrator: the gate every service waits on"]
direction TB
CERTS["<b>generate_certs.sh</b><br/>mkcert CA, server and client certs,<br/>JWT RSA pair, Crypt4GH <i>ega.sec</i>"]
COPY["<b>copy_certificates, copy_confs</b><br/>fan the material out into the shared volumes"]
SCHEMAS["<b>copy_json_schemas</b><br/>fetch the SDA message schemas from GitHub<br/><i>the only runtime internet call in the stack</i>"]
REPL["<b>replace_template_variables.sh</b><br/><i>second template-variable pass, inside the volumes</i>"]
OWN["<b>change_ownerships.sh</b>, then <b>touch /storage/ready</b>"]
CERTS --> COPY --> SCHEMAS --> REPL --> OWN
end
UP --> CERTS
UP -.->|"no dependencies at all"| CEGAAUTH["<b>cegaauth</b>"]
OWN ==>|"healthcheck sees the ready file"| T1
T1["<b>mq, db, cegamq, postgres, redis, tsd</b><br/><i>gated on the ready file alone</i>"]
T2["<b>proxy, interceptor,<br/>heartbeat-pub, heartbeat-sub</b><br/><i>gated on mq healthy</i>"]
T3["<b>ingest, verify, finalize,<br/>mapper, intercept, doa</b><br/><i>gated on db and mq healthy</i>"]
T1 --> T2
T1 --> T3
T2 --> RUN
T3 --> RUN
RUN["<b>e2e-tests</b><br/>gated on proxy, doa, cegamq, db and mq all healthy"]
RUN --> RESULT(["the suite runs, and its exit code is the verdict"])
Phase 1: on the host
Section titled “Phase 1: on the host”dev.sh sources e2e/env.sh at load time, before any function runs. start then cds
into e2e and sources it a second time, so the whole variable set is exported into the
environment that bootstrap.sh and docker compose inherit.
bootstrap.sh apply_configs copies docker-compose.template.yml to docker-compose.yml, scans
it for every <<VAR>> placeholder, and substitutes each one from the exported environment. Any
placeholder with no matching variable is reported as missing, along with the export line you
would need to add. The rendered docker-compose.yml is a generated file: edit the template, not
the output.
bootstrap.sh check_requirements then verifies that Docker is running, that the current user can
drive it without sudo, and that docker compose is available.
Phase 2: image builds
Section titled “Phase 2: image builds”docker compose up --build builds six images. Their build contexts differ, which matters when
you are wondering why a change was not picked up:
| Image | Context | Why |
|---|---|---|
tsd-api-mock, tsd-proxy |
repository root | Gradle needs the whole multi-project build |
mq-interceptor, cega-mock |
the service’s own directory | self-contained Go services |
file-orchestrator |
e2e/ |
its scripts and confs live there |
e2e-tests |
repository root | the suite plus cli/lega-commander |
Everything else is pulled: RabbitMQ, PostgreSQL and Redis from Docker Hub, the SDA pipeline
services and Data-Out from ghcr.io/neicnordic/sensitive-data-archive, and the heartbeat image
from ghcr.io/elixir-no. The SDA and RabbitMQ tags are pinned to a version in the compose
template; the rest track latest.
Phase 3: the file-orchestrator gate
Section titled “Phase 3: the file-orchestrator gate”Nothing except cegaauth starts until file-orchestrator is healthy, and its healthcheck is a
single test: does the file /storage/ready exist. That file is the last thing its entrypoint
does, after staging every certificate, configuration file and schema the rest of the stack mounts.
The entrypoint runs, in order:
chmod -R 777 /volumesgenerate_certs.sh: installs anmkcertroot CA, issues one server and one client certificate covering eight names (localhost,db,vault,mq,tsd,proxy,cegamq,doa), exports them to PKCS#12, generates a 4096-bit RSA pair for JWT signing, generates the Crypt4GH archive keyega.sec, and builds the truststorecopy_certificates_to_dest.sh: distributes that material to each service’s volume, renamed to whatever that service expectscopy_confs_to_dest.sh: RabbitMQ, heartbeat, PostgreSQL, CEGA MQ and SDA configurationcopy_json_schemas_to_dest.sh: downloads the SDA message schemasreplace_template_variables.sh: the second substitution passchange_ownerships.sh: sets the uid and gid each container expects, notably65534for the SDA services and100:101for RabbitMQtouch /storage/ready, thentail -f /dev/nullto stay alive
The steps are chained with &&, so any failure stops the chain before the ready file is
written. The container keeps running, its healthcheck never passes, and every dependent service
waits. A stack that appears to hang at startup is usually this: read docker logs file-orchestrator first.
The healthcheck carries a 180-second start period, because certificate and Crypt4GH key generation can run well past the default grace period under architecture emulation (an arm64 host running the amd64 image); without it the container is declared unhealthy at about 25 seconds and every dependent service aborts before the ready file ever appears.
Phase 4: services, in dependency order
Section titled “Phase 4: services, in dependency order”Compose releases each service the moment its own conditions are met, so the tiers below overlap rather than running strictly one after another:
cegaauthdeclares no dependencies and starts immediately, in parallel withfile-orchestrator.mq,db,cegamq,postgres,redis,tsdwait only for the ready file.proxy,interceptor,heartbeat-pub,heartbeat-subadditionally wait formqto be healthy.proxyalso wantstsd,cegamq,postgresandredismerely started, andheartbeat-subwantsredisstarted.ingest,verify,finalize,mapper,intercept,doawait for bothdbandmqto be healthy. Becausedb’s healthcheck ispg_isready, this tier typically comes up after the previous one even though nothing orders them.e2e-testswaits forproxy,doa,cegamq,db,mqandfile-orchestratorto all report healthy.
Phase 5: the runner
Section titled “Phase 5: the runner”E2E_SUITE (default go) picks which runner image the e2e-tests container builds; everything
below that container is shared, so both suites are measured against exactly the same stack. The
Go runner’s entrypoint selects one binary from E2E_ENV (default fega) and executes it; the
binary reads the staged certificates directly off the volume, so there is no truststore import
step. What each environment targets and needs is covered in
the e2e distributions.
The retiring JUnit runner (E2E_SUITE=java ./dev.sh start) instead imports the generated
/storage/certs/rootCA.pem into the JDK truststore with keytool and selects a test class from
E2E_TESTS_INTEGRATION (FEGA, the default, GDI or EGA_DEV). Its removal, and the
E2E_SUITE switch with it, is tracked in
#851.
The container mounts the TSD inbox read-only, so the suite can assert that the mapper removed the uploaded file. When the suite finishes the container exits, and its exit code is the result. See the caution at the top of this section for how to read it.
Rebuilding one thing
Section titled “Rebuilding one thing”After changing a single module you rarely want a full rebuild. Each of these rebuilds and redeploys just that piece:
# libraries./dev.sh rebuild_clearinghouse./dev.sh rebuild_tsd_file_api_client./dev.sh rebuild_crypt4gh
# services./dev.sh rebuild_and_deploy_proxy./dev.sh rebuild_and_deploy_tsd./dev.sh rebuild_and_deploy_mq_interceptor./dev.sh rebuild_and_deploy_heartbeat_sub./dev.sh rebuild_and_deploy_heartbeat_pubEverything else
Section titled “Everything else”| Command | What it does |
|---|---|
reexecute_tests_in_container |
Rebuild and re-run the end-to-end suite in its container |
apply_all_spotless_checks |
Format all Java and Kotlin sources |
restart_docker_daemon |
Restart the Docker daemon |
replace_root_ca |
Re-install the generated root CA into a container |
cleanup_environment |
Deep clean: remove all project containers, volumes and images |
Formatting before you push
Section titled “Formatting before you push”Formatting is enforced in CI by Spotless, so a badly formatted file fails the build. Either use
the dev.sh wrapper above or call Gradle directly:
./gradlew spotlessApply # fix formatting./gradlew spotlessCheck # verify onlyPorts that must be free
Section titled “Ports that must be free”The stack binds a number of host ports. If something else is already listening on one of them, startup fails:
80 5005 5006 5432 5671 56736379 8088 8443 10443 15671 15672 25672If you need different ports, remap them in the compose template rather than editing generated files.
When it goes wrong
Section titled “When it goes wrong”See troubleshooting for the failure modes you are most likely to hit.
Built from f91944d