1. Architecture and layout
1.1 The repository
upgrade-to-v1/
├── install.sh the upgrade (0.92 -> v1)
├── catch-up.sh reconcile a live v1 site
├── import-forms.sh the form import, standalone
├── import-concepts.sh the concept dictionary, standalone
├── ocl-fix.sh the post-startup OCL concept-name fix
├── bin/
│ └── bahmni_form_import.sh the form importer (installed to /usr/local/bin)
├── lib/
│ ├── core/ config, logging, traps, prompt, cli
│ ├── system/ platform, privilege, deps
│ └── upgrade/ verify, detect, backup, migrate, rollback,
│ postinstall, concepts, reporting, forms, idgen,
│ autopull, dbbackup, oclfix, catchup
└── tests/
└── check-published.sh verifies every module is reachable over raw HTTP
All logic lives in functions inside lib/. The entry scripts hold only their
own main() and the module list they need.
1.2 Module loading and the curl | bash path
Each entry script declares the modules it needs, in dependency order:
EREGISTER_MODULES=(
core/config.sh
core/logging.sh
core/traps.sh
core/prompt.sh
core/cli.sh
system/platform.sh
...
)
If lib/ sits next to the script, it is sourced from there. If it does not —
the curl | bash case, where only one file was downloaded — the script fetches
the modules itself:
- Shallow clone of the repo. One request, always a self-consistent tree, and it resolves the remote's default branch by itself.
- File by file from
EREGISTER_RAW_BASE, if the clone fails.
It proceeds only once every module in the list is present, so a
half-published branch fails here with a message naming the missing file, rather
than with curl's opaque (56) … error: 404 somewhere in the middle of a run.
Tip
Override the module location with EREGISTER_LIB_DIR — useful for a system
install where lib/ lives somewhere other than beside the script.
After adding or renaming anything under lib/ or bin/, push it and run
./tests/check-published.sh. That is what the one-liners fetch.
1.3 On-disk layout
Everything lives under <base>/v1, where <base> is /var/lib by default:
/var/lib/v1/
├── bahmni-docker-ls/ the v1 stack (compose files)
│ └── bahmni-standard/ <- RESTORE_DIR: where compose runs
│ ├── docker-compose.yml
│ ├── restore_bahmni_standard.sh
│ └── run-bahmni.sh
├── standard-config-ls/ Bahmni/clinical app config
├── openmrs-v1-modules/ OpenMRS omods (~246 MB)
├── implementer-interface-release/ the Implementer Interface build
├── clinical-obs-forms/ Form Builder JSON exports
├── dhisconnector_mappings_v1/ DHIS2 connector mappings
├── eregister_concepts_release_v1/ concept dictionary dumps
├── openmrs_reporting_release/ report definitions (serialized_object dump)
├── upgrade-to-v1/ this repo, kept current by catch-up.sh
├── bahmni-backup/
│ ├── openmrsdb_backup.sql the 0.92 dump the restore reads
│ ├── bahmni_config/ the 0.92 config (pinned, restore-only)
│ ├── concepts-preimport-*.sql taken before each dictionary import
│ └── reporting-preimport-*.sql taken before each report-definition import
├── db-backups/ the rolling nightly dumps (mode 0700)
│ ├── openmrs_<stamp>.sql.gz
│ └── latest.sql.gz symlink to the newest
├── form-import/ scratch dir for unattended form imports
├── .eregister-upgrade-complete the stage marker (see §2.6)
├── .bahmni_form_import_state.json per-form sha256 + version
├── .eregister_concept_import_state sha256 of the imported dictionary
└── .eregister_reporting_import_state
Outside <base>:
/usr/local/bin/bahmni-form-import.sh the form importer
/usr/local/bin/eregister-form-import.sh the wrapper cron/systemd runs
/usr/local/bin/eregister-concept-import.sh the concept import runner
/usr/local/bin/eregister-db-backup.sh the nightly database backup
/usr/local/bin/eregister-autopull.sh the repo sync
/etc/eregister/form-import.env EMR credentials (mode 0600)
/var/log/eregister-*.log one log per job
Note
Why the state files live in <base>/v1 and not inside the clones: the
auto-pull job runs git reset --hard on those clones. A deployment record
stored inside one would be wiped by a routine sync.
1.4 The repositories
| Repo | Default ref | Role | Auto-pulled? |
|---|---|---|---|
bahmni-docker-ls |
Bokang-changes |
The v1 stack — compose files | No (pinned) |
standard-config-ls |
Bokang-changes |
Clinical app configuration | Yes |
openmrs-v1-modules |
main |
OpenMRS omods (~246 MB) | Yes |
implementer-interface-release |
main |
Implementer Interface build | Yes |
clinical-obs-forms |
main |
Form Builder JSON exports | Yes |
dhisconnector_mappings_v1 |
master |
DHIS2 connector mappings | Yes |
eregister_concepts_release_v1 |
main |
Concept dictionary dumps | Yes |
openmrs_reporting_release |
master |
Report definitions | Yes |
bahmni_config092 |
main |
The 0.92 config, beside the backup | No (restore-only) |
upgrade-to-v1 |
main |
This toolkit | Self-updated by catch-up.sh |
Two deliberate exclusions from the auto-pull job:
bahmni-docker-lsholds the compose files the running stack reads. It must not drift underneath a running instance, so it moves only when a human runscatch-up.sh.bahmni_config(0.92) is historical. It is what the restore reads; it is checked for presence and never updated.
Pinning a ref across all repos
The repos do not share a branch name, so --target-ref takes a
comma-separated preference list, tried in order against each remote. The
first ref that exists on that remote wins; the repo's own default is the last
resort:
sudo ./install.sh --target-ref Bokang-changes,main
That resolves to Bokang-changes on the stack repos and main on the asset
repos, in one flag. The ref is resolved against the remote before cloning,
so a bad ref fails loudly rather than silently landing on the default branch.
A full 40-character SHA is also accepted, and is handled specially: git clone
--branch rejects a raw SHA, so the repo is cloned in full and the commit is
checked out afterwards.
1.5 Privilege
The scripts detect whether they need elevation and how to get it:
[ℹ] Elevation required for /var/lib; will use sudo.
Everything that writes under <base> or to /usr/local/bin, /etc or
/var/log goes through an internal as_root helper, so the scripts work both
when run as root and when run by a user who can sudo.
Repository ownership
Site clones end up owned by whoever created them — root when the installer ran
under sudo, the operator when it did not. Since git 2.35 a repo whose owner is
not the current user is refused outright:
fatal: detected dubious ownership in repository at '/var/lib/v1/...'
That is how a root-run cron job quietly stops updating a user-owned clone: every command fails, and the caller reads it as "no branch" or "not a repo". Every git call in the toolkit therefore goes through a wrapper:
git_here() { git -c safe.directory='*' "$@"; }
This relaxes only the ownership check, for that one invocation. File permissions still apply exactly as before.
1.6 Prompting
The scripts are "cautious by default": every step that changes something is confirmed individually.
- An answer that is neither yes nor no is treated as a slip — the prompt asks whether you meant to stop, and repeats itself if you did not.
- Prompts read from
/dev/tty, never from the script's own stdin. This is what makescurl … | bashwork: stdin is the script itself. - End-of-input on
/dev/ttytakes the advertised default rather than spinning. --yes(orEREGISTER_ASSUME_YES=1) answers yes to everything. Required for unattended runs, and worth thinking about carefully — see §3.11.
Passwords are never echoed, never passed on a command line where another user
could see them in ps, and never hard-coded:
- The 0.92 database password is prompted, or read from
EREGISTER_DB_PASS. - The EMR password is prompted, or read from
EREGISTER_BAHMNI_PASS, and is stored0600in/etc/eregister/form-import.envfor the unattended runs. - Inside containers,
MYSQL_PWDor the container's ownMYSQL_ROOT_PASSWORDis used, so the password never lands on a process list.