Skip to content

Commit 87c4626

Browse files
committed
feat(ci): render Docker Hub readme from versions.yml at build time
The Docker Hub readme update for openemr/openemr was previously triggered by edits to docker/openemr/OVERVIEW.md, which release runbook step 15 required maintainers to update by hand. The peter-evans/dockerhub-description workflow then propagated the file. This left the readme out of sync whenever the manual edit was forgotten and was the sole reason a maintainer had to touch a markdown file as part of the release runbook. Replace the hand-edited file with a Twig template (tools/release/templates/dockerhub-overview.md.twig) rendered from tools/release/versions.yml — the same registry that already drives slot rotation for build workflows and Dockerfiles. A new composite action (.github/actions/push-dockerhub-readme) renders the template and pushes to the Docker Hub API. Each production build workflow (build-704, build-800, build-810, build-811) calls it after a successful image push. Render is deterministic from versions.yml so the same-day race between nightly cron builds is benign — last writer wins with identical content. Drop the per-build dated tags (e.g. 8.0.0.3-2026-03-25) from the readme. Each build still pushes the immutable dated tag for pinning; users who want a specific build find it on the Docker Hub Tags page or via docker inspect. The readme now explains the scheme in one sentence instead of mirroring listings Docker Hub already displays natively. Closes #709. Assisted-by: Claude Code
1 parent aca0cd4 commit 87c4626

12 files changed

Lines changed: 364 additions & 30 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: 'Push Docker Hub readme for openemr/openemr'
2+
description: 'Render the readme from versions.yml + the Twig template and PATCH it to Docker Hub'
3+
4+
inputs:
5+
username:
6+
description: 'Docker Hub username'
7+
required: true
8+
password:
9+
description: 'Docker Hub access token (R/W/D scope on openemr/openemr)'
10+
required: true
11+
repository:
12+
description: 'Docker Hub repository to update'
13+
required: false
14+
default: 'openemr/openemr'
15+
16+
runs:
17+
using: 'composite'
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v6
21+
22+
- name: Setup PHP
23+
uses: shivammathur/setup-php@v2
24+
with:
25+
php-version: '8.5'
26+
27+
- name: Install Task
28+
uses: arduino/setup-task@v2
29+
30+
- name: Render readme
31+
shell: bash
32+
working-directory: tools/release
33+
run: task release:render-dockerhub-overview OUTPUT="${RUNNER_TEMP}/dockerhub-readme.md"
34+
35+
- name: Push readme to Docker Hub
36+
uses: peter-evans/dockerhub-description@v5
37+
with:
38+
username: ${{ inputs.username }}
39+
password: ${{ inputs.password }}
40+
repository: ${{ inputs.repository }}
41+
readme-filepath: ${{ runner.temp }}/dockerhub-readme.md

.github/workflows/build-704.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,9 @@ jobs:
2929
platforms: linux/amd64,linux/arm64
3030
push: true
3131
no-cache: true
32+
33+
- name: Push Docker Hub readme
34+
uses: ./.github/actions/push-dockerhub-readme
35+
with:
36+
username: ${{ secrets.DOCKERHUB_USERNAME }}
37+
password: ${{ secrets.DOCKERHUB_TOKEN }}

.github/workflows/build-800.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,9 @@ jobs:
2929
platforms: linux/amd64,linux/arm64
3030
push: true
3131
no-cache: true
32+
33+
- name: Push Docker Hub readme
34+
uses: ./.github/actions/push-dockerhub-readme
35+
with:
36+
username: ${{ secrets.DOCKERHUB_USERNAME }}
37+
password: ${{ secrets.DOCKERHUB_TOKEN }}

.github/workflows/build-810.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,9 @@ jobs:
8888
-t openemr/openemr:next \
8989
-t "openemr/openemr:8.1.0-${{ steps.build_date.outputs.date }}" \
9090
"${digests[@]}"
91+
92+
- name: Push Docker Hub readme
93+
uses: ./.github/actions/push-dockerhub-readme
94+
with:
95+
username: ${{ secrets.DOCKERHUB_USERNAME }}
96+
password: ${{ secrets.DOCKERHUB_TOKEN }}

