Refines #662 and overlaps with #638.
Goal
Automate the per-release documentation work currently tracked in the wiki (QA and Release Process — Documentation and Steps for an official release) so that the release manager's manual surface shrinks to: edit a generated release-notes draft, sign off on the ONC certification page, and merge three pre-built PRs.
Approach: release-please-shaped, multi-repo
Borrow the release-please pattern (one long-lived PR per release branch, auto-updated on every push, merged to ship) but spread it across the three repos that own different slices of the release. The openemr/openemr PR is the conductor; the other two are downstream and consume its events.
Move docs off the wiki
For docs that are release artifacts (install/upgrade for v8.1.0, OpenAPI spec, EHI/B10 schemaspy output, acknowledgements, release notes, ONC cert page), publish to website-openemr (Hugo, PR-reviewable, version-pinned) and website-openemr-files (large binaries). Leave the wiki for living community references (generic howtos, FAQ).
This eliminates the need for a MediaWiki bot account and makes every release-doc change diffable and reviewable. Redirects from old version-specific URLs become Hugo aliases (one line of frontmatter).
The three PRs
1. openemr/openemr — release-prep PR (the conductor)
Long-lived PR against rel-810, auto-updated on every push. Contains the mechanical changes from the wiki's pre-tag checklist:
version.php: strip -dev from $v_tag
library/globals.inc.php: allow_debug_language default to 0
docker/production/docker-compose.yml: pin image version (vs latest on master)
_rest_routes.inc.php: set version + regenerated swagger/openemr-api.yaml (CLI exists: php bin/console openemr:create-api-documentation)
docker-version increments (root, sites/default/, docker/.../root/)
- New
fsupgrade-N.sh scaffold + Dockerfile edits in upgrade dir
- New
sql/X_Y_Z-to-X_Y_Z+1_upgrade.sql skeleton on master
- Refreshed
acknowledge_license_cert.html
Merging this PR is the "we're shipping" decision. The workflow then creates an annotated tag on the merge commit (git tag -a or the GitHub API with a tag object — never a lightweight ref). Lightweight tags lack author/date/message metadata and break git describe, release tooling, and downstream consumers that introspect tag objects.
2. website-openemr (+ website-openemr-files) — docs PR
Subscribes to pushes on rel-* in openemr/openemr via repository_dispatch. Long-lived PR per release branch.
Generated content:
- OpenAPI YAML → committed to website repo
- EHI/B10 schemaspy HTML tree → pushed to
website-openemr-files under files/openemr-<version>-ehi/ (too large for Hugo repo)
- Install/upgrade pages — Hugo template + version param, replaces wiki copies
- Acknowledgements — generated from
git shortlog v8_0_0..v8_1_0
- Release-notes draft — milestone PRs grouped by
feat: / bug: / refactor: / chore: prefix (matches existing openemr-dev:create-release-change-log CLI conventions)
- Hugo aliases for legacy URLs (
OpenEMR 8 API → OpenEMR 8.1.0 API, etc.)
Each page renders with a release-status shortcode showing DRAFT — based on rel-810 @ <sha> until the conductor's tag event flips it to FINAL. Workflow force-updates the PR branch on every regeneration so history doesn't bloat.
Doesn't gate the tag — consumes it.
3. openemr-devops — infra/test-matrix PR
Per #638's current/next/dev rotation:
- When
rel-810 cuts: next becomes 8.1, current stays 8.0
- When
v8_1_0 tags: current becomes 8.1, drop the prior
Touches CI test matrices, package version refs, raspberrypi/docker pinned versions. Independent of the docs PR; runs in parallel. Should ideally merge before the tag so CI is ready against the new branch.
Workflow trigger DAG
openemr/openemr release-prep PR ── merge → tag v8_1_0
│ │
└── (push to rel-*) ───────────┼──→ website-openemr docs PR
│
└──→ openemr-devops infra PR
Three workflows total, in three repos, all driven by pushes on rel-* and the eventual tag. No new triggers, no cron, no human handoff between workflows.
Irreducibly manual
Even with full automation, the release manager still:
- Triggers the initial branch cut (
rel-N).
- Edits the auto-generated release-notes draft for tone and what's noteworthy.
- Reviews/signs off on the ONC Ambulatory EHR Certification Requirements page.
- (Major releases) writes the marketing piece.
- Merges three pre-built PRs.
Everything else — artifact builds, install/upgrade page rewrites, redirect setup, version bumps, acknowledgement lists, package version pins — is mechanical and lives in the workflows.
What needs doing
In rough dependency order:
Related
Cross-repo contracts
The three plan PRs (openemr/openemr#11896, #665, openemr/website-openemr#82) all consume or emit the same events. Pinned here so the three implementation tracks don't invent slightly-different shapes.
Event names
Emitted by the conductor in openemr/openemr; consumed by openemr-devops and website-openemr (+ website-openemr-files).
| Event |
When |
openemr-rel-cut |
First push to a new rel-* branch |
openemr-rel-update |
Subsequent push to an existing rel-* |
openemr-tag |
Annotated tag created on rel-* HEAD |
Payload schema
Common envelope on every event:
{
"event": "openemr-rel-cut",
"repo": "openemr/openemr",
"sha": "<40-char hex>",
"actor": "<dispatching identity>",
"dispatched_at": "<ISO8601 UTC>",
"data": { "...": "event-specific" }
}
Per-event data:
openemr-rel-cut / openemr-rel-update:
{ "branch": "rel-810", "version": "8.1.0", "prev_release": "8.0.0" }
openemr-tag:
{ "tag": "v8_1_0", "branch": "rel-810", "version": "8.1.0" }
The schema lives at tools/release/contracts/dispatch.schema.json in openemr/openemr-devops (since that repo already has the tools/release/ PHP foundation). Conductor and consumers each pull it in (git submodule, vendored copy, or composer package — TBD per repo).
Naming conventions
| Thing |
Pattern |
Example |
| Release branch |
rel-<MAJOR><MINOR>0 |
rel-810 |
| Release tag |
v<MAJOR>_<MINOR>_<PATCH> |
v8_1_0 |
| Hugo version param |
<MAJOR>.<MINOR>.<PATCH> |
8.1.0 |
| Slot names (devops) |
current / next / dev |
— |
| Auto-PR branches |
release-prep/<rel-branch> (conductor), release-rotation/auto (devops), release-docs/<version> (website) |
— |
Tag-object spec
Every release tag is annotated (Git object type tag, not commit).
- Tagger: the release-automation app/bot identity (TBD which app).
- Message: must contain the version (
8.1.0), the release date (YYYY-MM-DD UTC), and the merge-commit SHA. Suggested template:
OpenEMR <version> released <YYYY-MM-DD>
Conductor PR: <url>
Merge commit: <sha>
- Signing: open question. If we go signed, the bot identity needs a signing key; otherwise note in the tag message that automation produced it unsigned.
Tag-shape verifier
Lives in openemr/openemr-devops at tools/release/src/TagVerifier.php (alongside the existing VersionBumper). One copy, consumed by:
- conductor (openemr/openemr) — verify the tag it just created
- rotation workflow (openemr-devops) — verify the tag it's reacting to
- docs workflow (website-openemr) — verify the tag before flipping DRAFT → FINAL
Consumption mechanism: composer package published from tools/release/ (cleanest) or vendored copy (simpler to start). Pick one before any consumer wires it in.
Version regexes
branch: ^rel-(\d+)(\d)0$ # major, minor; patch is always 0 on the branch
tag: ^v(\d+)_(\d+)_(\d+)$ # major, minor, patch
Resolved contract decisions
Bot identity: A GitHub App (the "release app") is the bot. openemr-devops already has its credentials as repo secrets RELEASE_APP_ID and RELEASE_APP_PRIVATE_KEY. The same secrets must be added to openemr/openemr, openemr/website-openemr, and openemr/website-openemr-files, and the App installed on each.
Signing: Tags are unsigned. Tag message includes the notice Created by openemr-release-bot via automation. Revisit later if maintainers want signed tags.
Code-sharing for dispatch.schema.json and TagVerifier.php: Vendor both into each consumer. Canonical source lives in openemr-devops at tools/release/contracts/dispatch.schema.json and tools/release/src/TagVerifier.php. A drift-check script (tools/release/bin/check-vendored.php) runs in CI on each consumer to fail on divergence.
website-openemr-files sub-dispatch: Uses the same envelope as the conductor events. Event name openemr-docs-binaries. Per-event data:
{ "version": "8.1.0", "branch": "rel-810", "files": ["openemr-8.1.0-ehi.tar.gz", "openemr-8.1.0-b10.tar.gz"] }
Permissions self-check
Each repo's plan includes a release-permissions-check workflow (manual workflow_dispatch) that mints an App token from the secrets and probes only the operations that repo's release workflow performs. Running it after install is the maintainer's verification that the App is correctly scoped before any release runs. See each plan PR for the per-repo probe list.
Maintainer action items (blocking implementation)
Refines #662 and overlaps with #638.
Goal
Automate the per-release documentation work currently tracked in the wiki (QA and Release Process — Documentation and Steps for an official release) so that the release manager's manual surface shrinks to: edit a generated release-notes draft, sign off on the ONC certification page, and merge three pre-built PRs.
Approach: release-please-shaped, multi-repo
Borrow the release-please pattern (one long-lived PR per release branch, auto-updated on every push, merged to ship) but spread it across the three repos that own different slices of the release. The
openemr/openemrPR is the conductor; the other two are downstream and consume its events.Move docs off the wiki
For docs that are release artifacts (install/upgrade for v8.1.0, OpenAPI spec, EHI/B10 schemaspy output, acknowledgements, release notes, ONC cert page), publish to
website-openemr(Hugo, PR-reviewable, version-pinned) andwebsite-openemr-files(large binaries). Leave the wiki for living community references (generic howtos, FAQ).This eliminates the need for a MediaWiki bot account and makes every release-doc change diffable and reviewable. Redirects from old version-specific URLs become Hugo aliases (one line of frontmatter).
The three PRs
1.
openemr/openemr— release-prep PR (the conductor)Long-lived PR against
rel-810, auto-updated on every push. Contains the mechanical changes from the wiki's pre-tag checklist:version.php: strip-devfrom$v_taglibrary/globals.inc.php:allow_debug_languagedefault to 0docker/production/docker-compose.yml: pin image version (vslateston master)_rest_routes.inc.php: set version + regeneratedswagger/openemr-api.yaml(CLI exists:php bin/console openemr:create-api-documentation)docker-versionincrements (root,sites/default/,docker/.../root/)fsupgrade-N.shscaffold + Dockerfile edits in upgrade dirsql/X_Y_Z-to-X_Y_Z+1_upgrade.sqlskeleton on masteracknowledge_license_cert.htmlMerging this PR is the "we're shipping" decision. The workflow then creates an annotated tag on the merge commit (
git tag -aor the GitHub API with a tag object — never a lightweight ref). Lightweight tags lack author/date/message metadata and breakgit describe, release tooling, and downstream consumers that introspect tag objects.2.
website-openemr(+website-openemr-files) — docs PRSubscribes to pushes on
rel-*inopenemr/openemrviarepository_dispatch. Long-lived PR per release branch.Generated content:
website-openemr-filesunderfiles/openemr-<version>-ehi/(too large for Hugo repo)git shortlog v8_0_0..v8_1_0feat:/bug:/refactor:/chore:prefix (matches existingopenemr-dev:create-release-change-logCLI conventions)OpenEMR 8 API→OpenEMR 8.1.0 API, etc.)Each page renders with a release-status shortcode showing
DRAFT — based on rel-810 @ <sha>until the conductor's tag event flips it to FINAL. Workflow force-updates the PR branch on every regeneration so history doesn't bloat.Doesn't gate the tag — consumes it.
3.
openemr-devops— infra/test-matrix PRPer #638's
current/next/devrotation:rel-810cuts:nextbecomes 8.1,currentstays 8.0v8_1_0tags:currentbecomes 8.1, drop the priorTouches CI test matrices, package version refs, raspberrypi/docker pinned versions. Independent of the docs PR; runs in parallel. Should ideally merge before the tag so CI is ready against the new branch.
Workflow trigger DAG
Three workflows total, in three repos, all driven by pushes on
rel-*and the eventual tag. No new triggers, no cron, no human handoff between workflows.Irreducibly manual
Even with full automation, the release manager still:
rel-N).Everything else — artifact builds, install/upgrade page rewrites, redirect setup, version bumps, acknowledgement lists, package version pins — is mechanical and lives in the workflows.
What needs doing
In rough dependency order:
Related
Cross-repo contracts
The three plan PRs (openemr/openemr#11896, #665, openemr/website-openemr#82) all consume or emit the same events. Pinned here so the three implementation tracks don't invent slightly-different shapes.
Event names
Emitted by the conductor in openemr/openemr; consumed by openemr-devops and website-openemr (+ website-openemr-files).
openemr-rel-cutrel-*branchopenemr-rel-updaterel-*openemr-tagrel-*HEADPayload schema
Common envelope on every event:
{ "event": "openemr-rel-cut", "repo": "openemr/openemr", "sha": "<40-char hex>", "actor": "<dispatching identity>", "dispatched_at": "<ISO8601 UTC>", "data": { "...": "event-specific" } }Per-event
data:openemr-rel-cut/openemr-rel-update:{ "branch": "rel-810", "version": "8.1.0", "prev_release": "8.0.0" }openemr-tag:{ "tag": "v8_1_0", "branch": "rel-810", "version": "8.1.0" }The schema lives at
tools/release/contracts/dispatch.schema.jsonin openemr/openemr-devops (since that repo already has thetools/release/PHP foundation). Conductor and consumers each pull it in (git submodule, vendored copy, or composer package — TBD per repo).Naming conventions
rel-<MAJOR><MINOR>0rel-810v<MAJOR>_<MINOR>_<PATCH>v8_1_0<MAJOR>.<MINOR>.<PATCH>8.1.0current/next/devrelease-prep/<rel-branch>(conductor),release-rotation/auto(devops),release-docs/<version>(website)Tag-object spec
Every release tag is annotated (Git object type
tag, notcommit).8.1.0), the release date (YYYY-MM-DDUTC), and the merge-commit SHA. Suggested template:Tag-shape verifier
Lives in openemr/openemr-devops at
tools/release/src/TagVerifier.php(alongside the existingVersionBumper). One copy, consumed by:Consumption mechanism: composer package published from
tools/release/(cleanest) or vendored copy (simpler to start). Pick one before any consumer wires it in.Version regexes
Resolved contract decisions
Bot identity: A GitHub App (the "release app") is the bot.
openemr-devopsalready has its credentials as repo secretsRELEASE_APP_IDandRELEASE_APP_PRIVATE_KEY. The same secrets must be added to openemr/openemr, openemr/website-openemr, and openemr/website-openemr-files, and the App installed on each.Signing: Tags are unsigned. Tag message includes the notice
Created by openemr-release-bot via automation. Revisit later if maintainers want signed tags.Code-sharing for
dispatch.schema.jsonandTagVerifier.php: Vendor both into each consumer. Canonical source lives inopenemr-devopsattools/release/contracts/dispatch.schema.jsonandtools/release/src/TagVerifier.php. A drift-check script (tools/release/bin/check-vendored.php) runs in CI on each consumer to fail on divergence.website-openemr-files sub-dispatch: Uses the same envelope as the conductor events. Event name
openemr-docs-binaries. Per-eventdata:{ "version": "8.1.0", "branch": "rel-810", "files": ["openemr-8.1.0-ehi.tar.gz", "openemr-8.1.0-b10.tar.gz"] }Permissions self-check
Each repo's plan includes a
release-permissions-checkworkflow (manualworkflow_dispatch) that mints an App token from the secrets and probes only the operations that repo's release workflow performs. Running it after install is the maintainer's verification that the App is correctly scoped before any release runs. See each plan PR for the per-repo probe list.Maintainer action items (blocking implementation)
RELEASE_APP_IDandRELEASE_APP_PRIVATE_KEYsecrets to those three repos.contents:write,pull-requests:write, andactions:write(the last is needed by the dispatchers inopenemr/openemrandopenemr/website-openemr).