Posts
CI/CD in practice — how test and deploy automation lightens day-to-day work
Why a push-to-production pipeline still pays off when “manual upload” takes minutes — and which automation pieces actually shorten the cycle versus just making noise.
On small and mid-size projects, CI/CD often sounds like a corporate luxury. In practice the real cost is not wiring a pipeline — it is repeatable manual work and regressions that only show up for the client. Below is how I think about automating tests and deploys in general, without tying the story to one product domain or host.
What hurts without a pipeline
A familiar rhythm without CI/CD:
- Merge or push to the main branch.
- Manual front build / dependency install on the server.
- File upload (FTP, hosting panel, ad‑hoc SSH).
- A quick browser check — no repeatable scenario set.
- When something breaks, rollback is also manual and stressful.
A few minutes per deploy does not sound bad. The pain is frequency: several iterations a day × forgotten migrations, asset caches, or a critical-path test. The cost is not the upload — it is production debugging and losing trust in releases.
What a sensible CI/CD should do
For me a pipeline has three jobs, not one:
1. Verification before merge / before deploy
Automated unit and feature/integration tests on a clean CI environment. Goal: the same verdict whether or not “it worked on my Docker”. Early catches usually include form validation, pricing rules, permissions, schema migrations, and API regressions.
2. Repeatable deploy
One path: build → artifact/sync → migrations → cutover. No checklist in a Notes app. Fewer decisions mid-deploy means fewer “I forgot .env / the front build / the cache”.
3. Fast feedback for the author
CI should fail on the PR, not after a client ticket. A green check is a contract: “this change did not break known paths.”
What automation actually improves
- Less mental load. You do not keep deploy step order in your head — the pipeline enforces it.
- Shorter fix → production loop. After setup, a push to the right branch runs tests and (on green) deploys.
- Safer refactors. When business rules live in tests, you can restructure code without silent breakage fear.
- Shared environment baseline. Language version, extensions, and database in the CI job — fewer “works locally, fails on server” surprises.
- An audit trail. Job logs remain: what ran, which migration, which commit. That is the first line of investigation after an outage.
What not to force into the hard gate
Not every check should block production. Long soak/endurance runs, scans that depend on the live internet, or flaky external APIs are better beside the hard gate — as a signal, not as a veto on every merge.
Likewise: a deploy when tests were intentionally skipped (missing secrets, conditional job) must not pretend everything was verified. Make the rule explicit: skipped ≠ green.
A minimal shape that usually works
For a typical web app (e.g. PHP/Laravel + front build):
- On PR: install dependencies, optional lint/format, tests with a database service in CI.
- On the main branch: the same plus deploy over SSH / to a platform (rsync, artifact, registry — hosting-dependent).
- Secrets only in CI, never in the repo.
- Migrations as part of deploy, not “I will run them by hand later”.
- Clear rollback: previous release / previous commit can be restored without archaeology.
Tooling details (GitHub Actions, GitLab CI, Forge, a self-hosted runner) are interchangeable. The contract matters: push → proof → ship.
Common pitfalls
- Tests that need Vite / front assets in an environment with no build — disable that path in tests or build in the job.
- Database engine differences (e.g. default expressions on MySQL 8) — a migration that passes locally fails in CI.
- HTTPS behind a reverse proxy. When nginx / Cloudflare / Caddy terminates TLS in front of the app rather than the app itself, without trusting proxy headers (
X-Forwarded-Proto, etc.) generated asset URLs and redirects can still come out as plainhttp://. It often shows up on the first deploy behind a CDN — broken CSS/JS or a redirect loop. - Too wide a first pipeline. Start with tests + deploy from
main/master. Only later: dependency caching in CI, running tests across several PHP versions at once, preview environments per PR.
The outcome I aim for
Not “perfect DevOps”. I want a state where:
- a daily change does not require a manual deploy ritual,
- a critical-path regression fails in CI before a user sees it,
- releases are boring — and boring releases are a good sign.
CI/CD does not replace product thinking. It clears repetitive, error-prone work off the plate so time goes to what actually needs design: data model, UX, and business rules.