Architecture decision ADR-010

Branch-to-Environment CI/CD: 3-Branch Gitflow with develop, staging, and main

Superseded · 2026-05-10

Historical decision text is preserved. Accepted describes the decision, not deployment of every capability discussed. Consult the Architecture and Infrastructure tabs for the verified running system.

Successors or amendments: ADR-011

Date: 2026-05-10
Status: Superseded by ADR-011 on 2026-05-20. The 3-branch Gitflow described below was never fully exercised in production; the project switched to single-branch trunk-based development before the staging environment was used at all. The content below is preserved as historical context.
Deciders: Daniel Meza


⚠️ Superseded. The decision below is no longer in force. Read ADR-011 for the current branching and CI/CD strategy.


Context and Problem Statement

Urza's Wallet needs a CI/CD (Continuous Integration/Continuous Deployment) strategy that:

  1. Prevents unreviewed code from reaching production.
  2. Provides a staging environment that mirrors production closely enough to catch integration issues.
  3. Enables PR (Pull Request)-level review against a real deployment (not just a local dev server).
  4. Keeps the deployment pipeline simple enough for a small team to maintain.
  5. Is managed reproducibly via Terraform (GitHub environments, branch protections, Railway deploy tokens).

The deployment infrastructure involves two platforms: GitHub (source of truth and CI runner) and Railway (deployment target). The strategy must define the mapping between git branches, GitHub Actions triggers, and Railway environments.


Decision Drivers


Considered Options

  1. Trunk-based development (single main branch) — All developers commit to main directly; feature flags gate incomplete work. Single deployment pipeline to production.
  2. Gitflow (main + develop + feature/ branches)* — feature/* → develop → main; PR environments for features; staging from develop; production from main.
  3. Environment branches (main + staging branches) — A staging branch mirrors main but with staging-specific configuration. Prone to divergence.
  4. PR-only deployments (no long-lived staging) — Every deployment is a PR preview environment; no dedicated staging.

Decision Outcome

Option 2: Gitflow (simplified — main + develop + feature/ branches)* is adopted.

Branch structure:

Branch Lifetime Purpose Deploys to
main Permanent Production-ready code Production (Railway)
staging Permanent Pre-production validation Staging (Railway)
develop Permanent Integration branch for merged features Develop (Railway)
feature/{ticket}-{description} Temporary Feature development PR preview (Railway)
fix/{ticket}-{description} Temporary Bug fixes PR preview (Railway)
hotfix/{description} Temporary Urgent production fixes PR to main directly

Promotion path: feature/* → develop → staging → main

CI/CD pipeline (.github/workflows/):

workflows/
├── ci.yml          ← Runs on every PR (lint, typecheck, tests, build check)
├── deploy.yml      ← Runs on merge to develop (→ staging) or main (→ production)
└── preview.yml     ← Runs on PR open/update (Railway preview deploy via Railway GitHub App)

ci.yml (every PR):

on: [pull_request]
jobs:
  lint:       npm run lint
  typecheck:  npm run typecheck
  test:       npm run test
  build:      npm run build (next build + tsc for api)

All four jobs must pass for the PR to be mergeable (enforced via branch protection).

deploy.yml (merge to develop, staging, or main):

on:
  push:
    branches: [develop, staging, main]
jobs:
  deploy-develop:
    if: github.ref == 'refs/heads/develop'
    environment: develop
    steps:
      - Install Railway CLI
      - railway up --service web --environment develop
      - railway up --service api --environment develop
      - railway up --service ingest --environment develop

  deploy-staging:
    if: github.ref == 'refs/heads/staging'
    environment: staging
    steps:
      - Install Railway CLI
      - railway up --service web --environment staging
      - railway up --service api --environment staging
      - railway up --service ingest --environment staging

  deploy-production:
    if: github.ref == 'refs/heads/main'
    environment: production  # GitHub environment with required_reviewers = 1
    steps:
      - Install Railway CLI
      - railway up --service web --environment production
      - railway up --service api --environment production
      - railway up --service ingest --environment production
      - Post-deploy: trigger sitemap regeneration via /api/revalidate

Railway preview environments:

Railway's GitHub App is configured to create preview environments automatically when a PR is opened. Each preview environment gets its own Railway project subdomain (https://web-pr-{number}.up.railway.app). Preview environments connect to a shared staging PostgreSQL with a per-PR database schema prefix (pr_{number}_) to isolate data. Preview environments are automatically destroyed when the PR is closed or merged.

GitHub branch protection rules (managed via Terraform):

Branch Required checks Required reviewers Direct push Force push
main ci/lint, ci/typecheck, ci/test, ci/build 1 Blocked Blocked
staging ci/lint, ci/typecheck, ci/test, ci/build 1 Blocked Blocked
develop ci/lint, ci/typecheck, ci/test, ci/build 1 Blocked Blocked

Terraform configuration (infra/terraform/github/environments.tf):

resource "github_repository_environment" "develop" {
  environment = "develop"
  repository  = var.github_repo
}

resource "github_repository_environment" "staging" {
  environment = "staging"
  repository  = var.github_repo
}

resource "github_repository_environment" "production" {
  environment  = "production"
  repository   = var.github_repo
  reviewers {
    users = [data.github_user.owner.id]
  }
  deployment_branch_policy {
    protected_branches     = true
    custom_branch_policies = false
  }
}

Database migration strategy:

Schema migrations run as part of the deployment pipeline using node-pg-migrate or drizzle-kit push:

  1. Migrations are applied to staging automatically on develop merge.
  2. Migrations are applied to production as the first step of the production deploy job, before the application services are updated.
  3. All migrations must be backward-compatible (no dropping columns or changing types without a deprecation period) to support Railway's rolling restart model.
  4. The previous Railway deployment is pinned for 30 minutes after a production deploy to enable fast rollback via the Railway dashboard if a migration causes issues.

Positive Consequences

Negative Consequences / Trade-offs


Pros and Cons of the Options

Option Review Safety Staging Fidelity Simplicity Preview Envs
Trunk-based + feature flags Low (no staging) N/A High Manual
Gitflow simplified (chosen) High High Medium Railway native
Environment branches Medium Variable Low Manual
PR-only deployments Medium High (per-PR) High Native

Trunk-based development was considered for its simplicity. For a team of 1–2, it is appropriate. The Gitflow model is documented here as the target pattern because the project is designed for a team of 3+ and the branch structure is managed in Terraform (so switching back to trunk-based requires only a Terraform and branch protection rule change, not application changes).


References