Skip to content

Commit 10e07ae

Browse files
authored
feat(ci): render Docker Hub readme from versions.yml at build time (#715)
Fixes #709 #### Short description of what this resolves: Step 15 of the release runbook — updating the Docker Hub readme on https://hub.docker.com/r/openemr/openemr — was manual: maintainers edited `docker/openemr/OVERVIEW.md` by hand, then a workflow propagated it to Docker Hub. This PR removes that manual step. The readme is now rendered from `tools/release/versions.yml` (the same registry the slot rotation workflow already uses) and pushed by each production build workflow after a successful image push. #### Changes proposed in this pull request: - New Twig template `tools/release/templates/dockerhub-overview.md.twig` (renamed from `docker/openemr/OVERVIEW.md`, with version-dependent values parameterized on slot scalars). - New renderer `OpenEMR\Release\DockerHubOverviewRenderer` (`tools/release/src/`) with a thin Symfony Console wrapper at `tools/release/bin/render-dockerhub-overview.php` and a `release:render-dockerhub-overview` Task entry. Unit tests cover slot interpolation, determinism, and error paths. - New composite action `.github/actions/push-dockerhub-readme` that checks out, renders, then calls `peter-evans/dockerhub-description@v5`. - Build workflows `build-704`, `build-800`, `build-810`, `build-811` call the composite action as their final step (last-writer-wins is benign because renders are deterministic). - Retire `.github/workflows/dockerhub-description.yml` and the hand-edited `docker/openemr/OVERVIEW.md`. Drop the corresponding entry from `tools/release/versions.yml`. - Drop the per-build dated tags (e.g. `8.0.0.3-2026-03-25`) from the readme — they're an immutable-naming mechanism for pinning, not date-display, and Docker Hub's Tags page already lists them. Replaced with one sentence explaining the scheme. #### Notes - Blocked on the Docker Hub credential rotation in #714 — the previous workflow has been failing with `Forbidden` on its last two runs. The new push uses the same `DOCKERHUB_USERNAME` / `DOCKERHUB_TOKEN` secrets, so the same fix unblocks both. - Flex tracks (`build-322`, `build-323`, `build-edge`, `build-flex-core`) are intentionally not wired up — they aren't in the rotation registry and their portion of the readme is hand-curated. Easy follow-up if/when they join the rotation. - Follow-up on `openemr/openemr` to remove step 15 from `docs/RELEASE_PROCESS.md`'s release runbook. #### Test plan - `composer check` clean in `tools/release/` (phpcs, phpstan, rector dry-run, require-checker, phpunit — 83 tests). - `actionlint` clean on the four modified build workflows. - Local render of the template against the real `versions.yml` produces the expected markdown. - End-to-end push verifiable once #714 lands: workflow_dispatch on `build-800.yml` from a topic branch and confirm the Docker Hub readme reflects the rendered output.
1 parent e1a5b49 commit 10e07ae

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
@@ -161,6 +161,15 @@ tasks:
161161
php bin/check-dockerhub-credential.php
162162
{{if .REPOSITORY}}--repository={{shellQuote .REPOSITORY}}{{end}}
163163
164+
release:render-dockerhub-overview:
165+
desc: Render the Docker Hub readme for openemr/openemr from versions.yml
166+
deps: [setup]
167+
cmds:
168+
- >-
169+
php bin/render-dockerhub-overview.php
170+
--repo={{shellQuote (default "../.." .ROTATE_REPO)}}
171+
{{if .OUTPUT}}--output={{shellQuote .OUTPUT}}{{end}}
172+
164173
release:verify-tag:
165174
desc: Verify a release tag is annotated and well-formed per #664 spec
166175
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)