Skip to content
Select theme

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.

Terminal window
./dev.sh start # build images, template configs, bring the stack up
./dev.sh stop # tear down, removing volumes and networks

start 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.

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"])

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.

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.

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:

  1. chmod -R 777 /volumes
  2. generate_certs.sh: installs an mkcert root 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 key ega.sec, and builds the truststore
  3. copy_certificates_to_dest.sh: distributes that material to each service’s volume, renamed to whatever that service expects
  4. copy_confs_to_dest.sh: RabbitMQ, heartbeat, PostgreSQL, CEGA MQ and SDA configuration
  5. copy_json_schemas_to_dest.sh: downloads the SDA message schemas
  6. replace_template_variables.sh: the second substitution pass
  7. change_ownerships.sh: sets the uid and gid each container expects, notably 65534 for the SDA services and 100:101 for RabbitMQ
  8. touch /storage/ready, then tail -f /dev/null to 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.

Compose releases each service the moment its own conditions are met, so the tiers below overlap rather than running strictly one after another:

  • cegaauth declares no dependencies and starts immediately, in parallel with file-orchestrator.
  • mq, db, cegamq, postgres, redis, tsd wait only for the ready file.
  • proxy, interceptor, heartbeat-pub, heartbeat-sub additionally wait for mq to be healthy. proxy also wants tsd, cegamq, postgres and redis merely started, and heartbeat-sub wants redis started.
  • ingest, verify, finalize, mapper, intercept, doa wait for both db and mq to be healthy. Because db’s healthcheck is pg_isready, this tier typically comes up after the previous one even though nothing orders them.
  • e2e-tests waits for proxy, doa, cegamq, db, mq and file-orchestrator to all report healthy.

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.

After changing a single module you rarely want a full rebuild. Each of these rebuilds and redeploys just that piece:

Terminal window
# 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_pub
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 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:

Terminal window
./gradlew spotlessApply # fix formatting
./gradlew spotlessCheck # verify only

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 5673
6379 8088 8443 10443 15671 15672 25672

If you need different ports, remap them in the compose template rather than editing generated files.

See troubleshooting for the failure modes you are most likely to hit.

Built from f91944d