GitHub Actions Tutorial — Workflow Structure

2 minute read

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 →

Updated: