Skip to content

Release process ​

Ovahol ships in batches: features land on dev, then a single release moves dev into main. Coolify watches main and deploys it, so merging to main is the deploy.

Branch model ​

BranchPurposeDeploys?
mainProduction. Only ever updated by a release merge.Yes — on every push
devIntegration. Everything lands here first.No
feat/*, fix/*, chore/*One issue each, cut from devNo
mermaid
gitGraph
    commit id: "main"
    branch dev
    checkout dev
    branch fix/123-thing
    commit id: "fix"
    checkout dev
    merge fix/123-thing
    branch feat/456-other
    commit id: "feat"
    checkout dev
    merge feat/456-other
    checkout main
    merge dev id: "release 1.9.0" tag: "v1.9.0"

Day-to-day ​

bash
git checkout dev && git pull
git checkout -b feat/123-short-name
# ...work...
git push -u origin feat/123-short-name
gh pr create --base dev --head feat/123-short-name

Merge the PR into dev, delete the branch, and cut the next branch from the updated dev. Several PRs can pile up on dev before a release.

CI runs on pull requests into main (and on pushes to main), not on dev PRs. Run the suites locally before merging into dev:

bash
make test-unit
make test-e2e-local
cd frontend && pnpm type-check && pnpm test && pnpm lint && pnpm lint:tokens
make lint

Cutting a release ​

  1. Decide the version — semver: patch for fixes, minor for features, major for breaking API or data changes.

  2. Bump the version in frontend/package.json ("version": "1.9.0").

  3. Write the changelog — move ## [Unreleased] entries under ## [1.9.0] - YYYY-MM-DD in CHANGELOG.md. Keep the Keep a Changelog headings (Added, Changed, Fixed, Removed, Security).

  4. Open the release PR dev → main with the version and changelog changes. CI runs the full suite here; fix anything it catches on dev.

  5. Merge the release PR.

  6. Tag the merge commit on main:

    bash
    git checkout main && git pull
    git tag -a v1.9.0 -m "v1.9.0"
    git push origin v1.9.0
    gh release create v1.9.0 --title "v1.9.0" --notes-file <release-notes-file>
  7. Watch the deploy. Coolify picks up main and rolls the API, worker and scheduler; the frontend and marketing site are rebuilt as separate services.

One release, one tag ​

Do not tag intermediate versions that never reached main. If three issues batch into one merge, they share one version and one tag. Tags exist to mark what production ran, not what dev contained.

Migrations on deploy ​

Shared migrations (backend/repository/migrations/) run automatically when the API container starts. Tenant migrations are embedded in the binary and applied lazily per facility schema on first access.

That means a merge to main can change data shape without an operator step — so a migration must be safe to run against a live database, and a rollback needs its .down.sql written before merge.

Some changes are deliberately not migrations because they rewrite live asset state and need human review:

Version strings ​

SurfaceSource
Backend binarygit describe --tags --always --dirty via Makefile ldflags (main.Version)
Frontend buildresolveAppVersion(): exact tag → short SHA → APP_VERSION → dev
PWA update promptthe build writes /version.json; the app compares it with the running build

Set APP_VERSION in the frontend build environment when the build runs outside a git checkout (for example a Coolify build without tags fetched) — otherwise the app identifies itself by commit SHA, or as dev, and the update prompt cannot name the version it is offering.

Publishing the documentation ​

The developer site is a static build of docs/:

bash
make docs-build   # -> docs/.vitepress/dist
make docs-dev     # local preview on http://localhost:5174

docs/Dockerfile builds that directory and serves it with nginx on port 80, including the try_files rule VitePress needs for cleanUrls (/guide/billing → guide/billing.html). Deploy it as its own service:

Coolify settingValue
Build packDockerfile
Base directory/docs
Exposed port80
Domaindocs.ovahol.com (CNAME to the Coolify host; TLS is automatic)
Health checkGET /health

A subdomain needs no base setting — assets are root-absolute and the site is served from the domain root. If the docs ever move to a subpath (ovahol.com/docs), set base: '/docs/' in docs/.vitepress/config.mts.

Unlike the app frontend, the docs have no build-time environment variables, so there is nothing to pass in Coolify.

The API reference page is regenerated by make openapi-generate, and CI fails when the committed page disagrees with api/openapi.yaml, so a release cannot ship a spec and a reference that drift apart.

After a release ​

  • Verify /health on the API.
  • Confirm the PWA update prompt names the new version.
  • Run any operator steps the release notes call for (status-drift --verify, facility-access report) against production.
  • Close the issues that were merged. Closing keywords in merge commits do not fire, so close them explicitly.

Ovahol developer documentation