GitHub Actions Job Dependencies and Outputs
Jobs in one workflow start at the same time by default. Real pipelines need order: build before test, test before deploy. Dependencies set that order, and outputs pass data from one job to the next.
Parallel Jobs by Default
Each job runs on its own fresh machine. Two jobs without any link start together and finish independently.
jobs:
lint:
runs-on: ubuntu-latest
steps:
- run: echo "Linting"
test:
runs-on: ubuntu-latest
steps:
- run: echo "Testing"
Start
+--- lint ---+
| | both run together
+--- test ---+The needs Keyword
The needs keyword makes a job wait for another job to succeed. Write the name of the job that must finish first.
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo "Building"
test:
needs: build
runs-on: ubuntu-latest
steps:
- run: echo "Testing"
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- run: echo "Deploying"
build ---> test ---> deployWhen build fails, GitHub skips test and deploy automatically. A broken build never reaches production.
Waiting for Several Jobs
A list under needs makes a job wait for all listed jobs. This pattern is called fan-out and fan-in.
deploy:
needs: [ unit-tests, ui-tests, security-scan ]
+--- unit-tests ----+
build ----> +--- ui-tests ------+----> deploy
+--- security-scan -+
fan-out fan-inWhy Outputs Exist
Separate machines share nothing. A file or variable created in one job does not exist in the next. Outputs carry small pieces of text, such as a version number, across that gap. Use artifacts for files.
Writing an Output
A step writes a name and value to a special file called GITHUB_OUTPUT. The job then exposes that value under its own outputs block.
jobs:
build:
runs-on: ubuntu-latest
outputs:
app_version: ${{ steps.ver.outputs.version }}
steps:
- name: Set version
id: ver
run: echo "version=1.4.2" >> "$GITHUB_OUTPUT"The step needs an id so the job can find it. The job output refers to the step through steps.ver.outputs.version.
Reading an Output
A later job reads the value through the needs context. The job must list the producer under needs.
announce:
needs: build
runs-on: ubuntu-latest
steps:
- run: echo "Shipping version ${{ needs.build.outputs.app_version }}"Data Flow Diagram
Job: build Job: announce
+--------------------------+ +---------------------------+
| step id=ver | | reads |
| writes version=1.4.2 | | needs.build.outputs. |
| | | | app_version |
| v | | ^ |
| job outputs.app_version -+---------->-+------------+ |
+--------------------------+ +---------------------------+Running a Job After a Failure
A job with needs normally skips when its parent fails. A cleanup or notification job often must run anyway. Add a status check function to the if line.
notify:
needs: [ build, test ]
if: ${{ always() }}
runs-on: ubuntu-latest
steps:
- run: echo "Pipeline finished with status ${{ needs.test.result }}"Each needed job exposes a result value: success, failure, cancelled, or skipped.
Good Habits
- Keep outputs small. Store long data in artifacts.
- Name outputs after the data they hold, such as
image_tag. - Avoid secrets in outputs, since GitHub may block or mask them.
- Draw the job graph on paper before writing the file. The diagram maps directly to
needslines.
The Actions tab shows a visual graph of every run. Boxes connected by lines match your needs settings, which makes problems in the order easy to spot.
