GitHub Actions — Workflows, Jobs & Steps
BeginnerGitHub Actions workflows consist of jobs that run on runners. Jobs contain steps — shell commands or reusable actions. Understanding job dependencies, matrix builds, and artifact passing is essential for effective pipelines.
Overview
A GitHub Actions workflow is a YAML file defining one or more jobs. Jobs run in parallel by default but can be chained with needs. Each job runs on a fresh runner VM (ubuntu-latest, windows-latest, macos-latest) or a self-hosted runner. Steps within a job share the same filesystem and run sequentially. The three most important concepts for real pipelines: contexts and expressions (accessing dynamic values like branch name, commit SHA), matrix builds (test across multiple versions in parallel), and artifacts (passing build outputs between jobs).
Workflow Structure Deep Dive
Understanding the full workflow structure — contexts, expressions, and environment variables — lets you write dynamic, reusable pipelines.
# .github/workflows/build.yml
name: Build and Test
on:
push:
branches: [main]
paths: # only trigger if these paths change
- 'src/**'
- 'package*.json'
- '.github/workflows/**'
workflow_dispatch: # manual trigger
inputs:
environment:
description: 'Target environment'
required: true
default: 'staging'
type: choice
options: [staging, production]
env: # workflow-level env vars (all jobs)
NODE_VERSION: '20'
REGISTRY: ghcr.io
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 15 # fail job if it takes > 15 min
# Permissions for this job's GITHUB_TOKEN
permissions:
contents: read
packages: write # needed to push to GitHub Container Registry
# Outputs: pass values to downstream jobs
outputs:
image-tag: ${{ steps.meta.outputs.tags }}
git-sha: ${{ github.sha }}
steps:
- uses: actions/checkout@v4
# Contexts: github, env, secrets, inputs, steps, runner
- name: Print context info
run: |
echo "Branch: ${{ github.ref_name }}"
echo "SHA: ${{ github.sha }}"
echo "Actor: ${{ github.actor }}"
echo "Event: ${{ github.event_name }}"
echo "Repo: ${{ github.repository }}"
# Conditional steps
- name: Deploy to production
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: echo "Deploying to production..."Matrix Builds & Job Dependencies
Matrix strategy runs a job multiple times with different configurations — perfect for testing across Node versions, OS, or environment combinations. needs creates job dependency chains.
jobs:
# Matrix build: runs 6 parallel jobs (3 node versions x 2 OS)
test:
strategy:
matrix:
node: [18, 20, 21]
os: [ubuntu-latest, windows-latest]
exclude:
- node: 21
os: windows-latest # exclude this combination
fail-fast: false # don't cancel other matrix jobs on first failure
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci && npm test
# Job dependency: 'deploy' only runs if 'test' succeeds
build:
needs: test # waits for all matrix test jobs to pass
runs-on: ubuntu-latest
steps:
- run: echo "All tests passed, building image..."
deploy-staging:
needs: build
runs-on: ubuntu-latest
environment: staging # requires environment approval in GitHub settings
steps:
- run: echo "Deploy to staging..."
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production # separate approval gate for production
if: github.ref == 'refs/heads/main'
steps:
- run: echo "Deploy to production..."
# Pipeline graph:
# test (x6 matrix) -> build -> deploy-staging -> deploy-productionArtifacts & Caching
Artifacts pass files between jobs (test reports, build outputs). Caching saves and restores directories between runs (node_modules, Maven cache, pip packages) — dramatically speeds up pipelines.
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Cache node_modules between runs
- name: Cache dependencies
uses: actions/cache@v4
id: cache-deps
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node- # fallback: any node cache for this OS
- name: Install (if cache miss)
if: steps.cache-deps.outputs.cache-hit != 'true'
run: npm ci
- name: Build
run: npm run build
# Upload artifact (pass to another job or download later)
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: dist-${{ github.sha }}
path: dist/
retention-days: 5
deploy:
needs: build
runs-on: ubuntu-latest
steps:
# Download artifact from build job
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: dist-${{ github.sha }}
path: dist/
- run: ls -la dist/ # verify artifact was downloaded
- run: ./deploy.sh dist/ # deploy the built artifact
# Cache hit rates: node_modules cache saves ~2-3 minutes per run
# Cache key uses package-lock.json hash -> invalidated only when deps changeKey Points to Remember
- 1Jobs run in parallel by default; use needs: to create sequential dependencies.
- 2Matrix strategy multiplies a job across combinations — test NxM configurations in parallel.
- 3Artifacts pass build outputs between jobs; they are not available between workflow runs.
- 4Cache persists directories between runs — key should include a hash of the lockfile.
- 5Environment protection rules in GitHub settings create manual approval gates for deployment jobs.
- 6timeout-minutes prevents runaway jobs from consuming runner minutes forever.
Interview Questions
Sign in to ask AriaHow do you pass data between jobs in GitHub Actions?
What is a matrix build and when do you use it?
Ask Aria about GitHub Actions — Workflows, Jobs & Steps
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.