Problem
Production deploys of https://www.open-emr.org/ are currently fully manual: a maintainer clones this repo locally, runs hugo to produce public/, and rsyncs that directory to a remote server. The repo's existing CI workflow (.github/workflows/deploy.yml) only deploys to GitHub Pages staging at https://openemr.github.io/website-openemr/ — it has no path to production. Content merged to master reaches the live site only when a specific maintainer runs the deploy script from their machine, which is a single-point-of-human dependency with no SLA.
This ticket is the first concrete step toward retiring the openemr/website-devops repo entirely. Today that repo holds the manual operational scripts that exist only because automation hasn't been built yet; replacing each of them with first-party automation (in website-openemr and the related repos) lets the website-devops repo be archived. This ticket addresses the main-website deploy specifically.
Background
The current production deploy is the script web_openemr_update in openemr/website-devops: https://github.com/openemr/website-devops/blob/master/web_openemr_update
Reduced to its essential shape:
rsync --recursive --delete --exclude .git \
-e "ssh -i <maintainer-ssh-key>" \
<maintainer-local-checkout>/public/ \
root@<origin-host>:/var/www/html/open-emr.org/
So today's deploy target:
- Origin server: a DigitalOcean droplet (IP intentionally not duplicated here; see the linked script for the actual address), document root
/var/www/html/open-emr.org/
- Fronted by Cloudflare (
server: cloudflare on responses; cf-cache-status: HIT)
- SSH access via a key currently held only by the maintainer running the script
The Hugo build configuration (config.yaml) sets baseURL: https://www.open-emr.org/, so a default build with the existing config produces production-correct URLs without needing per-environment overrides.
Trigger model
Production deploys should be tag-based, not push-based. The two-tier model:
- Staging (Pages): every push to
master (the existing deploy.yml).
- Production: every published GitHub release / git tag.
Release tags will be created via release-please-action, which watches conventional-commit messages on master and maintains a release PR. Merging the release PR creates the tag and GitHub release, which triggers the production deploy workflow. This keeps prod intentional (a release PR review is the gate) while giving fast iteration on staging.
Setting up release-please is a separate piece of work and a prerequisite for this ticket. Track it independently or include it as the first step here.
Mechanism (open)
Two design directions worth comparing:
Direction A — automate the existing rsync model. Provision an SSH key dedicated to GitHub Actions (separate from any maintainer's personal key), add it as a repo or org secret along with the droplet's known_hosts entry, and write a workflow that builds and rsyncs to the droplet on release: published. The droplet stays as the prod origin.
Direction B — migrate production to GitHub Pages. Two sub-variants:
- B1 — custom domain on the same Pages site. Configure this repo's Pages site to serve
www.open-emr.org directly. Conflicts with using Pages for staging, so staging would have to move (e.g., to a sibling website-openemr-staging repo or a previewable PR-based deploy).
- B2 — separate Pages target + reverse proxy. Publish the prod build to a different Pages target (e.g., a sibling repo's Pages, or a different subpath). Configure Cloudflare (already in front of
www.open-emr.org) to proxy to that Pages target. Staging keeps the existing Pages site. This avoids the "one Pages site per repo" constraint without requiring the rsync/droplet path.
Direction B (especially B2) aligns with the broader goal of retiring website-devops. Direction A is the lowest-change path. Pick after a brief discussion with the maintainers about whether the droplet should stick around long-term.
Files
.github/workflows/deploy.yml — current Pages staging workflow; build steps can be reused or extracted.
config.yaml — defines baseURL (already production-correct).
themes/openemr/package.json — theme dependency installed via npm ci during build.
- (External)
openemr/website-devops/web_openemr_update — the canonical existing deploy procedure.
Constraints
- Today's deploy depends on a single maintainer's machine and credentials. Removing that dependency is the underlying goal; preserving manual approval (e.g.,
environment: production with required reviewers) is a valid design choice on top of the tag trigger.
- The droplet's
/var/www/html/ may host more than just open-emr.org — verify the layout before any automated deploy uses --delete.
- A clean Hugo build at the time of writing emits three pre-existing
WARN lines (allowlisted in .github/workflows/hugo-build.yml); a production deploy job should reuse the same allowlist or call the existing build job.
Definition of done
Out of scope
- Changes to the staging (GitHub Pages) deployment beyond what's needed to coexist with prod (the staging baseURL fix is tracked separately).
- Restructuring the Hugo content or theme.
- Per-PR review apps or additional environments.
- Replacing the other scripts in
openemr/website-devops (files_openemr_update, wiki_openemr_update, goBackup.sh, MediaWiki upgrade runbooks). Each of those is a separate piece of work; full retirement of the repo is tracked in openemr/website-devops#2.
Problem
Production deploys of https://www.open-emr.org/ are currently fully manual: a maintainer clones this repo locally, runs
hugoto producepublic/, and rsyncs that directory to a remote server. The repo's existing CI workflow (.github/workflows/deploy.yml) only deploys to GitHub Pages staging at https://openemr.github.io/website-openemr/ — it has no path to production. Content merged tomasterreaches the live site only when a specific maintainer runs the deploy script from their machine, which is a single-point-of-human dependency with no SLA.This ticket is the first concrete step toward retiring the
openemr/website-devopsrepo entirely. Today that repo holds the manual operational scripts that exist only because automation hasn't been built yet; replacing each of them with first-party automation (inwebsite-openemrand the related repos) lets the website-devops repo be archived. This ticket addresses the main-website deploy specifically.Background
The current production deploy is the script
web_openemr_updateinopenemr/website-devops: https://github.com/openemr/website-devops/blob/master/web_openemr_updateReduced to its essential shape:
So today's deploy target:
/var/www/html/open-emr.org/server: cloudflareon responses;cf-cache-status: HIT)The Hugo build configuration (
config.yaml) setsbaseURL: https://www.open-emr.org/, so a default build with the existing config produces production-correct URLs without needing per-environment overrides.Trigger model
Production deploys should be tag-based, not push-based. The two-tier model:
master(the existingdeploy.yml).Release tags will be created via release-please-action, which watches conventional-commit messages on
masterand maintains a release PR. Merging the release PR creates the tag and GitHub release, which triggers the production deploy workflow. This keeps prod intentional (a release PR review is the gate) while giving fast iteration on staging.Setting up release-please is a separate piece of work and a prerequisite for this ticket. Track it independently or include it as the first step here.
Mechanism (open)
Two design directions worth comparing:
Direction A — automate the existing rsync model. Provision an SSH key dedicated to GitHub Actions (separate from any maintainer's personal key), add it as a repo or org secret along with the droplet's known_hosts entry, and write a workflow that builds and rsyncs to the droplet on
release: published. The droplet stays as the prod origin.Direction B — migrate production to GitHub Pages. Two sub-variants:
www.open-emr.orgdirectly. Conflicts with using Pages for staging, so staging would have to move (e.g., to a siblingwebsite-openemr-stagingrepo or a previewable PR-based deploy).www.open-emr.org) to proxy to that Pages target. Staging keeps the existing Pages site. This avoids the "one Pages site per repo" constraint without requiring the rsync/droplet path.Direction B (especially B2) aligns with the broader goal of retiring
website-devops. Direction A is the lowest-change path. Pick after a brief discussion with the maintainers about whether the droplet should stick around long-term.Files
.github/workflows/deploy.yml— current Pages staging workflow; build steps can be reused or extracted.config.yaml— definesbaseURL(already production-correct).themes/openemr/package.json— theme dependency installed vianpm ciduring build.openemr/website-devops/web_openemr_update— the canonical existing deploy procedure.Constraints
environment: productionwith required reviewers) is a valid design choice on top of the tag trigger./var/www/html/may host more than justopen-emr.org— verify the layout before any automated deploy uses--delete.WARNlines (allowlisted in.github/workflows/hugo-build.yml); a production deploy job should reuse the same allowlist or call the existing build job.Definition of done
master, opening release PRs from conventional commits.web_openemr_updateinopenemr/website-devopsis marked deprecated and points to the new mechanism.https://www.open-emr.org/(e.g., a low-risk content change merged, release PR merged, deploy observed live).Out of scope
openemr/website-devops(files_openemr_update,wiki_openemr_update,goBackup.sh, MediaWiki upgrade runbooks). Each of those is a separate piece of work; full retirement of the repo is tracked inopenemr/website-devops#2.