.github/workflows/build-811.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,9 @@ jobs:
8888
-t openemr/openemr:dev \
8989
-t "openemr/openemr:8.1.1-${{ steps.build_date.outputs.date }}" \
9090
"${digests[@]}"
91+
92+
- name: Push Docker Hub readme
93+
uses: ./.github/actions/push-dockerhub-readme
94+
with:
95+
username: ${{ secrets.DOCKERHUB_USERNAME }}
96+
password: ${{ secrets.DOCKERHUB_TOKEN }}

.github/workflows/dockerhub-description.yml

Lines changed: 0 additions & 18 deletions
This file was deleted.

tools/release/Taskfile.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,15 @@ tasks:
153153
php bin/lint-versions.php
154154
--repo={{shellQuote (default "../.." .ROTATE_REPO)}}
155155
156+
release:render-dockerhub-overview:
157+
desc: Render the Docker Hub readme for openemr/openemr from versions.yml
158+
deps: [setup]
159+
cmds:
160+
- >-
161+
php bin/render-dockerhub-overview.php
162+
--repo={{shellQuote (default "../.." .ROTATE_REPO)}}
163+
{{if .OUTPUT}}--output={{shellQuote .OUTPUT}}{{end}}
164+
156165
release:verify-tag:
157166
desc: Verify a release tag is annotated and well-formed per #664 spec
158167
requires:
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
/**
5+
* Render the Docker Hub readme for openemr/openemr.
6+
*
7+
* Called from the build workflows after a successful image push; the rendered
8+
* output is fed to peter-evans/dockerhub-description, which PATCHes Docker
9+
* Hub's repo description endpoint.
10+
*
11+
* @package openemr-devops
12+
* @link https://www.open-emr.org
13+
* @author Michael A. Smith <michael@opencoreemr.com>
14+
* @copyright Copyright (c) 2026 OpenCoreEMR Inc.
15+
* @license https://github.com/openemr/openemr-devops/blob/master/LICENSE GNU General Public License 3
16+
*/
17+
18+
declare(strict_types=1);
19+
20+
require dirname(__DIR__) . '/vendor/autoload.php';
21+
22+
use OpenEMR\Release\DockerHubOverviewRenderer;
23+
use Symfony\Component\Console\Input\InputInterface;
24+
use Symfony\Component\Console\Input\InputOption;
25+
use Symfony\Component\Console\Output\OutputInterface;
26+
use Symfony\Component\Console\SingleCommandApplication;
27+
28+
(new SingleCommandApplication())
29+
->setName('render-dockerhub-overview')
30+
->setDescription('Render the Docker Hub readme for openemr/openemr from versions.yml')
31+
->addOption(
32+
'repo',
33+
null,
34+
InputOption::VALUE_REQUIRED,
35+
'Path to the repo root',
36+
getcwd() === false ? '.' : getcwd(),
37+
)
38+
->addOption(
39+
'registry',
40+
null,
41+
InputOption::VALUE_REQUIRED,
42+
'Path to versions.yml (defaults to <repo>/tools/release/versions.yml)',
43+
)
44+
->addOption(
45+
'template-dir',
46+
null,
47+
InputOption::VALUE_REQUIRED,
48+
'Twig template directory (defaults to <repo>/tools/release/templates)',
49+
)
50+
->addOption('output', 'o', InputOption::VALUE_REQUIRED, 'Output file (defaults to stdout)')
51+
->setCode(function (InputInterface $input, OutputInterface $output): int {
52+
$repo = $input->getOption('repo');
53+
if (!is_string($repo) || $repo === '') {
54+
$output->writeln('<error>--repo is required</error>');
55+
return 1;
56+
}
57+
58+
$registry = $input->getOption('registry');
59+
if (!is_string($registry) || $registry === '') {
60+
$registry = $repo . '/tools/release/versions.yml';
61+
}
62+
if (!is_file($registry)) {
63+
$output->writeln("<error>Registry not found: {$registry}</error>");
64+
return 1;
65+
}
66+
67+
$templateDir = $input->getOption('template-dir');
68+
if (!is_string($templateDir) || $templateDir === '') {
69+
$templateDir = $repo . '/tools/release/templates';
70+
}
71+
if (!is_dir($templateDir)) {
72+
$output->writeln("<error>Template directory not found: {$templateDir}</error>");
73+
return 1;
74+
}
75+
76+
$rendered = (new DockerHubOverviewRenderer($registry, $templateDir))->render();
77+
78+
$target = $input->getOption('output');
79+
if (is_string($target) && $target !== '') {
80+
file_put_contents($target, $rendered);
81+
$output->writeln("Rendered to <info>{$target}</info>");
82+
return 0;
83+
}
84+
$output->write($rendered);
85+
return 0;
86+
})
87+
->run();
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
/**
4+
* Render the Docker Hub readme for openemr/openemr from versions.yml.
5+
*
6+
* The rendered output is what the build workflows push to Docker Hub via the
7+
* peter-evans/dockerhub-description action. Pure: same input → same output;
8+
* no filesystem side effects beyond reading the registry path.
9+
*
10+
* @package openemr-devops
11+
* @link https://www.open-emr.org
12+
* @author Michael A. Smith <michael@opencoreemr.com>
13+
* @copyright Copyright (c) 2026 OpenCoreEMR Inc.
14+
* @license https://github.com/openemr/openemr-devops/blob/master/LICENSE GNU General Public License 3
15+
*/
16+
17+
declare(strict_types=1);
18+
19+
namespace OpenEMR\Release;
20+
21+
use Symfony\Component\Yaml\Yaml;
22+
use Twig\Environment;
23+
use Twig\Loader\FilesystemLoader;
24+
25+
final readonly class DockerHubOverviewRenderer
26+
{
27+
public const TEMPLATE_NAME = 'dockerhub-overview.md.twig';
28+
29+
public function __construct(
30+
private string $registryPath,
31+
private string $templateDir,
32+
) {
33+
}
34+
35+
public function render(): string
36+
{
37+
$registry = $this->loadRegistry();
38+
$twig = new Environment(
39+
new FilesystemLoader($this->templateDir),
40+
['autoescape' => false],
41+
);
42+
return $twig->render(self::TEMPLATE_NAME, [
43+
'current' => $this->slot($registry, 'current'),
44+
'next' => $this->slot($registry, 'next'),
45+
'dev' => $this->slot($registry, 'dev'),
46+
]);
47+
}
48+
49+
/**
50+
* @return array<string, array<string, string>>
51+
*/
52+
private function loadRegistry(): array
53+
{
54+
$parsed = Yaml::parseFile($this->registryPath);
55+
if (!is_array($parsed) || !isset($parsed['slots']) || !is_array($parsed['slots'])) {
56+
throw new \RuntimeException("Registry malformed: {$this->registryPath}");
57+
}
58+
/** @var array<string, array<string, string>> $slots */
59+
$slots = $parsed['slots'];
60+
return $slots;
61+
}
62+
63+
/**
64+
* @param array<string, array<string, string>> $registry
65+
* @return array<string, string>
66+
*/
67+
private function slot(array $registry, string $name): array
68+
{
69+
if (!isset($registry[$name])) {
70+
throw new \RuntimeException("Registry missing slot: {$name}");
71+
}
72+
return $registry[$name];
73+
}
74+
}

docker/openemr/OVERVIEW.md renamed to tools/release/templates/dockerhub-overview.md.twig

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
[OpenEMR](https://www.open-emr.org) is the most popular open source electronic health records and medical practice management solution. OpenEMR's goal is a superior alternative to its proprietary counterparts. With passionate volunteers and contributors dedicated to guarding OpenEMR's status as a free, open source software solution for medical practices with a commitment to openness, kindness and cooperation.
22

3-
**Current production OpenEMR version is 8.0.0**
3+
**Current production OpenEMR version is {{ current.full }}**
44

55
Supported tags:
66

7-
* `8.0.0`, `8.0.0.3`, `8.0.0.3-2026-03-25`, `latest` [Dockerfile](https://github.com/openemr/openemr-devops/blob/master/docker/openemr/8.0.0/Dockerfile) | [Instructions](https://github.com/openemr/openemr-devops/tree/master/docker/openemr/8.0.0/#openemr-official-docker-image)
8-
* `7.0.4`, `7.0.4.0`, `7.0.4.0-2025-12-24` [Dockerfile](https://github.com/openemr/openemr-devops/blob/master/docker/openemr/7.0.4/Dockerfile) | [Instructions](https://github.com/openemr/openemr-devops/tree/master/docker/openemr/7.0.4/#openemr-official-docker-image)
9-
* `8.1.0`, `next` [Dockerfile](https://github.com/openemr/openemr-devops/blob/master/docker/openemr/8.1.0/Dockerfile) | [Instructions](https://github.com/openemr/openemr-devops/tree/master/docker/openemr/8.1.0/#openemr-official-docker-image)
10-
* `8.1.1`, `dev` [Dockerfile](https://github.com/openemr/openemr-devops/blob/master/docker/openemr/8.1.1/Dockerfile) | [Instructions](https://github.com/openemr/openemr-devops/tree/master/docker/openemr/8.1.1/#openemr-official-docker-image)
7+
* `{{ current.full }}`, `{{ current.patch }}`, `latest` [Dockerfile](https://github.com/openemr/openemr-devops/blob/master/docker/openemr/{{ current.docker_dir }}/Dockerfile) | [Instructions](https://github.com/openemr/openemr-devops/tree/master/docker/openemr/{{ current.docker_dir }}/#openemr-official-docker-image)
8+
* `7.0.4`, `7.0.4.0` [Dockerfile](https://github.com/openemr/openemr-devops/blob/master/docker/openemr/7.0.4/Dockerfile) | [Instructions](https://github.com/openemr/openemr-devops/tree/master/docker/openemr/7.0.4/#openemr-official-docker-image)
9+
* `{{ next.full }}`, `next` [Dockerfile](https://github.com/openemr/openemr-devops/blob/master/docker/openemr/{{ next.docker_dir }}/Dockerfile) | [Instructions](https://github.com/openemr/openemr-devops/tree/master/docker/openemr/{{ next.docker_dir }}/#openemr-official-docker-image)
10+
* `{{ dev.full }}`, `dev` [Dockerfile](https://github.com/openemr/openemr-devops/blob/master/docker/openemr/{{ dev.docker_dir }}/Dockerfile) | [Instructions](https://github.com/openemr/openemr-devops/tree/master/docker/openemr/{{ dev.docker_dir }}/#openemr-official-docker-image)
1111
* `flex-3.23-php-8.5` `flex-3.23` `flex` [Dockerfile](https://github.com/openemr/openemr-devops/blob/master/docker/openemr/flex/Dockerfile) | [Instructions](https://github.com/openemr/openemr-devops/tree/master/docker/openemr/flex/#openemr-official-docker-image)
1212
* `flex-3.23-php-8.4` [Dockerfile](https://github.com/openemr/openemr-devops/blob/master/docker/openemr/flex/Dockerfile) | [Instructions](https://github.com/openemr/openemr-devops/tree/master/docker/openemr/flex/#openemr-official-docker-image)
1313
* `flex-3.23-php-8.3` [Dockerfile](https://github.com/openemr/openemr-devops/blob/master/docker/openemr/flex/Dockerfile) | [Instructions](https://github.com/openemr/openemr-devops/tree/master/docker/openemr/flex/#openemr-official-docker-image)
@@ -18,6 +18,8 @@ Supported tags:
1818
* `flex-edge-php-8.4` [Dockerfile](https://github.com/openemr/openemr-devops/blob/master/docker/openemr/flex/Dockerfile) | [Instructions](https://github.com/openemr/openemr-devops/tree/master/docker/openemr/flex/#openemr-official-docker-image)
1919
* `flex-edge-php-8.3` [Dockerfile](https://github.com/openemr/openemr-devops/blob/master/docker/openemr/flex/Dockerfile) | [Instructions](https://github.com/openemr/openemr-devops/tree/master/docker/openemr/flex/#openemr-official-docker-image)
2020

21+
Each production build also pushes an immutable `X.Y.Z-YYYY-MM-DD` tag (e.g. `{{ current.patch }}-2026-03-25`) so a deployment can pin a specific build and survive future rebuilds of the same version. See the [Tags page](https://hub.docker.com/r/openemr/openemr/tags) for the most recent dated tag of each track.
22+
2123
This [OpenEMR](http://www.open-emr.org) official docker supports automated installation/configuration of OpenEMR. It requires a companion mysql/mariadb container to work.
2224

2325
* Required environment settings for auto installation are `MYSQL_HOST` and `MYSQL_ROOT_PASS`
@@ -36,4 +38,4 @@ This [OpenEMR](http://www.open-emr.org) official docker supports automated insta
3638
* Can turn on kubernetes/orchestration/swarm/replication support with the `SWARM_MODE` (set it to 'yes'). This only works for the `5.0.2+` and `flex*` images on Docker Swarm and can see [docker-compose.yml](https://gist.github.com/bradymiller/980086836af187285bf28b8db9eecabc) for an example of use in Docker Swarm. This only works for the `6.0.0+` and `flex*` images on Kubernetes and can see [README.md](https://github.com/openemr/openemr-devops/tree/master/kubernetes) for examples of use in Kubernetes. Note the shared volumes /var/www/localhost/htdocs/openemr/sites, /etc/ssl and /etc/letsencrypt are needed in this mode.
3739
* Can force database SSL or X509 connection by setting one of the following parameters to "1", `FORCE_DATABASE_SSL_CONNECT` or `FORCE_DATABASE_X509_CONNECT`. (ensure you have the SSL or X509 properly configured in the database). This is supported in `7.0.1`+ and flex (`3.15-8`, `edge`, `3.17`+) series.
3840
* The `flex*` images (`flex-3.22` is Alpine 3.22, `flex-edge` is Alpine Edge, etc.) are for testers and developers and allows use of a OpenEMR version from the specified git repository. Required environment settings for these images are `FLEX_REPOSITORY` and (`FLEX_REPOSITORY_BRANCH` or `FLEX_REPOSITORY_TAG`). `FLEX_REPOSITORY` is the public git repository holding the openemr version that will be used. And `FLEX_REPOSITORY_BRANCH` or `FLEX_REPOSITORY_TAG` represent the branch or tag to use in this git repository, respectively. An exception to the above required settings for these images is if the user sets `EMPTY` environment setting to 'yes'; then these images will not install any openemr in it (this gives a developer flexibility to set up shared volume(s) from host). Another special flag in the `flex*` docker series is `FORCE_NO_BUILD_MODE`, which will force the docker to not build openemr dependent packages/assets via composer/npm if it is set to 'yes'. Two other special flags in the `flex*` docker series is `EASY_DEV_MODE` and `EASY_DEV_MODE_NEW`, which allows use of the "easy development environment" if they are set to 'yes'. Another special flag in the `flex*` docker series is `INSANE_DEV_MODE`, which allows use of devtools in the "insane development environment" if it is set to 'yes'. Another special flag in the `flex*` docker series is `DEVELOPER_TOOLS`, which installs developer/testing tools/packages. Yet another special flag in the `flex*` docker series is `GITHUB_COMPOSER_TOKEN`, which is used to place a github auth token. Another set of special flags in the `flex*` docker series are `XDEBUG_ON`, which will turn on support for xdebug when set to 1 and `XDEBUG_PROFILER_ON`, which will turn on support for xdebug profiling when set to `1` (optional setting for xdebug support are `XDEBUG_IDE_KEY`, `XDEBUG_CLIENT_HOST` and `XDEBUG_CLIENT_PORT`). Another special setting in the `flex*` docker series is `DEMO_MODE`, which when set to `standard`, will load in standard demo data. Another special setting in the `flex*` docker series is `SQL_DATA_DRIVE`, which can be set to a path in the docker; all of the *.sql in this path will be loaded into the OpenEMR database.
39-
* Automatic upgrading of OpenEMR is supported when upgrade from `5.0.1` to most recent production image tag (for example `8.0.0`) (note this is not supported in the `flex*` series). In your docker-compose.yml file, you can basically change the image version tag (such as `5.0.1`) to the most recent production image version tag (for example `8.0.0`) and then do a `docker-compose up`. Note it will not work for patch version upgrades (ie. `7.0.3` to `7.0.3.4` will not work). Also noted this will only work if you have set a shared volume for the `/var/www/localhost/htdocs/openemr/sites` directory (it will not work if either you didn't set a shared volume or you set the shared volume to be the entire `/var/www/localhost/htdocs/openemr` directory). Before you do this, recommend backing everything up.
41+
* Automatic upgrading of OpenEMR is supported when upgrade from `5.0.1` to most recent production image tag (for example `{{ current.full }}`) (note this is not supported in the `flex*` series). In your docker-compose.yml file, you can basically change the image version tag (such as `5.0.1`) to the most recent production image version tag (for example `{{ current.full }}`) and then do a `docker-compose up`. Note it will not work for patch version upgrades (ie. `7.0.3` to `7.0.3.4` will not work). Also noted this will only work if you have set a shared volume for the `/var/www/localhost/htdocs/openemr/sites` directory (it will not work if either you didn't set a shared volume or you set the shared volume to be the entire `/var/www/localhost/htdocs/openemr` directory). Before you do this, recommend backing everything up.

0 commit comments

Comments
 (0)