Reconciliation bot that derives ip_map_branch.txt + docker/scripts/demoLibrary.source
from upstream openemr/openemr state.
See the G6 design in the release-mechanism migration plan (PR #12598 on openemr/openemr, docs/release-mechanism-migration-from-devops.md) for full background. Short version:
- What: reads upstream
release-targets.yml, per-branchdocker/release/DockerfileARGs (Alpine + PHP), master'sdocker/flex/DockerfileARGs (the anchor for master-demo Alpine selection AND for the up-for-grabs flex image -- both Alpine and PHP), theci/apache_*directory listing on master (source of truth for the supported-PHP set), and master's flex-build workflow files.github/workflows/docker-build-*.yml(source of truth for which Alpine+PHP combinations actually build); reconciles cluster assignments + flex images against current demo_farm state. - Sticky cluster identity: each cluster maps to an externally-referenced
subdomain (e.g.
eight->eight.openemr.io), so the bot reads the current file as state and applies a diff rather than rendering from scratch. - Section ownership (per G6 matrix — see the design doc linked above):
- Production (
fivefamily): fixed clusters;branchcolumn set to thelatest-tagged row'sopenemr_version_ref(e.g.v8_0_0_3); description regenerated from a template using the latest row'sdocker_tagsprimary version (shortest pure-numeric tag -- e.g.8.0.0out of"8.0.0,8.0.0.3,latest"): base row readsPublic OpenEMR <ver> Production Demo,_areadsAlternate Public OpenEMR <ver> Production Demo,_breadsAnother Alternate Public OpenEMR <ver> Production Demo; flex image from that branch's Dockerfile. - Up-for-grabs (
fourfamily): rows preserved verbatim (community claims); flex image taken directly from master'sdocker/flex/Dockerfile-- bothARG ALPINE_VERSIONandARG PHP_VERSIONused verbatim (e.g.flex-3.23-php-8.5). - Master demos: one cluster per supported PHP. The supported set is the
unique PHP prefixes (
apache_82_*-> 8.2,apache_85_*-> 8.5, etc.) across master'sci/apache_*directory listing. Sticky assignment from prior state; new from parked; retired returns to parked. The flex image for each cluster isflex-<Alpine>-php-<PHP>, where<Alpine>is chosen by starting at master'sdocker/flex/DockerfileARG ALPINE_VERSIONand, if that Alpine doesn't support the cluster's PHP per the flex matrix, dropping down to the next-lower Alpine that does (e.g. Alpine 3.23 dropped PHP 8.2, so the PHP 8.2 cluster falls back to 3.22). The selection is never higher than master flex's Alpine -- the flex Dockerfile is the upper bound. The matrix is read from.github/workflows/docker-build-<NN>.yml+docker-build-edge.ymlon master;docker-build-flex-core.yml(reusable) anddocker-build-release.yml(production) are skipped. Edge is excluded from canonical demo selection. - Release demos: one cluster per non-master row in
release-targets.yml(multiple rows under the samerel-*branch produce multiple clusters -- one per row). Thelatestrow also drives the production demos in addition to its own release-demo cluster. Per row, sticky assignment uses a 3-tier priority:- existing release-demo cluster whose CURRENT
branchcolumn (col 3) exactly matches the row'sopenemr_version_ref(typical no-op case), - else existing release-demo cluster whose CURRENT
branchcolumn matches the row'sbranchfield (single-row lifecycle: cluster previously pinned torel-810rolls forward tov8_1_0when the row'sopenemr_version_refadvances), - else claim from parked (new row case).
Already-claimed clusters in the current run are skipped at each tier.
After all rows assigned, any release-demo cluster NOT claimed in this
run is released to parked (retired branch case). Col 3 = the row's
openemr_version_ref. Description regenerated from a template using the row'sdocker_tagsprimary version (shortest pure-numeric tag); the label is shape-derived from col 3:Release Demoif col 3 matchesv<digits>_<digits>_<digits>(_<digits>)?(tag), elseDevelopment Demo(branch ref likerel-810). Alpine + PHP come from that rel branch'sdocker/release/DockerfileARGs.
- existing release-demo cluster whose CURRENT
- Parked: bench cluster pool.
- Miscellaneous: preserved verbatim.
- Production (
# dry-run against your demo_farm checkout
./derive.sh --dry-run
# write mode (mutates files in place -- this is what the daily reconcile uses)
./derive.sh --writederive.sh --help prints the full usage.
- Zero (or more than one) non-unreleased
latestrows inrelease-targets.yml. - Parked bench empty when a new cluster is needed (adding a parked cluster first is required to preserve cluster->subdomain stability -- the bot refuses to invent cluster names).
ARG ALPINE_VERSIONorARG PHP_VERSIONnot parseable from a branch'sdocker/release/Dockerfile.ARG ALPINE_VERSIONorARG PHP_VERSIONnot parseable from master'sdocker/flex/Dockerfile(the anchor for master demos + up-for-grabs).- No
ci/apache_<NN>_*directories on upstream master (would yield an empty supported-PHP set). - No
docker-build-<NN>.ymlordocker-build-edge.ymlflex-matrix workflow files on upstream master. - A PHP version from
ci/apache_*not supported by master flex Dockerfile's Alpine or any lower non-edge Alpine in the flex matrix (canonical master demos can't fall back to edge, and the master flex Alpine is the upper bound -- the bot won't pick higher). alpine_versionorphp_versionsnot parseable from adocker-build-*.ymlworkflow.
./fixtures-and-tests/test.shEach fixture under fixtures-and-tests/fixtures/<name>/ carries:
current/-- inputs (current demo_farm state, mirrors what's in the repo root +docker/scripts/)upstream/<ref>/...-- simulated upstream openemr/openemr files, fetched via--upstream-base file://...expected/-- what derive.sh should produce (orexpected/fail.txtwith a substring expected in the failure message)
Add a new fixture by copying an existing one, modifying inputs, then re-capturing expected:
WS=$(mktemp -d) && cp -a fixtures-and-tests/fixtures/<your-name>/current/. $WS/ && \
./derive.sh --current-dir $WS \
--upstream-base "file://$PWD/fixtures-and-tests/fixtures/<your-name>/upstream" \
--write && \
cp -a $WS/. fixtures-and-tests/fixtures/<your-name>/expected/ && rm -rf $WSThe derive-ip-map workflow runs derive.sh --write and opens a PR on diff.
It fires on three triggers:
- Daily 07:00 UTC cron — load-bearing self-healing fallback; catches any drift that wasn't picked up by an event trigger (e.g., the bot was down during a push, or a maintainer hand-edited demo_farm state).
repository_dispatcheventrelease-targets-changed— fired by openemr/openemr's.github/workflows/notify-release-targets-changed.ymlon push to master touching.github/release-targets.yml. Eliminates the up-to-24h lag for the most common trigger condition (a release-targets row changes → demo_farm reconciles within minutes).- Manual
workflow_dispatch— input dropdown picksreconcile(write- PR) or
dry-run(preview only, no branch/PR side effects, output uploaded as thederive-outputartifact).
- PR) or
In all three trigger paths the reconcile job:
- Runs
derive.sh --writeand checksip_map_branch.txt+docker/scripts/demoLibrary.sourcefor any diff vsmaster. Diff detection is scoped to those two files so unrelated edits totools/auto-derive/can't trip a bot PR. - On diff: force-pushes the stable branch
auto-derive/reconciliationand opens (or updates, if already open) a PR titled[auto-derive] reconcile demo_farm against upstream openemr master. - On no diff: if a reconciliation PR is open from a prior run, closes it and deletes the remote branch (upstream drift reverted).
The branch is stable + force-pushed, so the bot updates the same PR across days rather than spamming new ones. Merge to land the reconciled state; the bot quiets until the next genuine drift.
Caveat: the bot PR is created via GITHUB_TOKEN, so downstream
workflows (rabbit, CI) don't run on it automatically. The fixture suite
job in this same workflow is the authoritative algorithm validator. To
manually re-trigger CI on the PR, push an empty commit:
git commit --allow-empty -m 'trigger ci' && git pushThe auto-derive workstream (all shipped):
- PR #1: scaffold + dry-run output (artifact + step summary). No live PR opening.
- PR #2: write + auto-PR mode (force-push stable branch, open or update PR on diff, close on no-diff).
- PR #3: atomic flip -- wired
repository_dispatch types=release-targets-changedlistener and retired.github/workflows/bump-tag.yml+ thetools/release/PHP toolchain.