GitHub Actions Tutorial — Defining Jobs
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 runcancel-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 →