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
| Branch | Purpose | Deploys? |
|---|---|---|
main | Production. Only ever updated by a release merge. | Yes — on every push |
dev | Integration. Everything lands here first. | No |
feat/*, fix/*, chore/* | One issue each, cut from dev | No |
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
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-nameMerge 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:
make test-unit
make test-e2e-local
cd frontend && pnpm type-check && pnpm test && pnpm lint && pnpm lint:tokens
make lintCutting a release
Decide the version — semver:
patchfor fixes,minorfor features,majorfor breaking API or data changes.Bump the version in
frontend/package.json("version": "1.9.0").Write the changelog — move
## [Unreleased]entries under## [1.9.0] - YYYY-MM-DDinCHANGELOG.md. Keep the Keep a Changelog headings (Added,Changed,Fixed,Removed,Security).Open the release PR
dev → mainwith the version and changelog changes. CI runs the full suite here; fix anything it catches ondev.Merge the release PR.
Tag the merge commit on
main:bashgit 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>Watch the deploy. Coolify picks up
mainand 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:
status-drift(see status drift repair)facility-access(see facility access lock)
Version strings
| Surface | Source |
|---|---|
| Backend binary | git describe --tags --always --dirty via Makefile ldflags (main.Version) |
| Frontend build | resolveAppVersion(): exact tag → short SHA → APP_VERSION → dev |
| PWA update prompt | the 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/:
make docs-build # -> docs/.vitepress/dist
make docs-dev # local preview on http://localhost:5174docs/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 setting | Value |
|---|---|
| Build pack | Dockerfile |
| Base directory | /docs |
| Exposed port | 80 |
| Domain | docs.ovahol.com (CNAME to the Coolify host; TLS is automatic) |
| Health check | GET /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
/healthon the API. - Confirm the PWA update prompt names the new version.
- Run any operator steps the release notes call for (
status-drift --verify,facility-accessreport) against production. - Close the issues that were merged. Closing keywords in merge commits do not fire, so close them explicitly.