One Command to Ship: A Practical CI Pipeline

If deploying means remembering an order of operations, it is not a process — it is folklore. The goal is one command that either works completely or changes nothing.

The four stages, in order

  1. Build — produce an artifact you can identify. A commit hash in the filename is enough.
  2. Verify — run the tests and the linter against that exact artifact, not against a developer's working copy.
  3. Migrate — apply database changes, and make every migration safe to run twice.
  4. Release — switch traffic, or upload, then confirm the new version is actually what is answering requests.

Make migrations boring

-- Safe on a live table: additive, nullable, no rewrite
ALTER TABLE articles ADD COLUMN reading_minutes SMALLINT UNSIGNED NOT NULL DEFAULT 0;

-- Rude awakening: this rebuilds the table and locks writes
ALTER TABLE articles ADD COLUMN slug VARCHAR(200) NOT NULL;

Add columns as nullable or with a default, backfill in batches, then tighten the constraint in a later release. Never combine “change the schema” and “change the code that reads it” in a single deploy if you can avoid it — during that window one of them is always wrong.

Advertisement

Migrations on a live table, in three releases

Backfilling is where a safe migration usually stops being safe. Adding a nullable column is instant on modern MySQL and PostgreSQL; a single UPDATE over a million rows is not. It holds locks, grows the undo or redo log, and can stall writes for minutes while replication falls behind.

-- batch it, and pause between batches so replicas can catch up
UPDATE articles SET reading_minutes = 0
 WHERE reading_minutes IS NULL LIMIT 5000;

Repeat until zero rows are affected, then tighten the column in a later deploy. Add, backfill, constrain: three releases instead of one, and the only version that survives a busy table. If a change genuinely needs a rewrite — a column type change, an index on a very large table — give it a release of its own with nothing else in it.

Write the pipeline once, as a script

The pipeline should be a file in the repository, runnable on your laptop and by CI with the same command. Bash is enough, and set -euo pipefail is the difference between a failure that stops the deploy and one that scrolls past.

#!/usr/bin/env bash
set -euo pipefail
SHA=$(git rev-parse --short HEAD)

build()   { tar --exclude=.git -czf "build/app-$SHA.tgz" .; }
verify()  { composer install --no-dev --no-interaction; php tests/run.php; }
migrate() { php app_private/cli/migrate.php --once; }

Two properties make this worth the effort. It is idempotent, so a half-finished deploy can simply be run again; and it is ordered, so release cannot run when verify failed. That ordering is the whole point of the exercise — the script is what removes the folklore.

Verify the release, do not assume it

# After upload: prove the new code is live
curl -fsS https://example.com/health | grep -q '"version":"'"$GIT_SHA"'"' \
  || { echo "Release verification failed"; exit 1; }

An endpoint that echoes the deployed commit turns “I think it deployed” into a fact, and it is the first thing you will want during an incident.

Smoke tests that fail the deploy, not the customer

A health response should answer two questions: is the process alive, and is it the version I think it is? Database reachable, cache warm, queue draining — all of that belongs in the same JSON body, with a status code your pipeline reads.

curl -fsS --max-time 5 https://example.com/health | grep -q '"db":"ok"' \
  || { echo "health check failed - rolling back"; ./deploy.sh rollback; exit 1; }

Then hit the artifact with three or four real requests: sign in with a test account, load the page that leans hardest on the database, fetch one API endpoint with a known payload. A deploy that succeeds and serves a broken page is worse than one that refuses to finish, because only the second is easy to notice.

Version by commit SHA, keep secrets out of the artifact

Stamp the build with its SHA and bake it into something the app can read at runtime — a version.json, or a constant generated during the build. That one string answers “which code is live?” in ten seconds and turns rollback into copying a directory instead of rebuilding one.

Secrets travel the other way. They never enter the artifact, the repository or the build log; read them from the environment or a secret manager at runtime, and rotate them on a schedule — the OWASP secrets management guidance covers storage and rotation properly. Add a startup check that fails loudly when a required key is missing: discovered by the deploy it is a five-minute fix, discovered by a user it is an incident.

A rollback plan is part of the pipeline

  • Keep the previous artifact available. If the build is identified by hash, rolling back is re-uploading one directory.
  • Write down which migrations are reversible, and which are not. Data-changing migrations usually are not — plan them as forward-only.
  • Decide who calls the rollback, and at what signal. A pipeline with no rollback rule becomes an argument in a chat window.

Roll out gradually, and drill the rollback

Switching all traffic at once gives you no way to learn gradually. A rolling or blue/green deploy lets you send 5% of traffic to the new version, watch error rate and latency for ten minutes, then continue or abandon. Feature flags go further: ship the code dark, enable it for one account, then for everyone, and switch it off without a deploy when it misbehaves.

Practise the rollback before you need it. Once a quarter, roll the previous artifact back on staging and time it; a rollback nobody has run is a hypothesis, not a plan. Agree the trigger numbers in advance — 5xx rate above 1%, p95 latency doubled, any failed checkout — and name the person allowed to call it.

In the first hour, watch four numbers: 5xx rate, p95 latency, queue depth and database connection count. Application logs only tell you what broke after a user found it; these four tell you first.

None of this requires a large platform. A shell script with four functions, run by the same person every time, beats a sophisticated pipeline nobody dares to modify.

Advertisement
khallaf

Writing about programming, AI and the tools that make engineering teams faster. Published by A1 Systems.

Last updated 19 Sep 2026

// Keep reading

Related articles

Tools & Tricks 5 min read

Regex you will actually use

The small set of regex constructs that cover everyday work, the patterns worth keeping in a snippet file, and how to avoid catastrophic backtracking.

khallaf Tip