CI/CD & DevOps65 min total · 17 parts
CI/CD Pipelines Explained: From Push to Production
Part 10 of 17 · ~2 min
Build Artifacts and Why You Promote, Not Rebuild
Before this pipeline existed, staging and production images were two separate docker build runs, sometimes hours apart, and the team finds out exactly why that's dangerous the hard way. A staging build kicks off at 2:10 in the afternoon; the production build for the same commit — same source, same package-lock.json, nothing in the diff — kicks off at 4:45, after a slow manual review. In those two and a half hours, node:20-alpine's upstream tag moves, picking up a routine security patch to a system library that one of worker's compiled dependencies — the same PDF-page renderer from the Docker chapters — happens to link against. Staging, built against the older base layer, passes every check. Production, built two and a half hours later against the patched one, throws an error on exactly the kind of dense scanned statement that exercises that dependency's compiled path — the same shape of failure as the earlier glibc/musl incident, but this time the two builds weren't even using different base images. They were using the same tag, at two different moments, which silently resolved to two different sets of bytes.
The fix the pipeline above already has baked in, once you look at it correctly: build-image runs exactly once per commit, tags the result with that commit's SHA, and pushes it once. deploy-staging and a new deploy-production job both pull that identical tag — nothing rebuilt, nothing re-resolved, nothing that could have moved in the gap between the two deploys:
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
url: https://app.snapledger.example
steps:
- run: ./deploy.sh production ghcr.io/snapledger/web:${{ github.sha }}
"Passed staging" is only a meaningful sentence about production if production is running the literal artifact that staging tested — not something that merely started from the same source and happened to get built again. The moment two builds of "the same commit" can diverge, staging stops proving anything about what's actually about to ship, and you're back to testing a close cousin of your release rather than the release itself.
That environment: name: production line is also doing real work, and it's worth pausing on, because it's the piece that turns "a human clicks a button" from a policy written down somewhere into something the pipeline actually enforces. Configured once in the repository's settings with a required reviewer, a job targeting that environment simply stops and waits — sitting there, visibly, in GitHub's UI — until a named person clicks Approve. No branch protection rule, no separate ticketing system, no honor code: the job is mechanically incapable of proceeding without that click. That's the concrete shape of the line between Continuous Delivery and Continuous Deployment from the very first chapter — not a philosophy, one YAML block and one setting in a repo's admin panel.