Branching & Deployment Strategy — mpac
Current State
| What | Status |
|---|---|
| Branch model | main + ad-hoc feature branches |
| CI | PR to main, path-based filtering (pgw, sdks, infra, obs) |
| CD staging | Auto-deploy on push to main (pgw only) |
| CD production | Manual workflow_dispatch, 2 approvers |
| Environments | dev (manual), staging (auto), production (manual) |
| Release tracking | Single tag v2026.3.1, no formal process |
Strategy: Trunk-Based Development with Staged Promotion
Single trunk (main), short-lived feature branches, and a dev → staging → production promotion flow where each environment is gated:
- dev: auto-deploy on merge — continuous integration feedback
- staging: manual promote — QA gets a stable snapshot
- production: tag and deploy — auditable, traceable releases
1. Branch Model
main (trunk)
│
├── feature/PSP-123-add-psp-domain ← short-lived, from main
├── feature/PSP-124-sftp-fetcher ← short-lived, from main
├── fix/PGW-99-hmac-edge-case ← short-lived, from main
│
└── (tags: v2026.3.1, v2026.4.0, ...) ← cut from main for productionBranch Types
| Branch | Naming | Lifetime | Merges to |
|---|---|---|---|
main | — | permanent | — |
| Feature | feature/<TICKET>-<slug> | days (< 1 week ideal) | → main via PR |
| Fix | fix/<TICKET>-<slug> | hours–days | → main via PR |
| Hotfix | hotfix/<TICKET>-<slug> | hours | → main via PR, then tag |
Rules
mainis always deployable — CI must pass before merge- No long-lived feature branches — use feature flags if a feature takes > 1 week
- PRs require review — 1 reviewer minimum
- No direct pushes to
main— enforce via GitHub branch protection - No release branches — tags cut directly from main (simpler than maintaining release branches)
2. Environment Mapping
┌──────────────┐ ┌──────────────┐ ┌───────────────┐
│ dev │─────►│ staging │─────►│ production │
│ │ │ │ │ │
│ Auto on main │ │ Manual trigger │ │ Manual trigger │
│ Every merge │ │ Specific SHA │ │ Release tag │
│ Fast feedback │ │ QA validates │ │ 2 approvers │
└──────────────┘ └──────────────┘ └───────────────┘| Environment | Trigger | What gets deployed | Approval | Purpose |
|---|---|---|---|---|
| dev | Auto on push to main | main HEAD | None (CI passed) | Continuous integration, catch infra/migration issues early |
| staging | Manual workflow_dispatch | Specific commit SHA from main | None | QA validation on a stable, frozen snapshot |
| production | Manual workflow_dispatch | Release tag v* | 2 approvers | Live traffic |
Why this order
| Principle | How it's achieved |
|---|---|
| Every merge is tested in a real environment | Dev auto-deploys |
| QA tests a stable snapshot, not a moving target | Staging is manually triggered with a specific SHA |
| Production runs exactly what was validated | Tag points to the same SHA that passed staging |
| Audit trail | CalVer tags + GitHub environment approvals |
What changes from today
| Current | Proposed | Why |
|---|---|---|
| Staging auto-deploys on push to main | Dev auto-deploys on push to main | Frees staging for intentional QA |
| Dev is manual and rarely used | Dev is automatic and always current | Every merge gets real-environment feedback |
| Production uses git SHA | Production uses release tags | Auditable, rollback-friendly |
| No formal release process | Tag from main when staging passes QA | Traceable releases for payment compliance |
3. Promotion Flow
Day-to-day: Feature Development
# Developer works on feature
git checkout -b feature/PSP-123-add-psp-domain
# ... code, test locally ...
git push origin feature/PSP-123-add-psp-domain
# Open PR → CI runs → 1 reviewer approves → squash merge to main
# → dev auto-deploys ← happens automaticallyPromoting to Staging
When enough features have landed on main and been verified on dev:
# 1. Identify the commit SHA validated on dev
git log --oneline main # e.g., abc1234
# 2. Trigger staging deploy via GitHub Actions workflow_dispatch
# Input: ref = abc1234Staging now holds a frozen snapshot. QA tests against it. New merges to main continue deploying to dev without disturbing staging.
Cutting a Release for Production
When QA signs off on staging:
# 1. Tag the exact SHA that passed staging
git tag v2026.4.0 abc1234
git push origin v2026.4.0
# 2. Trigger production deploy via workflow_dispatch (input: tag v2026.4.0)
# 3. 2 approvers approve in GitHub environmentHotfix on Production
# 1. Fix on main via normal PR flow
git checkout -b hotfix/PGW-100-fix-settlement-parse
# ... fix ...
# PR → CI → merge to main → auto-deploys to dev
# 2. Verify fix on dev
# 3. Deploy the fix commit to staging, verify
# workflow_dispatch: ref = <fix-commit-sha>
# 4. Tag and deploy to production
git tag v2026.4.1 <fix-commit-sha>
git push origin v2026.4.1
# workflow_dispatch: tag = v2026.4.1, 2 approversNo cherry-picks, no release branches. Hotfix follows the same path as any change: main → dev → staging → production.
Versioning Scheme
v<YEAR>.<MONTH>.<PATCH>
v2026.3.0 ← March 2026 initial release
v2026.3.1 ← patch/hotfix
v2026.4.0 ← April releaseCalVer (calendar versioning) fits because:
- Payment systems are compliance-sensitive — auditors want to know when code shipped
- You already started with
v2026.3.1 - Simpler than SemVer for a platform (not a library)
4. CI/CD Pipeline Details
On Pull Request (any branch → main)
PR opened/updated
│
├─ [pgw changed?] → Go tests (80% coverage), lint, race detection
├─ [sdk-js changed?] → pnpm test, type-check, lint
├─ [sdk-go changed?] → Go tests (80% coverage)
├─ [sdk-py changed?] → pytest
├─ [infra changed?] → cfn-lint, CloudFormation validate
└─ [obs changed?] → Config validation (Prometheus, Loki, Tempo, Alloy)No changes needed — path-based filtering already works well.
On Merge to main → Dev (auto)
Push to main
│
├─ Build Docker image → push to ECR (mpac-pgw-dev)
├─ Run migrations → RDS (dev)
├─ Deploy to ECS (mpac-pgw-dev)
├─ Wait for stability
└─ Post deploy status to commitWorkflow: cd-dev.yml, trigger: push: branches: [main]
Manual Promote to Staging
workflow_dispatch (input: ref/SHA)
│
├─ Build Docker image from <ref> → push to ECR (mpac-pgw-staging)
├─ Run migrations → RDS (staging)
├─ Deploy to ECS (mpac-pgw-staging)
├─ Wait for stability
├─ Run smoke tests against staging
└─ Notify mpac-infra via repository dispatchWorkflow: cd-staging.yml, trigger: workflow_dispatch with ref input
On Release Tag → Production
workflow_dispatch (input: tag)
│
├─ [2 approvers approve in GitHub environment]
├─ Verify image exists in ECR (reuse staging-built image if same SHA)
├─ Tag image: production-YYYYMMDDHHMMSS
├─ Run migrations → RDS (production)
├─ Deploy to ECS (mpac-pgw-production)
├─ Wait for stability
├─ Run smoke tests against production
└─ Notify + post deployment summaryWorkflow: cd-production.yml, trigger: workflow_dispatch with tag input
5. Commit SHA Traceability
The same SHA flows through all three environments:
main merge (SHA abc1234)
│
▼
dev deploys abc1234 ← auto
│
▼
staging deploys abc1234 ← manual, input: ref=abc1234
│
▼
tag v2026.4.0 → abc1234 ← git tag
production deploys v2026.4.0 ← manual, 2 approversGuarantee: production runs exactly the code that was tested on dev and validated on staging. No merge, no cherry-pick, no drift.
6. Submodule Coordination
Dependency Order
mpac-infra (infrastructure first)
↓
mpac-pgw (application)
↓
mpac-obs (observability — can lag behind)Coordination Rules
| Scenario | Process |
|---|---|
| PGW code change only | Normal PR → main → dev → staging → production |
| Infra change required for PGW | Deploy infra first, then PGW. Use repository-dispatch (already wired) |
| New migration + infra change | Infra first (new RDS params, etc.) → PGW migration → PGW deploy |
| Obs config update | Independent — deploy anytime |
Submodule Ref Updates
When tagging a production release:
# In mpac root, update submodule refs to the release tag
cd mpac-pgw && git checkout v2026.4.0
cd mpac-infra && git checkout v2026.4.0
cd ..
git add mpac-pgw mpac-infra
git commit -m "release: bump submodules to v2026.4.0"
git tag v2026.4.0This gives you a single commit in the parent repo that records the exact combination deployed to production.
7. Branch Protection Rules (GitHub)
main
Require pull request (1 reviewer minimum)
Require status checks to pass (CI jobs)
Require branch to be up to date before merging
No direct push
No force push
Require linear history (squash merge)Tags v*
Restrict tag creation to repository admins / release managers8. Rollback Strategy
| Environment | Rollback Method | Time |
|---|---|---|
| dev | Revert commit on main → auto-redeploy | Minutes |
| staging | Re-trigger workflow with previous SHA | Minutes |
| production | Re-trigger workflow with previous tag (e.g., v2026.3.1) | Minutes, needs 2 approvers |
ECR keeps all tagged images. Rolling back = deploying a previous tag/SHA. No rebuild needed.
Database Rollback
Migrations are forward-only (psql -f). For rollback:
- Phase 1 migrations (additive — new columns, tables) don't need rollback
- Phase 2 migrations (destructive — drop columns) should only run 24h+ after Phase 1
- If a migration breaks: fix forward with a new migration, don't manually undo
9. Action Items
Immediate (low effort, high value)
| # | Action | Where |
|---|---|---|
| 1 | Enable branch protection on main | GitHub repo settings (each submodule) |
| 2 | Enforce squash merges | GitHub repo settings |
| 3 | Create cd-dev.yml — auto-deploy main to dev | mpac-pgw workflows |
| 4 | Change cd-staging.yml — manual trigger with SHA input | mpac-pgw workflows |
| 5 | Change cd-production.yml — accept release tags | mpac-pgw workflows |
Near-term (before next release)
| # | Action | Where |
|---|---|---|
| 6 | Add post-deploy smoke tests to staging and production CD | workflows |
| 7 | Tag first formal release v2026.4.0 from main | git |
| 8 | Document the release checklist in RELEASING.md | repo root |
Later (as team grows)
| # | Action | Where |
|---|---|---|
| 9 | Image promotion: reuse staging-built image for production (skip rebuild) | cd-production.yml |
| 10 | Deployment notifications to Slack/Teams | workflows |
| 11 | Canary/blue-green deployment for production | ECS config + infra |
10. Visual: Full Flow
Developer GitHub AWS
───────── ────── ───
git checkout -b feature/x
... code ...
git push
┌─ PR created ──────┐
│ CI runs │
│ Review (1 person)│
└─ Merge to main ───┘
│
▼ (auto)
┌─ Dev CD ──────────┐
│ Build image │──► ECR (dev)
│ Run migrations │──► RDS (dev)
│ Deploy ECS │──► ECS (dev)
└───────────────────┘
│
Verified on dev
│
▼ (manual: workflow_dispatch, ref=SHA)
┌─ Staging CD ──────┐
│ Build image │──► ECR (staging)
│ Run migrations │──► RDS (staging)
│ Deploy ECS │──► ECS (staging)
│ Smoke tests │
└───────────────────┘
│
QA validates
│
┌─ Tag release ─────┐
│ git tag v2026.4.0│
└───────────────────┘
│
▼ (manual: workflow_dispatch, tag=v2026.4.0)
┌─ Production CD ───┐
│ 2 approvers │
│ Promote image │──► ECR (production)
│ Run migrations │──► RDS (production)
│ Deploy ECS │──► ECS (production)
│ Smoke tests │
└───────────────────┘