Reuse, release, and deploy

Build a release from one artifact

Promote the exact tested artifact instead of rebuilding independently in the deployment job.

Tests exercise one build. Production should ship those same bytes, not a fresh build after approval with a different dependency tree.

Build once, promote many is the pattern. The artifact is the contract between CI and deploy. If staging and production each rebuild, you lose that contract.

I have seen teams pass review on commit abc123 while production silently built from abc123 plus whatever npm resolved that afternoon. The diff was one patch version of a transitive dependency. No human tested that combination.

Upload after the authoritative build

jobs:
  package:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - run: shasum -a 256 dist/index.html
      - uses: actions/upload-artifact@v4
        with:
          name: site-${{ github.sha }}
          path: dist/

Record the digest in the log. That hash is your receipt.

Download the same artifact to deploy

  deploy-staging:
    needs: package
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: site-${{ github.sha }}
          path: dist/
      - run: ./scripts/deploy.sh dist/ staging

  deploy-production:
    needs: deploy-staging
    environment: production
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: site-${{ github.sha }}
          path: dist/
      - run: ./scripts/deploy.sh dist/ production

Staging and production deploy the tarball that passed tests. No second npm run build hidden after approval.

Compare hashes across environments

After a release, compare artifact hashes in build logs, staging deploy records, and production deploy records. They should match.

A realistic bug: production runs npm install again because someone thought it was faster. A patch release of a dependency slips in untested.

Tag releases should upload the same artifact GitHub attaches to the release, not rebuild inside the release job. Container images follow the same idea: build and scan once, promote the digest.

If you must rebuild for a platform-specific binary, document why and keep a separate artifact per platform. Do not pretend one generic rebuild equals the tested one.

Try this on your own project: remove any npm run build step from deploy jobs and rely on the artifact alone.

Lesson completed