Skip to main content

GitHub Actions Fundamentals

Understanding the core concepts and building blocks of GitHub Actions workflows.

What is GitHub Actions?

GitHub Actions is GitHub's built-in CI/CD platform that allows you to automate workflows directly within your GitHub repositories. It enables you to build, test, and deploy your code right from GitHub with powerful automation capabilities.

Core Concepts

Key Components

1. Workflows

  • Definition: Automated procedures defined in YAML files
  • Location: .github/workflows/ directory in your repository
  • Trigger: Events that start workflow execution
  • Scope: Can be triggered by repository events or manually

2. Events

  • Push Events: Code pushed to branches
  • Pull Request Events: PR opened, updated, or merged
  • Schedule Events: Time-based triggers using cron syntax
  • Manual Events: Workflow dispatch triggers
  • Webhook Events: External service notifications

3. Jobs

  • Definition: Set of steps that execute on the same runner
  • Parallel Execution: Multiple jobs can run simultaneously
  • Dependencies: Jobs can depend on other jobs
  • Environment: Each job runs in a fresh virtual environment

4. Steps

  • Individual Tasks: Single commands or actions within a job
  • Sequential Execution: Steps run one after another
  • Shared Environment: Steps share the same runner environment
  • Conditional Execution: Steps can run based on conditions

5. Actions

  • Reusable Units: Pre-built or custom workflow components
  • Marketplace: Thousands of community-contributed actions
  • Custom Actions: Actions you create for your specific needs
  • Composite Actions: Actions that combine multiple steps

Workflow Syntax

Basic Workflow Structure

name: CI/CD Pipeline

# When to run the workflow
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]

# Environment variables available to all jobs
env:
NODE_VERSION: '18'
DOCKER_REGISTRY: ghcr.io

# Jobs to execute
jobs:
build-and-test:
runs-on: ubuntu-latest

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

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

- name: Install dependencies
run: npm ci

- name: Run tests
run: npm test

Workflow Metadata

Name

name: My Workflow Name
# Optional: Display name for the workflow

Description

description: 'Builds and tests the application'
# Optional: Description of what the workflow does

Run Name

run-name: 'Build and test on ${{ github.ref }} by ${{ github.actor }}'
# Optional: Dynamic name for workflow runs

Event Configuration

Push Events

on:
push:
branches: [ main, develop ]
branches-ignore: [ 'legacy/**' ]
tags: [ 'v*' ]
paths: [ 'src/**', 'package.json' ]
paths-ignore: [ 'docs/**', '*.md' ]

Pull Request Events

on:
pull_request:
branches: [ main ]
types: [ opened, synchronize, reopened, closed ]
paths: [ 'src/**' ]

Schedule Events

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

Manual Triggers

on:
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy to'
required: true
default: 'staging'
type: choice
options:
- staging
- production
version:
description: 'Version to deploy'
required: false
type: string

Multiple Events

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
schedule:
- cron: '0 2 * * *'
workflow_dispatch:

Job Configuration

Basic Job Structure

jobs:
job-name:
runs-on: ubuntu-latest
needs: [other-job]
if: github.ref == 'refs/heads/main'

steps:
- name: Step name
run: echo "Hello World"

Runner Selection

# GitHub-hosted runners
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
runs-on: windows-latest
runs-on: windows-2019
runs-on: macos-latest
runs-on: macos-11

# Self-hosted runners
runs-on: [self-hosted, linux, x64]
runs-on: [self-hosted, windows, x64]
runs-on: [self-hosted, macos, x64]

Job Dependencies

jobs:
setup:
runs-on: ubuntu-latest
steps:
- run: echo "Setup complete"

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

test:
runs-on: ubuntu-latest
needs: [setup, build]
steps:
- run: echo "Testing..."

Job Conditions

jobs:
deploy:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- run: echo "Deploying to production"

notify:
runs-on: ubuntu-latest
if: always()
needs: [build, test]
steps:
- run: echo "Sending notifications"

Step Configuration

Using Actions

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

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'

Running Commands

steps:
- name: Install dependencies
run: npm ci

- name: Run tests
run: |
npm run test:unit
npm run test:integration
npm run test:e2e

