Skip to content

Job Dependencies


Parallel jobs are the default for a reason, but not every relationship between jobs fits that shape. Sometimes one job’s result should decide whether another job even bothers to run. That’s what needs is for, and it’s the last building block this section covers before the mental model you’ve built holds together on its own.

The needs Keyword

Adding needs to a job tells GitHub not to start that job until the job(s) it names have finished successfully:

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      # ...

  test:
    needs: lint
    runs-on: ubuntu-latest
    steps:
      # ...

With this in place, test no longer starts alongside lint — it waits for lint to finish, and only starts if lint succeeded. If lint fails, test is skipped entirely, not run and failed — skipped, shown with a distinct grey icon in the run graph rather than a red one.

Sequencing Your Workflow

Apply this to hello.yml: make style checking a gate before the test suite bothers running at all — there’s little point running tests against code that doesn’t even pass a basic lint check.

name: CI

on:
  push:

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v4
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - name: Install dependencies
        run: pip install flake8
      - name: Run flake8
        run: flake8 app.py test_app.py

  test:
    needs: lint
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v4
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - name: Install dependencies
        run: pip install pytest
      - name: Run tests
        run: pytest
      - name: Write summary
        run: |
          echo "### Test Run Complete" >> "$GITHUB_STEP_SUMMARY"
          echo "- Python version: 3.12" >> "$GITHUB_STEP_SUMMARY"
          echo "- Status: all tests passed" >> "$GITHUB_STEP_SUMMARY"

Push it, then watch the run graph: lint starts immediately, and test visibly waits, showing as queued until lint finishes.

git add .github/workflows/hello.yml
git commit -m "Make test depend on lint passing"
git push
    flowchart LR
    A[push event] --> B[lint job]
    B -->|success| C[test job]
    B -->|failure| D[test job: skipped]
  

Try breaking it to see the skip behavior firsthand: introduce a deliberate style violation in app.py (an unused import, or a line far over flake8’s default length limit works well), push, and watch lint fail while test shows as skipped rather than run. Then fix it and push again to confirm the chain runs cleanly end to end.

Depending On More Than One Job

needs also accepts a list, for a job that shouldn’t start until several others have finished:

  deploy:
    needs: [lint, test]
    runs-on: ubuntu-latest
    steps:
      # ...

Tip

It is abosolutely valid to use inline list syntax needs: [lint, test] but using lists by listing adheres readability:

  deploy:
     needs:
       - lint
       - test

A deploy job written this way waits for both lint and test to succeed before starting — useful once a workflow has more than one independent check that all need to pass before something further downstream (like a deployment) should even be attempted. Nothing in your practice workflow needs a third job right now, but it’s worth knowing this scales past two without changing shape.

Warning

By default, a downstream job is skipped the moment any of its needs jobs fails — not just the one that failed. If you ever want a job to run regardless of whether its dependencies succeeded (a cleanup step, for instance, that should run even after a failure), that requires an if: condition overriding the default behavior. That’s beyond what this section covers, but it’s worth knowing the default isn’t the only option once you need it.

What You’ve Actually Built

Step back and look at what hello.yml has become since it started as a single echo statement: a real two-job pipeline, checking out code, installing dependencies, running a linter, gating a test suite behind it, and writing a readable summary — using nothing but the vocabulary this section introduced one piece at a time. That’s the mental model this section set out to build, and it’s the same shape you’ll recognize underneath every more elaborate workflow from here on, however many more steps, jobs, and triggers get layered on top of it.

Last updated on