Skip to main content

GitHub Actions Fundamentals

What is GitHub Actions?

GitHub Actions is a CI/CD and automation platform built into GitHub. It allows you to automate workflows directly in your repository—building, testing, and deploying code on every push or pull request.

Core Components

Workflow (YAML file)
├─ Events (triggers)
├─ Jobs (parallel or sequential)
│ ├─ Runners (execution environment)
│ ├─ Steps (individual tasks)
│ │ ├─ Actions (reusable code)
│ │ └─ Run (shell commands)
│ └─ Environment variables & secrets
└─ Artifacts (build outputs)

Workflows

A workflow is a YAML file in .github/workflows/ that defines automated processes.

GitHub Actions Workflow Structure


name: Build and Deploy
on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run tests
run: npm test
- name: Deploy
run: npm run deploy

File Location: .github/workflows/my-workflow.yml


Events (Triggers)

Workflows are triggered by GitHub events.

on:
# Repository events
push:
branches: [main, develop]
paths: ['src/**'] # Only when src/ changes
pull_request:
branches: [main]

# Scheduled events
schedule:
- cron: '0 9 * * 1' # Every Monday at 9 AM

# Manual trigger
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy'
required: true

# Release events
release:
types: [published]

# External events
workflow_run:
workflows: [CI]
types: [completed]

Jobs

Jobs are a set of steps that execute on the same runner.

jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo "Building..."

test:
runs-on: ubuntu-latest
needs: build # Wait for 'build' job first
steps:
- run: echo "Testing..."

deploy:
runs-on: ubuntu-latest
needs: [build, test] # Depends on multiple jobs
if: github.ref == 'refs/heads/main' # Only on main branch
steps:
- run: echo "Deploying..."

Runners

Runners are machines that execute your jobs.

Runner Types

GitHub-Hosted Runners

jobs:
linux:
runs-on: ubuntu-latest # or ubuntu-20.04
windows:
runs-on: windows-latest
macos:
runs-on: macos-latest

Self-Hosted Runners

jobs:
build:
runs-on: [self-hosted, linux, x64]
steps:
- run: echo "Running on self-hosted runner"

Steps

Steps are individual tasks within a job.

jobs:
example:
runs-on: ubuntu-latest
steps:
# Use an action
- uses: actions/checkout@v3
with:
ref: develop

# Run shell command
- name: Install dependencies
run: npm install

# Run multiline command
- name: Build
run: |
npm run build
npm run test

# Use environment variables
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
NODE_ENV: production
run: npm run deploy

Actions (Reusable Code)

Actions are reusable units of code.

Official Actions

# Checkout repository
- uses: actions/checkout@v3

# Setup Node.js
- uses: actions/setup-node@v3
with:
node-version: '16'

# Setup Python
- uses: actions/setup-python@v4
with:
python-version: '3.9'

# Upload artifacts
- uses: actions/upload-artifact@v3
with:
name: build-output
path: dist/

# Deploy to GitHub Pages
- uses: actions/deploy-pages@v2

Community Actions

# Build and push Docker image
- uses: docker/build-push-action@v4
with:
context: .
push: true
tags: myregistry/myimage:latest

# Create a PR
- uses: peter-evans/create-pull-request@v4
with:
title: 'Automated PR'
body: 'This is an automated pull request'

Secrets

Sensitive data (API keys, passwords) stored securely.

# Define in GitHub settings
# Settings > Secrets and variables > Actions > New repository secret

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to AWS
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
aws s3 cp build/ s3://my-bucket/

Best Practices:

  • Never hardcode secrets in YAML
  • Use organization secrets for shared values
  • Rotate secrets regularly
  • Audit secret access

Matrix Builds

Test across multiple versions/environments.

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14, 16, 18]
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm test

Generated Matrix:

  • Node 14 on Ubuntu
  • Node 16 on Ubuntu
  • Node 18 on Ubuntu
  • Node 14 on Windows
  • Node 16 on Windows
  • Node 18 on Windows

(6 total combinations)


Caching

Cache dependencies to speed up workflows.

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- uses: actions/setup-node@v3
with:
node-version: 16
cache: 'npm' # Automatically cache node_modules

# Or manual cache
- uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-

- run: npm ci
- run: npm test

Environment Variables

Define variables at different scopes.

# Workflow level
env:
REGISTRY: ghcr.io

jobs:
build:
runs-on: ubuntu-latest

# Job level
env:
NODE_ENV: production

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

# Default environment variables
- run: echo $GITHUB_EVENT_NAME
- run: echo $GITHUB_REPOSITORY
- run: echo $GITHUB_SHA

Conditional Execution

Control when steps/jobs run.

jobs:
build:
runs-on: ubuntu-latest
steps:
# Run if condition is true
- name: Build
if: github.event_name == 'push'
run: npm run build

# Always run
- name: Notify
if: always()
run: curl notify.example.com

# Run if previous step failed
- name: Error handler
if: failure()
run: echo "Build failed!"

deploy:
needs: build
# Skip if any needed job failed
if: success()
runs-on: ubuntu-latest
steps:
- run: npm run deploy

Practical Examples

Example 1: Simple CI

name: CI

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

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- uses: actions/setup-node@v3
with:
node-version: 16
cache: 'npm'

- run: npm ci
- run: npm run lint
- run: npm test
- run: npm run build

Example 2: Build and Publish Docker Image

name: Docker Build

on:
push:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- uses: docker/setup-buildx-action@v2

- uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:latest

Example 3: Deploy to AWS

name: Deploy to AWS

on:
push:
branches: [main]

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- uses: actions/setup-node@v3
with:
node-version: 16

- run: npm ci
- run: npm run build

- uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::123456789:role/github-actions
aws-region: us-east-1

- run: aws s3 sync dist/ s3://my-bucket/

Key Takeaways

  1. Workflows are YAML: Simple syntax, version-controlled
  2. Events trigger workflows: Push, PR, schedule, manual, etc.
  3. Jobs run on runners: GitHub-hosted or self-hosted
  4. Actions are reusable: Use official or community actions
  5. Secrets are secure: Never hardcode sensitive data
  6. Matrix builds: Test multiple configurations easily
  7. Caching speeds up: Cache dependencies, build artifacts

Further Resources