GitHub Actions
01 / 02

Workflows & Syntax

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/

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free