Environment Variables

steps:
- name: Build application
run: npm run build
env:
NODE_ENV: production
API_URL: ${{ secrets.API_URL }}
BUILD_VERSION: ${{ github.sha }}

Conditional Steps

steps:
- name: Run tests
run: npm test
if: github.event_name == 'pull_request'

- name: Deploy to staging
run: npm run deploy:staging
if: github.ref == 'refs/heads/develop'

- name: Deploy to production
run: npm run deploy:production
if: github.ref == 'refs/heads/main'

Events and Triggers

Repository Events

Push Events

on:
push:
branches: [ main, develop ]
# Trigger on pushes to main or develop branches

tags: [ 'v*' ]
# Trigger on version tags

paths: [ 'src/**', 'package.json' ]
# Trigger only when specified paths change

paths-ignore: [ 'docs/**', '*.md' ]
# Ignore changes to documentation files

Pull Request Events

on:
pull_request:
branches: [ main ]
types: [ opened, synchronize, reopened, closed ]
# Trigger on specific PR events

paths: [ 'src/**' ]
# Trigger only when source code changes

Issue Events

on:
issues:
types: [ opened, closed, labeled ]
# Trigger on issue events

External Events

Webhook Events

on:
repository_dispatch:
types: [ deployment-requested ]
# Trigger on repository dispatch events

workflow_dispatch:
inputs:
environment:
description: 'Target environment'
required: true
type: choice
options:
- staging
- production
# Manual workflow trigger with inputs

Schedule Events

on:
schedule:
# Every day at 2:30 AM UTC
- cron: '30 2 * * *'

# Every Monday at 9 AM UTC
- cron: '0 9 * * 1'

# Every 15 minutes
- cron: '*/15 * * * *'

Event Filters

Branch Filters

on:
push:
branches:
- main
- develop
- 'feature/*'
- 'release/*'
branches-ignore:
- 'legacy/**'
- 'deprecated/**'

Path Filters

on:
push:
paths:
- 'src/**'
- 'package.json'
- 'Dockerfile'
paths-ignore:
- 'docs/**'
- '*.md'
- 'README.md'

Tag Filters

on:
push:
tags:
- 'v*'
- 'v*.*.*'
- 'release-*'

Actions Marketplace

Source Control

- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch all history

- name: Checkout with submodules
uses: actions/checkout@v3
with:
submodules: recursive

Language Setup

# Node.js
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'

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

# Java
- name: Setup Java
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'

Docker Actions

- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:${{ github.sha }}
ghcr.io/${{ github.repository }}:latest

Testing Actions

- name: Run tests with coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
fail_ci_if_error: true

- name: Upload test results
uses: actions/upload-artifact@v3
with:
name: test-results
path: test-results/

Custom Actions

Creating a Custom Action

# action.yml
name: 'Custom Action'
description: 'A custom action for my workflow'
inputs:
name:
description: 'Name to greet'
required: true
default: 'World'
runs:
using: 'node16'
main: 'dist/index.js'

JavaScript Action

// src/index.js
const core = require('@actions/core');
const github = require('@actions/github');

async function run() {
try {
const name = core.getInput('name');
console.log(`Hello, ${name}!`);

// Set output
core.setOutput('greeting', `Hello, ${name}!`);

} catch (error) {
core.setFailed(error.message);
}
}

run();

Secrets and Environment Variables

Secrets Management

Repository Secrets

steps:
- name: Use secret
run: echo "API key: ${{ secrets.API_KEY }}"
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

Organization Secrets

steps:
- name: Use organization secret
run: echo "Org secret: ${{ secrets.ORG_SECRET }}"

Environment Secrets

jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy
run: echo "Deploying with ${{ secrets.DEPLOY_KEY }}"

Environment Variables

Workflow-Level Variables

env:
NODE_ENV: production
BUILD_VERSION: ${{ github.sha }}
REPOSITORY: ${{ github.repository }}

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Build
run: echo "Building version ${{ env.BUILD_VERSION }}"

Job-Level Variables

