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
- testA 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.