The critical detail in any real deployment pipeline: it's the same build artifact promoted through environments, not rebuilt fresh for each one.
jobs:
build:
steps:
- run: npm run build
- uses: actions/upload-artifact@v4
with: { name: app-build, path: dist/ }
deploy-staging:
needs: build
steps:
- uses: actions/download-artifact@v4
with: { name: app-build, path: dist/ }
- run: ./deploy.sh staging
deploy-production:
needs: deploy-staging
steps:
- uses: actions/download-artifact@v4
with: { name: app-build, path: dist/ }
- run: ./deploy.sh production
If staging and production each ran npm run build independently, they could pick up a dependency update published between the two builds, run against a slightly different compiler version on the runner, or otherwise produce a build that's similar but not identical — which quietly defeats the entire point of testing on staging first. You're no longer testing what you're about to ship; you're testing something that merely resembles it. Downloading the exact same artifact built once and verified once is what makes "it passed staging" an actual guarantee about what reaches production.