jobs:
build:
runs-on: ubuntu-latest
env:
NODE_ENV: production
API_URL: https://api.example.com
steps:
- name: Build
run: echo "Building for ${{ env.API_URL }}"

Step-Level Variables

steps:
- name: Build
run: echo "Building application"
env:
NODE_ENV: production
DEBUG: false

Built-in Variables

GitHub Context Variables

steps:
- name: Show GitHub context
run: |
echo "Repository: ${{ github.repository }}"
echo "Ref: ${{ github.ref }}"
echo "SHA: ${{ github.sha }}"
echo "Actor: ${{ github.actor }}"
echo "Event: ${{ github.event_name }}"
echo "Workflow: ${{ github.workflow }}"
echo "Run ID: ${{ github.run_id }}"
echo "Run Number: ${{ github.run_number }}"

Runner Context Variables

steps:
- name: Show runner context
run: |
echo "OS: ${{ runner.os }}"
echo "Architecture: ${{ runner.arch }}"
echo "Tool cache: ${{ runner.tool_cache }}"
echo "Temp: ${{ runner.temp }}"
echo "Workspace: ${{ runner.workspace }}"

Best Practices

Workflow Organization

File Naming

.github/workflows/
├── ci.yml # Continuous integration
├── cd.yml # Continuous deployment
├── security.yml # Security scanning
├── dependency-update.yml # Dependency updates
└── release.yml # Release workflows

Workflow Structure

name: Descriptive Workflow Name
description: 'Clear description of what this workflow does'

on:
# Specific event triggers
push:
branches: [ main ]

env:
# Global environment variables
NODE_VERSION: '18'

jobs:
# Well-named jobs with clear purposes
test:
runs-on: ubuntu-latest
steps:
# Clear step names and descriptions

Security Best Practices

Secrets Usage

steps:
- name: Secure secret usage
run: |
# Good: Use secrets in environment variables
export API_KEY="${{ secrets.API_KEY }}"
./deploy.sh

# Bad: Don't echo secrets
# echo "API key: ${{ secrets.API_KEY }}"

Permission Management

permissions:
contents: read
packages: write
pull-requests: read
statuses: write

Performance Optimization

Caching

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

Parallel Jobs

jobs:
test-unit:
runs-on: ubuntu-latest
steps:
- run: npm run test:unit

test-integration:
runs-on: ubuntu-latest
steps:
- run: npm run test:integration

lint:
runs-on: ubuntu-latest
steps:
- run: npm run lint

Common Patterns

Conditional Execution

steps:
- name: Run tests
run: npm test
if: github.event_name == 'pull_request'

- name: Deploy
run: npm run deploy
if: github.ref == 'refs/heads/main'

Matrix Builds

strategy:
matrix:
node-version: [16, 18, 20]
os: [ubuntu-latest, windows-latest, macos-latest]

Artifact Management

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

- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: build-files
path: dist/

Key Takeaways

Essential Concepts

  1. Workflows are the main automation units in GitHub Actions
  2. Events trigger workflow execution based on repository activities
  3. Jobs run in parallel and contain sequential steps
  4. Actions are reusable workflow components from the marketplace
  5. Secrets provide secure storage for sensitive information

Best Practices Summary

  • Clear Naming: Use descriptive names for workflows, jobs, and steps
  • Security First: Never expose secrets in logs or outputs
  • Performance: Use caching and parallel execution when possible
  • Organization: Structure workflows logically and maintainably
  • Documentation: Comment complex workflows and document custom actions

Common Mistakes to Avoid

  • Hardcoded Values: Use environment variables and secrets
  • Large Workflows: Break complex workflows into smaller, focused ones
  • Missing Conditions: Use conditional execution to avoid unnecessary runs
  • Poor Caching: Implement caching for dependencies and build artifacts
  • Insecure Practices: Never commit secrets or expose sensitive data

Next Steps: Ready to build advanced workflows? Continue to Section 2.2: Advanced Workflow Patterns to learn about matrix builds, conditional execution, and performance optimization.


Understanding these fundamentals is crucial for building effective GitHub Actions workflows. In the next section, we'll explore advanced patterns that will help you create more sophisticated and efficient CI/CD pipelines.