
The scenario
One developer knew how to release. The steps lived in their head and on their laptop. When they were away, nothing shipped.
Releases happened late at night “to be safe”, and roughly one in three needed a follow-up fix the next morning.
Symptoms
- “It works on my machine” and a different result in production.
- Environment settings copied by hand between servers.
- No clear way to go back to the previous version.
How it was found
Writing the release down step by step showed the real problem: about a dozen manual steps, three of them depending on files that only existed on that laptop. Staging didn’t match production either, because it had been built by hand at a different time.
What fixed it
- The app runs in a container, so the same image goes to every environment.
- Staging and production are defined in Bicep, so they’re created the same way.
- One pipeline for every change: build, test, deploy to staging, smoke test, then production after approval.
- Production deploys to a staging slot first and swaps only after a health check. The previous version stays ready to swap back.
name: deploy
on: { push: { branches: [main] } }
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t app:${{ github.sha }} . && docker run app:${{ github.sha }} npm test
production:
needs: test
environment: production # requires approval
runs-on: ubuntu-latest
steps:
- run: ./scripts/deploy-to-slot.sh && ./scripts/health-check.sh && ./scripts/swap.shResult
Releases become a merge to main. Anyone on the team can ship during the day, and a bad release is undone in one step instead of a night of fixes.
Lessons
- If a release needs a specific person or laptop, it isn’t a process yet.
- Build staging and production from the same code, or staging tests nothing.
- Design the rollback before the first automated deploy.
