Development Guide
Current branch structure. We are still going to be using a main branch to hold both development and production code would be separated by using tags.
Feature Branch ──► PR to main ──► CI (detects what changed)
│
┌─────────┴─────────┐
▼ ▼
Backend changed? Frontend changed?
├─ Python lint ├─ ESLint
├─ pytest
├─ Docker build ├─ Vitest (TBI)
└─ Schema lint └─ Vite build
│ │
└─────────┬─────────┘
▼
Merge to main
│ │
▼ ▼
Auto-deploy Docs deploy
to DEV (GitHub Pages)
(selective)
│
▼
Ready for production?
│
▼
Actions → "Prepare Release"
(manual trigger only)
│
▼
release/vX PR to main
(CalVer bump + CHANGELOG.md)
│
▼
Review & merge PR
│
▼
"Promote to PROD" auto-runs
(merge is the trigger)
│
▼
Pre-flight: release merged?
Heroku apps set up?
│
▼
Approval gate
│
┌─────────┼─────────┐
▼ ▼ ▼
Backend Backend Frontend
Realtime Multiproc
│ │ │
└─────────┼─────────┘
▼
Tag + GitHub Release
(from merged changelog)
Day-to-day workflow
-
Create a branch following the convention
/ (e.g. GSCHED-955/modify-steps-in-atom-sequence-parsing). hotfix is also available for fast issues that might not need tracking in JIRA. -
Make changes in backend/, frontend/, or both
-
Add fragment: ./scripts/changelog-fragment "Add steps in ProgramProvider" feature . The scripts looks for this convention
<fragment comment> <type>.\ Types:feature fix breaking deprecation improvement doc misc -
Commit with the template:
<type>[(<scope>)]: <description>\ Types:feat|fix|chore|docs|refactor|test|ci|style|perf|build|revert\ Scopes:backend|frontend|. Can be empty if is outside this two.\ Example: feat(backend): Add step count in ProgramProvider -
Push → CI runs relevant checks
-
Open PR to main → changelog auto-populates in PR description
-
Review, approve, merge
-
Dev auto-deploys (only changed components)
Working with gpp-client development versions
gpp-client ships two parallel tracks on PyPI: stable releases (used by the GPP PRODUCTION environment) and .devN pre-releases (used by the GPP DEVELOPMENT environment). They are managed via two conflicting dependency groups in backend/pyproject.toml:
gpp-prod = ["gpp-client>=X.Y.Z"]— latest stable, used by CI and the Docker build.gpp-dev = ["gpp-client>=X.Y.Z.dev0,<X.Y.Z.a0"]— restricts the resolver to.devNpre-releases of the current dev cycleX.Y.Z.
You must pass --group explicitly; running plain uv sync from the workspace root does not pick these groups up:
# stable (matches CI/Docker)
uv sync --group gpp-prod --no-group gpp-dev
# switch local venv to the latest .devN of the current cycle
uv sync --group gpp-dev --no-group gpp-prod
# pull a newer .devN later
uv sync --group gpp-dev --no-group gpp-prod --upgrade-package gpp-client
When the gpp-client team rolls a new dev cycle (e.g. X.Y.Z finalizes and .devN starts being published for the next version X.Y.(Z+1) or X.(Y+1).0), bump both numbers in the gpp-dev specifier together — >=X.Y.Z.dev0,<X.Y.Za0. If they diverge the range is empty and uv sync will fail.
Which version is used by each deployment
The backend/Dockerfile accepts a GPP_GROUP build-arg that picks the track at image-build time:
| Deployment | Workflow | GPP_GROUP |
gpp-client track |
|---|---|---|---|
| Auto-deploy → DEV | .github/workflows/deploy-dev.yml | gpp-dev |
latest .devN of the current cycle (talks to GPP DEVELOPMENT) |
| Promote → PROD | .github/workflows/promote-prod.yml | gpp-prod |
latest stable (talks to GPP PRODUCTION) |
The arg is passed via heroku container:push --arg GPP_GROUP=…. If you build the image locally without the arg, it defaults to gpp-prod.
Promoting to Production
Releasing is a two-step flow: first a reviewable release PR, then the actual promotion. The workflows never push to main directly (it is a protected branch).
Step 1 — Prepare the release
-
Verify dev is working as expected
-
Go to Actions → "Prepare Release" → Run workflow
-
The workflow computes the next CalVer version, bumps
version.py, buildsCHANGELOG.mdwith towncrier (consuming thechangelog.d/fragments), and opens arelease/vX.Y.ZPR againstmain -
Review the PR — it shows exactly what will be released — then approve and merge it. Merging is the deploy trigger; close the PR instead if you are not ready to ship
Step 2 — Promotion (automatic)
Merging the release/* PR automatically triggers "Promote to PROD" (it can also be run manually from the Actions tab as a fallback, e.g. to retry a failed run):
-
Pre-flight checks run first: the
CHANGELOG.mdentry for the current version must exist (and the tag must not), and the Heroku production apps are verified (apps reachable, backend apps on thecontainerstack,DATABASE_URLpresent) -
All components deploy to prod. If required reviewers are configured on the
prod-backend/prod-frontendenvironments, the deploy jobs pause for approval — remove them in Settings → Environments for a fully automatic promotion -
Tag
vX.Y.Zis created and the GitHub Release is published (Releases tab) with the changelog section from the merged release PR
Versioning: CalVer
Format: YYYY.MM.PATCH (e.g., 2026.04.1, 2026.04.2, 2026.05.1)
The "Prepare Release" workflow auto-calculates the next version from version.py. Within the same month, patch increments. New month resets to 1. You can override with a specific version via the version_override input.
Tags are the source of truth for what's in production.