Real pipelines lint, test and deploy in separate jobs; needs is how you express what waits for what.
Jobs in a workflow start in parallel. To order them, a job lists the jobs it depends on in needs. GitHub builds a graph from these edges: a job starts when all of its needs have succeeded, and if one of them fails the dependent job is skipped.
deploy:
needs: [lint, test]Because each job runs on a fresh runner, anything a later job needs must be passed explicitly: build files go through artifacts, small values through job outputs, both covered later. Each job also re-checks out the code.
Useful job-level keys:
runs-on: different jobs can use different operating systems.timeout-minutes: the default is 360 minutes, far too long for most CI. Set something realistic.if:: skip the job conditionally, for example only deploy frommain.continue-on-error: true: let the workflow succeed even if this job fails (for experimental checks).container:andservices:: run the job inside a Docker image, or start side containers such as a database for tests (Linux runners only).
The Actions UI draws the graph, so a good structure doubles as documentation: fast checks first, slow or privileged jobs only after them.
Example
jobs:
lint:
runs-on: ubuntu-latest
steps: [{uses: actions/checkout@v7}, {run: make lint}]
test:
runs-on: ubuntu-latest
timeout-minutes: 15
services:
postgres:
image: postgres:17
env: {POSTGRES_PASSWORD: pw}
ports: ["5432:5432"]
steps: [{uses: actions/checkout@v7}, {run: make test}]
deploy:
needs: [lint, test] # waits for both; skipped if either fails
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps: [{run: echo deploying}]Remember
- Jobs run in parallel unless needs orders them
- A failed dependency skips the dependent job
- Set timeout-minutes; the default is 6 hours
Watch out for
- Writing files in one job and expecting the next job to find them on disk.