DEV Community

Carlos Oliva Pascual
Carlos Oliva Pascual

Posted on • Originally published at stacknotice.com

GitHub Actions Complete Guide: CI/CD for Every Project (2026)

GitHub Actions is CI/CD built into GitHub — no separate service, 2,000 free minutes/month for private repos. Every push, PR, or tag can trigger a workflow that tests, builds, and deploys automatically.

How It Works

Workflows are YAML files in .github/workflows/. GitHub spins up a virtual machine (Ubuntu, Windows, or macOS) and runs jobs when the trigger fires.

First Workflow

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

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

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci
      - run: npx tsc --noEmit
      - run: npm test
      - run: npm run build
Enter fullscreen mode Exit fullscreen mode

Triggers

on:
  push:
    branches: [main, 'release/*']
    paths: ['src/**', 'package.json']  # only when these change

  schedule:
    - cron: '0 2 * * 1'  # every Monday at 2am UTC

  workflow_dispatch:    # manual trigger from GitHub UI
    inputs:
      environment:
        type: choice
        options: [staging, production]

  workflow_run:         # when another workflow completes
    workflows: ['CI']
    types: [completed]
Enter fullscreen mode Exit fullscreen mode

Job Dependencies

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test

  build:
    needs: test            # waits for test
    runs-on: ubuntu-latest
    steps:
      - run: npm ci && npm run build

  deploy:
    needs: [test, build]   # waits for both
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying..."
Enter fullscreen mode Exit fullscreen mode

Secrets

steps:
  - name: Deploy
    env:
      DATABASE_URL: ${{ secrets.DATABASE_URL }}
      VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
    run: npx vercel --prod --token=$VERCEL_TOKEN
Enter fullscreen mode Exit fullscreen mode

Store in GitHub → Settings → Secrets and variables. Organization secrets work across all repos.

Caching

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

- run: npm ci
Enter fullscreen mode Exit fullscreen mode

Cache invalidates automatically when package-lock.json changes.

Matrix Builds

jobs:
  test:
    strategy:
      matrix:
        node-version: ['18', '20', '22']
        os: [ubuntu-latest, macos-latest]
      fail-fast: false

    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci && npm test
Enter fullscreen mode Exit fullscreen mode

Generates 6 parallel jobs. fail-fast: false keeps all running even if one fails.

Full CI/CD: Node.js + Docker + Deploy

name: Deploy

on:
  push:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: testdb
        ports: ['5432:5432']
        options: --health-cmd pg_isready --health-interval 10s --health-retries 5

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci
      - run: npx prisma migrate deploy
        env:
          DATABASE_URL: postgres://test:test@localhost:5432/testdb
      - run: npm test
        env:
          DATABASE_URL: postgres://test:test@localhost:5432/testdb

  build-and-push:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=sha-
            type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}

      - uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    needs: build-and-push
    environment: production   # requires manual approval
    runs-on: ubuntu-latest

    steps:
      - uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
            docker stop my-app || true
            docker rm my-app || true
            docker run -d --name my-app --restart unless-stopped \
              -p 3000:3000 -e DATABASE_URL=${{ secrets.DATABASE_URL }} \
              ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
Enter fullscreen mode Exit fullscreen mode

Useful Patterns

Skip CI

if: "!contains(github.event.head_commit.message, '[skip ci]')"
Enter fullscreen mode Exit fullscreen mode

Notify on failure

- name: Notify Slack
  if: failure()
  uses: slackapi/slack-github-action@v1
  with:
    payload: '{"text": "Deploy failed for ${{ github.repository }}"}'
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
Enter fullscreen mode Exit fullscreen mode

Dependabot for auto-updates

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: github-actions
    directory: /
    schedule:
      interval: weekly
  - package-ecosystem: npm
    directory: /
    schedule:
      interval: weekly
Enter fullscreen mode Exit fullscreen mode

Free Tier vs Costs

Plan Free minutes Windows multiplier macOS multiplier
Free 2,000/month 2x 10x
Pro 3,000/month 2x 10x

Public repos: unlimited minutes. Self-hosted runners: free (you provide hardware).

Save minutes: use paths filters, cache aggressively, limit matrix size.


Full article at stacknotice.com/blog/github-actions-complete-guide-2026

Top comments (0)