GitHub Actions Tutorial — Job Dependencies

2 minute read

Job Dependencies

By default, all jobs run in parallel. The needs: keyword creates dependencies between jobs — making them run sequentially or in controlled patterns like fan-out and fan-in.


Basic Sequential Chain


jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Building..."

  test:
    runs-on: ubuntu-latest
    needs: build                # waits for build
    steps:
      - run: echo "Testing..."

  deploy:
    runs-on: ubuntu-latest
    needs: test                 # waits for test
    steps:
      - run: echo "Deploying..."

┌───────┐    ┌──────┐    ┌────────┐
│ build │ → │ test │ → │ deploy │
└───────┘    └──────┘    └────────┘

Fan-Out Pattern

Multiple jobs depend on a single job:


jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Building..."

  test-unit:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: echo "Unit tests..."

  test-integration:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: echo "Integration tests..."

  test-e2e:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: echo "E2E tests..."

                 ┌────────────┐
              ┌─►│ test-unit  │
┌───────┐    │  └────────────┘
│ build │ ───┤  ┌────────────────┐
└───────┘    ├─►│ test-integration│
              │  └────────────────┘
              │  ┌──────────┐
              └─►│ test-e2e │
                 └──────────┘

After build completes, all three test jobs start in parallel.


Fan-In Pattern

A single job waits for multiple jobs:


jobs:
  test-unit:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Unit tests..."

  test-integration:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Integration tests..."

  deploy:
    needs: [test-unit, test-integration]    # waits for BOTH
    runs-on: ubuntu-latest
    steps:
      - run: echo "All tests passed — deploying!"

┌────────────┐
│ test-unit  │──┐
└────────────┘  │    ┌────────┐
                 ├──►│ deploy │
┌────────────────┐│   └────────┘
│ test-integration├┘
└────────────────┘

Diamond Pattern (Fan-Out + Fan-In)


jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Building..."

  test-unit:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: echo "Unit tests..."

  test-integration:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: echo "Integration tests..."

  deploy:
    needs: [test-unit, test-integration]
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying..."

              ┌────────────┐
         ┌───►│ test-unit  │───┐
┌───────┐│    └────────────┘   │  ┌────────┐
│ build │─┤                     ├─►│ deploy │
└───────┘│    ┌────────────────┐│  └────────┘
         └───►│ test-integration├┘
              └────────────────┘

Conditional with needs

Run a job even if a dependency failed:


jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - run: exit 1       # this job fails

  cleanup:
    needs: test
    if: always()           # runs even if 'test' failed
    runs-on: ubuntu-latest
    steps:
      - run: echo "Cleaning up..."

  report:
    needs: test
    if: failure()          # runs ONLY if 'test' failed
    runs-on: ubuntu-latest
    steps:
      - run: echo "Test failed — sending notification"

Status Function Runs When
success() All dependencies succeeded (default)
failure() At least one dependency failed
always() Always, regardless of dependency status
cancelled() Workflow was cancelled

Accessing Dependency Results

You can check the result of a dependency in if: conditions:


jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - run: npm test

  deploy:
    needs: test
    if: needs.test.result == 'success'
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying..."


Common Mistakes

Mistake Fix
Circular dependency needs cannot be circular (A needs B, B needs A)
Job skipped unexpectedly Default if for needs is success() — add if: always() to run regardless
Typo in needs value The value must match a job ID exactly (case-sensitive)

Quick Reference

Pattern Syntax
Sequential needs: job-id
Wait for multiple needs: [job-a, job-b]
Always run if: always() with needs:
Run on failure if: failure() with needs:
Check result needs.<job-id>.result

← Previous: GitHub-Hosted Runners

Next: Job Outputs →

Updated: