Optivem Journal

Optivem Journal

Continuous Delivery

One Build, One Deploy Script, Many Environments

What we TESTED is what we SHIP

Valentina Jemuović's avatar
Valentina Jemuović
Jun 19, 2026
∙ Paid

🔒 Hello, this is Valentina with a premium issue of the Optivem Journal. I help Engineering Leaders & Senior Software Developers apply TDD in Legacy Code.


“Should we build once, or build per environment?”

“Should production have its own deploy workflow?”

These are the same question.

The first is about running a fresh docker build per environment — one build for QA, another for production, “because production needs different config baked in.”

The second is about writing a separate deploy workflow per environment — a deploy-qa.yml, a deploy-production.yml, each with its own steps and copy-pasted-then-edited logic, drifting apart commit by commit.

Both come from the same place: this environment is special, so it needs its own thing.

A pipeline’s entire job is to make “what we tested” and “what we shipped” the same. Every per-environment fork is a place where they quietly stop being the same.

Build once: no second build

The build is created in the Commit Stage. Every stage after that uses the same build — it gets tagged and deployed, but never rebuilt.

The temptation to rebuild happens because of configuration: “production needs the production API URL,” “QA needs the QA feature flags.”

So a second docker build happens with different build args, and now production is running a build that no test ever ran against. The green checkmark from QA is for a different build than the one customers actually get.

The build has no environment. Configuration is injected at deploy time — environment variables, mounted config, secrets from the environment — into the same build. That’s what allows the same build to move from Acceptance to QA to Production unchanged.


⚡Want to stop stressful releases, late-night fixes, and broken deployments?

Join the workshop →

Limited spots. Register now - 100 EUR off with code DISCOUNT_100


Deploy once: same script, different environment

Here’s the same deploy step running in three different environments.

Acceptance:

- name: Deploy
  uses: acme/actions/deploy@v1
  with:
    environment: acceptance
    version: ${{ inputs.version }}     # the system version, e.g. v2.5.0-rc.3
    image-urls: |
      ghcr.io/acme/shop/frontend
      ghcr.io/acme/shop/backend

This post is for paid subscribers

Already a paid subscriber? Sign in
© 2026 Valentina Jemuović, Optivem · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture