Concepts

GitHub Actions

GitHub's native CI/CD platform. Declarative YAML workflows that automate build, test, deploy, and any development lifecycle task — directly from the repository.

evergreen#ci-cd#automation#github#devops

GitHub Actions is the automation platform integrated into GitHub. Every repository can define YAML workflows that execute in response to events — push, PR, schedule, release, or any webhook.

Why choose it

  • Native integration — lives inside the repository, no external services to configure
  • Marketplace — over 20,000 reusable actions
  • Free runners — 2,000 minutes/month on public repos (unlimited), private with plan limits
  • Matrix builds — test on multiple OS/versions in parallel
  • Secrets management — encrypted variables at repo, environment, and org level

Anatomy of a workflow

# .github/workflows/ci.yml
name: CI                          # Workflow name
 
on:                               # Triggers
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
permissions:                      # GITHUB_TOKEN permissions
  contents: read
 
env:                              # Global variables
  NODE_VERSION: 20
 
jobs:                             # One or more jobs
  build:
    runs-on: ubuntu-latest        # Runner
    steps:
      - uses: actions/checkout@v4 # Reusable action
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'
      - run: pnpm install         # Shell command
      - run: pnpm test
      - run: pnpm build

Main triggers

on:
  # Push and PRs
  push:
    branches: [main, 'release/**']
    paths: ['src/**', 'package.json']     # only if these files change
  pull_request:
    types: [opened, synchronize, reopened]
 
  # Scheduled (cron UTC)
  schedule:
    - cron: '0 6 * * 1'                   # Monday at 6:00 UTC
 
  # Manual from UI or API
  workflow_dispatch:
    inputs:
      environment:
        description: 'Deploy target'
        required: true
        type: choice
        options: [staging, production]
 
  # When a release is published
  release:
    types: [published]
 
  # When another workflow completes
  workflow_run:
    workflows: [CI]
    types: [completed]

Jobs and dependencies

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pnpm lint
 
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pnpm test
 
  # Deploy only if lint and test pass
  deploy:
    needs: [lint, test]
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh

Matrix strategy

Test on multiple combinations:

jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        node: [18, 20, 22]
      fail-fast: false              # don't cancel others if one fails
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm test

Secrets and variables

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production         # environment with protections
    steps:
      - run: ./deploy.sh
        env:
          API_KEY: ${{ secrets.API_KEY }}
          DEPLOY_URL: ${{ vars.DEPLOY_URL }}

Secret levels:

  • Repository — available in all repo workflows
  • Environment — require approval and have protection rules
  • Organization — shared across org repos

Caching

- uses: actions/cache@v4
  with:
    path: |
      ~/.pnpm-store
      node_modules
    key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
    restore-keys: |
      ${{ runner.os }}-pnpm-

Artifacts

Share files between jobs or download later:

- uses: actions/upload-artifact@v4
  with:
    name: build-output
    path: dist/
    retention-days: 7
 
# In another job:
- uses: actions/download-artifact@v4
  with:
    name: build-output

Reusable workflows

Define workflows other repos can call:

# .github/workflows/reusable-deploy.yml
on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
    secrets:
      deploy_key:
        required: true
 
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying to ${{ inputs.environment }}"
# Call from another workflow
jobs:
  deploy:
    uses: org/repo/.github/workflows/reusable-deploy.yml@main
    with:
      environment: production
    secrets:
      deploy_key: ${{ secrets.DEPLOY_KEY }}

Composite actions

Create custom actions combining steps:

# .github/actions/setup-project/action.yml
name: Setup Project
description: Install dependencies and build
runs:
  using: composite
  steps:
    - uses: actions/setup-node@v4
      with:
        node-version: 20
        cache: 'pnpm'
    - run: pnpm install --frozen-lockfile
      shell: bash
    - run: pnpm build
      shell: bash

Security

# Principle of least privilege
permissions:
  contents: read
  pull-requests: write
 
# Pin actions by SHA (not by tag)
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
 
# Limit default GITHUB_TOKEN permissions
# Settings → Actions → General → Workflow permissions → Read repository contents

Costs and limits

PlanMinutes/monthStorage
Free2,000500 MB
Team3,0002 GB
Enterprise50,00050 GB

Public repos: unlimited minutes on Linux runners.

Anti-patterns

  • Monolithic workflows — a 500-line YAML. Split into reusable workflows.
  • No caching — reinstalling dependencies every run wastes minutes.
  • Secrets in logsecho $SECRET exposes them. GitHub masks but don't trust blindly.
  • Mutable tags on actions@v4 can change. Pin by SHA for security.
  • No timeout — hanging jobs consume minutes. Always define timeout-minutes.

Why it matters

GitHub Actions eliminated the need to maintain a separate CI server for most projects. By living alongside the code, workflows are versioned, reviewed in PRs, and executed without external configuration. For open source projects, the free minutes make it the default choice.

References

Concepts