GitHub Actions Tutorial — Workflow Structure
Workflow Structure
Every GitHub Actions workflow follows the same YAML structure. Understanding this structure is like learning the grammar of a language — once you know it, you can write any workflow.
The Anatomy of a Workflow
name: <workflow-name> # 1. Name (display name)
on: # 2. Trigger (when to run)
<event>:
<filters>
permissions: # 3. Permissions (optional)
contents: read
env: # 4. Environment variables (optional)
MY_VAR: "value"
jobs: # 5. Jobs (what to run)
<job-id>:
runs-on: <runner> # 6. Runner (where to run)
steps: # 7. Steps (sequential tasks)
- name: <step-name>
uses: <action> # Pre-built action
- name: <step-name>
run: <command> # Shell command
1. Name
The name: field sets the display name in the Actions tab. It’s optional but highly recommended.
name: CI Pipeline
If omitted, GitHub uses the filename as the workflow name.
2. Trigger (on:)
The on: field defines what events trigger this workflow. The most common triggers:
# Single event
on: push
# Multiple events
on: [push, pull_request]
# Events with filters
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
We cover triggers in depth in Chapter 3.
3. Permissions
Control what the workflow’s GITHUB_TOKEN can access:
permissions:
contents: read
pull-requests: write
issues: write
Best practice: Use least-privilege permissions. Only grant what the workflow needs.
4. Environment Variables
Define variables available to all jobs and steps:
env:
NODE_ENV: production
APP_VERSION: "2.1.0"
You can also set env: at the job or step level (more specific scopes override broader ones).
5. Jobs
Jobs are the main work units. Each job gets its own runner (fresh VM):
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo "Building..."
test:
runs-on: ubuntu-latest
steps:
- run: echo "Testing..."
deploy:
runs-on: ubuntu-latest
needs: [build, test] # runs after build AND test
steps:
- run: echo "Deploying..."
┌───────┐ ┌───────┐
│ build │ │ test │ ← parallel by default
└───┬───┘ └───┬───┘
│ │
└────┬─────┘
│
┌────▼────┐
│ deploy │ ← sequential (needs: [build, test])
└─────────┘
6. Runner (runs-on:)
Specifies the machine type for the job:
# GitHub-hosted runners
runs-on: ubuntu-latest
runs-on: windows-latest
runs-on: macos-latest
# Self-hosted runner with labels
runs-on: [self-hosted, linux, x64]
7. Steps
Steps run sequentially within a job. Two types:
Shell command (run:):
steps:
- name: Install dependencies
run: npm install
- name: Multi-line command
run: |
echo "Line 1"
echo "Line 2"
npm test
Pre-built action (uses:):
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
Complete Example
Here’s a realistic CI workflow showing all structural elements:
name: CI Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
env:
NODE_VERSION: 20
jobs:
lint:
name: Lint Code
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- run: npm ci
- run: npm run lint
test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- run: npm ci
- run: npm test
build:
name: Build
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- run: npm ci
- run: npm run build
Quick Reference
| Element | YAML Key | Required? |
|---|---|---|
| Name | name: |
Optional (recommended) |
| Trigger | on: |
✅ Yes |
| Permissions | permissions: |
Optional |
| Env vars | env: |
Optional |
| Jobs | jobs: |
✅ Yes |
| Runner | runs-on: |
✅ Yes (per job) |
| Steps | steps: |
✅ Yes (per job) |
| Shell command | run: |
At least one step |
| Pre-built action | uses: |
At least one step |
← Previous: First Workflow
Next: Workflow Dispatch →