Jobs are isolated VMs. When job A builds something that job B needs to deploy, you need a bridge. This lesson teaches the two mechanisms: outputs for small data and artifacts for files.

The Problem: Jobs Don't Share State

Job: build Creates: dist/app.js Calculates: version=1.2.3 VM destroyed after job ☠️ 🚫 No shared FS Job: deploy Needs: dist/app.js Needs: version number How to get them? 🤔
The build job creates files and values that the deploy job needs — but they're on different machines.

Solution 1: Job Outputs (Small Values)

For passing strings between jobs — version numbers, image tags, URLs, flags.

Step (inside build job) echo "tag=sha-abc12" >> $GITHUB_OUTPUT Job outputs: { image-tag: "sha-abc12" } needs.build ${{ needs.build.outputs.image-tag }}
Step sets output → Job exposes it → Downstream job reads it via needs.<job>.outputs

Complete Pattern

jobs:
  build:
    runs-on: ubuntu-latest
    # 1. Declare which outputs this job exposes
    outputs:
      version: ${{ steps.ver.outputs.value }}
      image-tag: ${{ steps.tag.outputs.value }}
    steps:
      - uses: actions/checkout@v4
      
      # 2. Set outputs from steps (must have id:)
      - name: Get version
        id: ver
        run: echo "value=$(node -p 'require(\"./package.json\").version')" >> $GITHUB_OUTPUT
      
      - name: Set image tag
        id: tag
        run: echo "value=sha-$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      # 3. Read outputs from the upstream job
      - run: |
          echo "Version: ${{ needs.build.outputs.version }}"
          echo "Image: ${{ needs.build.outputs.image-tag }}"

Solution 2: Artifacts (Files)

For passing files between jobs — build output, test reports, binaries, Docker contexts.

Job: build npm run build → dist/ 📤 upload-artifact (dist/) ☁️ GitHub Storage Job: deploy 📥 download-artifact → dist/ Deploy dist/ to server
Upload stores files in GitHub's cloud. Download retrieves them in any subsequent job.
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      
      - name: Upload build
        uses: actions/upload-artifact@v4
        with:
          name: app-build              # artifact name
          path: dist/                  # what to upload
          retention-days: 5            # auto-delete after 5 days
          if-no-files-found: error    # fail if dist/ is empty

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Download build
        uses: actions/download-artifact@v4
        with:
          name: app-build
          path: dist/                  # where to put it
      
      - run: ls -la dist/  # Your files are here!

Outputs vs Artifacts: When to Use Which

Job OutputsArtifacts
Data typeStrings (small values)Files & directories
Size~1 MB total500 MB (free tier)
Examplesversion, image tag, URL, booleandist/, test reports, binaries
DownloadableNo (only in workflow)Yes (from Actions UI)
LifetimeCurrent run onlyConfigurable (1-90 days)
SpeedInstantUpload/download time
Rule of thumb: If it's a string → output. If it's a file → artifact. If in doubt, ask: "Can I fit this in a single echo statement?" Yes = output. No = artifact.

🏋️ Exercise: Build Once, Deploy Twice

Create a workflow that builds once and deploys the same artifact to staging and production:

# .github/workflows/build-deploy.yml
name: Build & Deploy
on: workflow_dispatch

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.info.outputs.version }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci && npm run build
      - id: info
        run: echo "version=$(node -p 'require(\"./package.json\").version')" >> $GITHUB_OUTPUT
      - uses: actions/upload-artifact@v4
        with: { name: app, path: dist/ }

  deploy-staging:
    needs: build
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/download-artifact@v4
        with: { name: app, path: dist/ }
      - run: echo "🚀 Deploying v${{ needs.build.outputs.version }} to staging"
      - run: ls dist/

  deploy-production:
    needs: [build, deploy-staging]
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/download-artifact@v4
        with: { name: app, path: dist/ }
      - run: echo "🚀 Deploying v${{ needs.build.outputs.version }} to PRODUCTION"

Key principle: Build ONCE → deploy the SAME artifact to every environment. Never rebuild for production — that can introduce inconsistency.

🧠 Recall Check

  1. What file do you write to in order to set a step output?
  2. What's the syntax for reading job A's output "tag" from job B?
  3. Why should you build once and deploy the same artifact, rather than rebuilding per environment?
  4. Can you download an artifact from the GitHub Actions UI?
Reveal answers
  1. $GITHUB_OUTPUT — e.g., echo "key=value" >> $GITHUB_OUTPUT
  2. ${{ needs.A.outputs.tag }}
  3. Consistency. Rebuilding might pick up different dependency versions, different timestamps, or different env vars. The same binary that passed tests in CI should be exactly what goes to production.
  4. Yes. Artifacts appear at the bottom of the workflow run page as downloadable zip files.
You now have all the building blocks of a CI pipeline: triggers, jobs, steps, caching, secrets, environments, outputs, and artifacts. You can build production-quality CI workflows. Next: Matrix Builds — testing across multiple versions and platforms simultaneously.