GitHub Actions Jobs and Steps

Jobs and steps are the core execution units of any GitHub Actions workflow. Jobs define what work gets done and where. Steps define the individual commands that make up each job. Mastering how jobs and steps work together gives you full control over how your automation runs.

Jobs — The Work Containers

A job is a named block inside the jobs section of your workflow. Each job runs on its own runner machine, completely isolated from other jobs.

jobs:
  build:          ← Job 1
    runs-on: ubuntu-latest
    steps: ...

  test:           ← Job 2
    runs-on: ubuntu-latest
    steps: ...

The names build and test are identifiers you choose. GitHub uses them to display results in the Actions tab and to allow other jobs to reference them.

Parallel vs Sequential Jobs

By default, all jobs in a workflow run at the same time — in parallel. This is the fastest approach when jobs do not depend on each other.

Workflow starts
       │
       ├──── build job starts (runs on its own runner)
       │
       └──── lint job starts  (runs on its own runner)

When one job must finish before another begins, use the needs keyword to create a dependency:

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

  test:
    needs: build        ← Wait for build to finish first
    runs-on: ubuntu-latest
    steps:
      - run: echo "Testing..."

  deploy:
    needs: test         ← Wait for test to finish first
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying..."

This creates a pipeline where each job waits for the previous one to succeed:

build → test → deploy

Waiting for Multiple Jobs

A job can wait for several jobs to complete before it starts:

deploy:
  needs: [build, lint, security-scan]

The deploy job starts only after all three jobs in the list succeed.

Job Outputs

Jobs can pass data to other jobs using outputs. This is useful when one job produces a value (like a version number) that another job needs.

jobs:
  generate-version:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.get-version.outputs.version }}
    steps:
      - name: Get version
        id: get-version
        run: echo "version=1.4.2" >> $GITHUB_OUTPUT

  deploy:
    needs: generate-version
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying version ${{ needs.generate-version.outputs.version }}"

Steps — The Individual Instructions

Steps live inside a job and execute one after another from top to bottom. If a step fails, the job stops and marks all remaining steps as skipped (unless you configure otherwise).

The run Step

The run key executes shell commands directly on the runner:

steps:
  - name: Install packages
    run: npm install

  - name: Run multiple commands
    run: |
      echo "Starting build"
      npm run build
      echo "Build complete"

The pipe symbol | lets you write multi-line commands. Each line runs in sequence.

The uses Step

The uses key calls a pre-built action from the GitHub Marketplace or a local file:

steps:
  - name: Download repository code
    uses: actions/checkout@v4

  - name: Set up Python
    uses: actions/setup-python@v5
    with:
      python-version: '3.12'

The with key passes input parameters to the action. Think of it like filling in a form that the action reads.

Step IDs

Each step can have an optional id field. This lets other steps reference the output of this step:

steps:
  - name: Run tests and capture output
    id: run-tests
    run: echo "result=pass" >> $GITHUB_OUTPUT

  - name: Show result
    run: echo "Test result was ${{ steps.run-tests.outputs.result }}"

Continuing After a Failure

Use continue-on-error: true on a step to allow the workflow to keep running even if that step fails:

steps:
  - name: Optional lint check
    run: npm run lint
    continue-on-error: true     ← Failure here does not stop the job

  - name: Run tests
    run: npm test               ← This still runs even if lint failed

Working Directory

By default, all steps run in the root directory of the repository. You can change the working directory for a specific step:

steps:
  - run: npm install
    working-directory: ./frontend

Or set a default working directory for all steps in a job:

jobs:
  build:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./frontend
    steps:
      - run: npm install
      - run: npm test

Job and Step Summary

Feature               | Job               | Step
----------------------|-------------------|---------------------------
Runs on               | Its own runner    | Same runner as its job
Execution order       | Parallel by default | Always sequential
Can depend on others  | Yes (needs)       | No (always top to bottom)
Can pass data         | Yes (outputs)     | Yes (GITHUB_OUTPUT)
Can fail independently| Yes               | Yes (continue-on-error)

Leave a Comment

Your email address will not be published. Required fields are marked *