Skip to content

ci: automate production deploy to open-emr.org #92

Description

@kojiromike

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

  • release-please-action is configured on master, opening release PRs from conventional commits.
  • A documented production deploy mechanism that does not require the maintainer's local machine, triggered by published releases.
  • web_openemr_update in openemr/website-devops is marked deprecated and points to the new mechanism.
  • A successful production deploy is verified end-to-end on https://www.open-emr.org/ (e.g., a low-risk content change merged, release PR merged, deploy observed live).

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Fields

    No fields configured for Task.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions