All topics
DevOps · Learning hub

GitHub Actions notes for developers

Master GitHub Actions with a curated set of 2 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore DevOps notes
GitHub Actions

Workflows & Syntax

Workflows & Syntax Workflow Structure # .github/workflows/ci.yml name: CI on: push: branches: [main, develop] pull_request: branches: [main] schedule: - cron: '

Workflows & Syntax

Workflow Structure

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

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 2 * * *'        # daily at 2am UTC
  workflow_dispatch:             # manual trigger
    inputs:
      environment:
        description: 'Deploy to'
        required: true
        default: 'staging'
        type: choice
        options: [staging, production]

jobs:
  test:
    name: Test
    runs-on: ubuntu-latest       # ubuntu-latest | ubuntu-22.04 | windows-latest | macos-latest
    timeout-minutes: 30

    strategy:
      matrix:
        node-version: [18, 20, 22]
        os: [ubuntu-latest, macos-latest]
      fail-fast: false           # continue other matrix jobs on failure

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0         # full history for git-based versioning

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

      - name: Install pnpm
        uses: pnpm/action-setup@v3
        with:
          version: 9

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Type check
        run: pnpm tsc --noEmit

      - name: Lint
        run: pnpm lint

      - name: Test
        run: pnpm test --coverage
        env:
          NODE_ENV: test
          DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}

      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

Expressions & Contexts

# Contexts
${{ github.sha }}                  # commit SHA
${{ github.ref }}                  # refs/heads/main
${{ github.ref_name }}             # main
${{ github.repository }}           # owner/repo
${{ github.actor }}                # triggering user
${{ github.event_name }}           # push, pull_request, etc.
${{ github.workspace }}            # /home/runner/work/repo

${{ runner.os }}                   # Linux, Windows, macOS
${{ env.MY_VAR }}                  # environment variable
${{ secrets.MY_SECRET }}           # repository secret
${{ vars.MY_VAR }}                 # repository variable (not secret)
${{ inputs.environment }}          # workflow_dispatch input

# Expressions
${{ github.event_name == 'push' }}
${{ contains(github.ref, 'main') }}
${{ startsWith(github.ref, 'refs/tags/') }}
${{ matrix.node-version >= 20 }}

# Conditional steps
- name: Deploy to production
  if: github.ref == 'refs/heads/main' && github.event_name == 'push'
  run: ./deploy.sh

- name: Only on PRs
  if: github.event_name == 'pull_request'
  run: echo "PR event"

- name: Run if previous step failed
  if: failure()
  run: ./notify-slack.sh

Caching & Artifacts

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

# Upload artifact (pass between jobs or download later)
- name: Upload build
  uses: actions/upload-artifact@v4
  with:
    name: build-output
    path: dist/
    retention-days: 7

# Download artifact in another job
- name: Download build
  uses: actions/download-artifact@v4
  with:
    name: build-output
    path: dist/
GitHub Actions

CI/CD Pipelines & Deployment

CI/CD Pipelines & Deployment Multi-Job Pipeline name: CI/CD Pipeline on: push: branches: [main] pull_request: branches: [main] jobs: test: runs-on: ubuntu-lates

CI/CD Pipelines & Deployment

Multi-Job Pipeline

name: CI/CD Pipeline

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

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: testdb
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: npm test
        env:
          DATABASE_URL: postgres://postgres:postgres@localhost:5432/testdb

  build:
    needs: test            # only runs if test passes
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name == 'push' }}
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy-staging:
    needs: build
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: staging          # requires approval if configured
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to staging
        run: |
          echo "Deploying ${{ github.sha }} to staging"
          # kubectl set image deployment/app app=ghcr.io/...

  deploy-production:
    needs: deploy-staging
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://myapp.com
    steps:
      - name: Deploy to production
        run: echo "Deploy to prod"

Reusable Workflows & Composite Actions

# .github/workflows/reusable-deploy.yml
name: Reusable Deploy
on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
    secrets:
      DEPLOY_KEY:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to ${{ inputs.environment }}
        env:
          KEY: ${{ secrets.DEPLOY_KEY }}
        run: ./deploy.sh ${{ inputs.environment }}

# Call from another workflow
jobs:
  deploy:
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: production
    secrets:
      DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}

# Composite action — .github/actions/setup/action.yml
name: Setup Project
runs:
  using: composite
  steps:
    - uses: actions/setup-node@v4
      with:
        node-version: 20
        cache: pnpm
    - uses: pnpm/action-setup@v3
    - run: pnpm install --frozen-lockfile
      shell: bash

# Use in workflow
- uses: ./.github/actions/setup

Secrets & Security

# Secrets — set in repo/org settings → Actions → Secrets
# Never echo secrets in logs
- name: Use secret
  env:
    TOKEN: ${{ secrets.MY_TOKEN }}
  run: ./script.sh    # uses $TOKEN

# GITHUB_TOKEN — auto-provided, scoped to repo
- name: Create release
  uses: softprops/action-gh-release@v1
  with:
    files: dist/*
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# Permissions — principle of least privilege
permissions:
  contents: read
  packages: write
  pull-requests: write

# OIDC — keyless cloud auth (no long-lived secrets)
- uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789:role/deploy
    aws-region: us-east-1
# Requires: permissions: { id-token: write, contents: read }

Keep your GitHub Actions knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever