DEV Community

Cover image for From "Merge is Deploy" to Release Engineering with GitHub Actions
Victor Lis Bronzo
Victor Lis Bronzo

Posted on

From "Merge is Deploy" to Release Engineering with GitHub Actions

Have you ever stopped to think about the risk of having a pipeline where any merge into the main branch deploys straight to production without a single safety gate?

For a long time, our workflow here was that classic setup almost every developer has used at some point: merge on main triggering an SSH script with git pull and pm2 restart

It worked for day-to-day tasks, but it gave a false sense of stability lol

The reality check hit when I found a critical blind spot in the automation: remote SSH scripts were running without strict error handling. In other words, if a git pull caused a conflict or a database migration failed halfway through, the script simply ignored the failure, ran to the end, and GitHub Actions marked the pipeline as green

The absolute worst-case scenario for monitoring: the pipeline reported that everything went smoothly, while production was already completely down

On top of that, the execution order was inverted: database migrations were running before the application build. If TypeScript threw a type error right after, the database schema had already advanced while the new code never booted. And since Prisma has no native down migrations, rolling back meant a high-risk manual intervention

I decided to stop everything and redesign our delivery pipeline from scratch, starting from one clear premise: a tag is a release, a merge is not

Today, nothing touches the production server without an annotated SemVer tag, going through 6 tightly coupled stages:

  1. Strict tag validation: only accepts annotated tags matching vX.Y.Z, ensuring author, timestamp, and audit trail for every single release
  2. Quality gates across PR and Release: automated tests with Vitest, strict typechecking, builds, and migration validation against a clean database via workflow_call
  3. Decoupled backups: an independent daily scheduled routine combined with a mandatory safety snapshot right before touching production
  4. Real migration dry-run: the most valuable gate, where the pipeline restores the latest production dump into an ephemeral scratch database directly inside the VPS, times the migration run, and checks for schema drift before touching the live database
  5. Deterministic deploy: direct checkout on the immutable tag ref, building before running migrations, and concurrency locks to eliminate simultaneous deploy races
  6. Smoke tests: active HTTP health checks and PM2 process scans to verify no service is looping in an error state

The operational impact has been huge: we gained real predictability on migrations, complete traceability for every version, and the confidence that a green pipeline actually means a healthy system in production

For anyone also managing Node and TypeScript applications on VPS or dedicated servers who wants to eliminate blind deploys, I packaged this entire architecture into an open and modular GitHub Actions template

I made the repository public for anyone who wants to use it as a foundation: https://github.com/victor-lis-bronzo/tag-release-template

How do you usually handle deployment gates and database migrations in your stack?

Top comments (0)