A pipeline that works in a demo is not the same as a pipeline that works at 2 AM when a critical hotfix needs to ship. We have seen teams invest weeks building elaborate CI/CD configurations that collapse the moment someone pushes a database migration alongside a feature branch. The fix is not more tooling — it is better design.
The three-gate model
Every pipeline we build follows a three-gate model: lint and type-check (fast feedback), test (comprehensive validation), and deploy (environment-specific). Each gate is independently retriable. If tests pass but deploy fails due to a transient cloud error, the team reruns only the deploy stage instead of waiting 12 minutes for the full pipeline to repeat.
# .github/workflows/deploy.yml
jobs:
gate-1-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pnpm lint && pnpm typecheck
gate-2-test:
needs: gate-1-lint
runs-on: ubuntu-latest
steps:
- run: pnpm test:ci
gate-3-deploy:
needs: gate-2-test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- run: pnpm deploy:productionDatabase migrations as first-class citizens
Migrations run in a dedicated step before the application deploy. They are tested against a shadow database in CI, and every migration file includes a corresponding rollback. We have seen too many teams treat migrations as an afterthought — that approach works until it does not, usually on a Friday evening.
- Run migrations before application deploy, not during startup
- Test every migration against a shadow database
- Include rollback scripts for every forward migration
- Version-lock your migration tool alongside your ORM