GitHub Actions Tutorial — Defining Jobs

2 minute read

Defining Jobs

Every workflow needs at least one job. A job is a set of steps that execute on the same runner. Each job gets a fresh virtual machine — no state is shared between jobs unless you explicitly pass data.


Job Anatomy


jobs:
  my-job-id:                         # unique identifier (letters, numbers, -, _)
    name: "My Friendly Job Name"     # display name in the UI
    runs-on: ubuntu-latest           # runner to use
    timeout-minutes: 30              # max runtime (default: 360)
    continue-on-error: false         # if true, workflow continues even if this job fails
    if: github.ref == 'refs/heads/main'  # conditional execution
    env:                             # job-level environment variables
      NODE_ENV: test
    steps:
      - run: echo "Hello from my job"


Job ID Rules

The job ID (my-job-id above) must:

  • Start with a letter or _
  • Contain only alphanumeric characters, -, or _
  • Be unique within the workflow
jobs:
  build:        # ✅ valid
  test_unit:    # ✅ valid
  deploy-prod:  # ✅ valid
  1-build:      # ❌ can't start with a number
  build job:    # ❌ no spaces allowed

Parallel vs Sequential

By default, all jobs run in parallel:

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Linting..."

  test:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Testing..."

  security:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Security scan..."
┌──────┐  ┌──────┐  ┌──────────┐
│ lint │  │ test │  │ security │    ← all start simultaneously
└──────┘  └──────┘  └──────────┘

To make them sequential, use needs: (covered in Job Dependencies).


Timeout

By default, a job can run for up to 6 hours (360 minutes). Set a shorter timeout to fail fast:

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - run: npm test

If the job exceeds 15 minutes, GitHub cancels it.


Continue on Error

Normally, if a job fails, the entire workflow is marked as failed. Use continue-on-error: true to let the workflow continue:


jobs:
  lint:
    runs-on: ubuntu-latest
    continue-on-error: true      # don't block other jobs if lint fails
    steps:
      - run: npm run lint

  test:
    runs-on: ubuntu-latest
    steps:
      - run: npm test

This is useful for non-critical checks (linting, code style) that shouldn’t block deployments.


Conditional Jobs

Use if: to conditionally run a job:


jobs:
  deploy:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    steps:
      - run: echo "Deploying to production..."

Common conditions:

Condition When
if: github.ref == 'refs/heads/main' Only on main branch
if: github.event_name == 'push' Only on push events
if: github.actor == 'octocat' Only when triggered by specific user
if: contains(github.event.head_commit.message, '[skip ci]') Skip CI based on commit message

Concurrency

Prevent multiple runs of the same job from executing simultaneously:


jobs:
  deploy:
    runs-on: ubuntu-latest
    concurrency:
      group: deploy-${{ github.ref }}
      cancel-in-progress: true
    steps:
      - run: echo "Deploying..."

If a new run starts while one is in progress:

  • cancel-in-progress: true → cancels the in-progress run
  • cancel-in-progress: false → queues the new run (waits)

Quick Reference

Property Purpose Default
runs-on: Runner selection Required
name: Display name Job ID
timeout-minutes: Max runtime 360 (6 hours)
continue-on-error: Don’t fail workflow false
if: Conditional execution Always runs
concurrency: Prevent parallel runs No limit
env: Job-level variables None

← Back to: Chapter 4: Jobs & Runners

Next: GitHub-Hosted Runners →

Updated: