GitHub Actions Workflow Basics: A Complete Introduction

Shunku

Introduction

GitHub Actions is a powerful automation platform built directly into GitHub. It allows you to automate your software development workflows right in your repository—from building and testing code to deploying applications.

This article covers the fundamental concepts you need to understand to start creating effective workflows.

Core Concepts

flowchart TB
    subgraph Workflow["Workflow (workflow.yml)"]
        subgraph Job1["Job: build"]
            S1["Step 1: Checkout"]
            S2["Step 2: Setup Node"]
            S3["Step 3: Install deps"]
            S4["Step 4: Run tests"]
        end
        subgraph Job2["Job: deploy"]
            S5["Step 1: Checkout"]
            S6["Step 2: Deploy"]
        end
    end

    Event["Event (push, PR, schedule)"] --> Workflow
    Job1 --> Job2

    style Event fill:#f59e0b,color:#fff
    style Workflow fill:#3b82f6,color:#fff
    style Job1 fill:#8b5cf6,color:#fff
    style Job2 fill:#22c55e,color:#fff

Hierarchy Overview

Component Description
Workflow Automated process defined in a YAML file
Event Trigger that starts a workflow
Job Set of steps that execute on the same runner
Step Individual task within a job
Action Reusable unit of code
Runner Server that executes the workflow

YAML Syntax Essentials

Workflows are written in YAML. Here are the key syntax rules:

# Key-value pairs
name: My Workflow

# Nested structures use indentation (2 spaces)
jobs:
  build:
    runs-on: ubuntu-latest

# Lists use hyphens
steps:
  - name: First step
  - name: Second step

# Multi-line strings
run: |
  echo "Line 1"
  echo "Line 2"

# Inline strings
run: echo "Single line"

Workflow File Structure

Workflow files are stored in .github/workflows/ directory:

# .github/workflows/ci.yml
name: CI Pipeline

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

Event Triggers

Push and Pull Request Events

on:
  push:
    branches:
      - main
      - 'release/**'
    paths:
      - 'src/**'
      - '!src/**/*.md'
    tags:
      - 'v*'

  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened]

Scheduled Triggers

on:
  schedule:
    # Run at 00:00 UTC every day
    - cron: '0 0 * * *'
    # Run every Monday at 9:00 AM UTC
    - cron: '0 9 * * 1'

Manual Triggers

on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Deployment environment'
        required: true
        default: 'staging'
        type: choice
        options:
          - staging
          - production

      debug:
        description: 'Enable debug mode'
        required: false
        type: boolean
        default: false

Trigger Comparison

Trigger Use Case
push Run on every commit to specified branches
pull_request Run when PRs are opened or updated
schedule Periodic tasks (nightly builds, cleanup)
workflow_dispatch Manual execution with optional inputs
workflow_call Called by another workflow
repository_dispatch Triggered by external events via API

Jobs and Steps

Job Configuration

jobs:
  build:
    name: Build Application
    runs-on: ubuntu-latest
    timeout-minutes: 30

    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build

  test:
    name: Run Tests
    runs-on: ubuntu-latest
    needs: build  # Wait for build job

    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

Steps: Actions vs Run Commands

steps:
  # Using a pre-built action
  - name: Checkout repository
    uses: actions/checkout@v4
    with:
      fetch-depth: 0

  # Running shell commands
  - name: Build project
    run: |
      npm ci
      npm run build
    working-directory: ./frontend

  # Using a specific shell
  - name: PowerShell script
    shell: pwsh
    run: Get-Process

Job Dependencies

flowchart LR
    subgraph Parallel["Parallel Execution"]
        lint["lint"]
        test["test"]
    end

    build["build"] --> Parallel
    Parallel --> deploy["deploy"]

    style build fill:#3b82f6,color:#fff
    style Parallel fill:#8b5cf6,color:#fff
    style deploy fill:#22c55e,color:#fff
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Building..."

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

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

  deploy:
    needs: [lint, test]  # Wait for both
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying..."

Expressions and Contexts

Using Contexts

steps:
  - name: Show context information
    run: |
      echo "Repository: ${{ github.repository }}"
      echo "Branch: ${{ github.ref_name }}"
      echo "Actor: ${{ github.actor }}"
      echo "Event: ${{ github.event_name }}"
      echo "SHA: ${{ github.sha }}"

Common Contexts

Context Description
github Workflow run information
env Environment variables
vars Repository/org variables
secrets Encrypted secrets
job Current job information
steps Step outputs and status
runner Runner information
matrix Matrix values for current job

Conditional Execution

steps:
  - name: Only on main branch
    if: github.ref == 'refs/heads/main'
    run: echo "On main branch"

  - name: Only on pull requests
    if: github.event_name == 'pull_request'
    run: echo "This is a PR"

  - name: Run even if previous step failed
    if: always()
    run: echo "This always runs"

  - name: Run only on failure
    if: failure()
    run: echo "Previous step failed"

  - name: Skip on forks
    if: github.repository == 'owner/repo'
    run: echo "Not a fork"

Environment Variables

Defining Variables

env:
  # Workflow-level
  NODE_ENV: production

jobs:
  build:
    runs-on: ubuntu-latest
    env:
      # Job-level
      CI: true

    steps:
      - name: Build
        env:
          # Step-level
          API_URL: https://api.example.com
        run: |
          echo "NODE_ENV: $NODE_ENV"
          echo "CI: $CI"
          echo "API_URL: $API_URL"

Using Secrets

steps:
  - name: Deploy
    env:
      API_KEY: ${{ secrets.API_KEY }}
    run: ./deploy.sh

  - name: Login to Docker Hub
    uses: docker/login-action@v3
    with:
      username: ${{ secrets.DOCKER_USERNAME }}
      password: ${{ secrets.DOCKER_PASSWORD }}

Practical Example: Complete CI Workflow

name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  NODE_VERSION: '20'

jobs:
  lint:
    name: Lint Code
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - run: npm ci
      - run: npm run lint

  test:
    name: Run Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - run: npm ci
      - run: npm test -- --coverage

      - name: Upload coverage
        uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage/

  build:
    name: Build
    runs-on: ubuntu-latest
    needs: [lint, test]
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - run: npm ci
      - run: npm run build

      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: build
          path: dist/

Best Practices

1. Pin Action Versions

# Good: Use specific version or SHA
- uses: actions/checkout@v4
- uses: actions/setup-node@v4.0.2
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608

# Avoid: Using latest or branches
- uses: actions/checkout@main  # Don't do this

2. Use Caching

- name: Cache node modules
  uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

3. Minimize Secrets Exposure

# Good: Pass as environment variable
- name: Deploy
  env:
    TOKEN: ${{ secrets.DEPLOY_TOKEN }}
  run: ./deploy.sh

# Avoid: Inline in command (can leak in logs)
- run: ./deploy.sh ${{ secrets.DEPLOY_TOKEN }}  # Don't do this

4. Use Timeout

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 15  # Prevent runaway jobs

Summary

Concept Key Points
Workflows YAML files in .github/workflows/
Triggers push, pull_request, schedule, workflow_dispatch
Jobs Run in parallel by default; use needs for dependencies
Steps uses for actions, run for commands
Contexts ${{ github.* }}, ${{ secrets.* }}, etc.
Conditions if expressions control execution

With these fundamentals, you can create workflows that automatically build, test, and deploy your applications whenever you push code or open pull requests.

References

  • Manning - GitHub Actions in Action, Chapter 3
  • O'Reilly - Learning GitHub Actions, Chapters 1-4
  • GitHub Docs - Workflow Syntax