GitHub Actions CI/CD DevOps

GitHub Actions — Automate ทุกอย่างใน CI/CD Pipeline 🤖GitHub Actions — Automate Everything in Your CI/CD Pipeline 🤖

By Anirach Mingkhwan DevOps & Vibe Coding 2026
GitHub Actions — Automate ทุกอย่างใน CI/CD
ในบทความนี้
  1. 🤖 GitHub Actions คืออะไร?
  2. 📝 Workflow แรก — Hello GitHub Actions
  3. 🚀 Real-World CI/CD Pipeline
  4. 🎯 Triggers — เมื่อไรจะรัน?
  5. 🔐 Secrets & Variables
  6. 🔀 Matrix Strategy — ทดสอบหลาย versions
  7. 📦 Caching — เร็วขึ้น 10x!
  8. 🧩 Marketplace Actions — อย่าเขียนเอง!
  9. ♻️ Reusable Workflows — DRY!
  10. 🔒 Security Scanning
  11. 🏠 Self-hosted Runners
  12. 💡 Advanced Patterns
  13. 📋 Best Practices
  14. 💰 Pricing — ฟรีแค่ไหน?
  15. สรุป
In this post
  1. 🤖 What Is GitHub Actions?
  2. 📝 Your First Workflow — Hello GitHub Actions
  3. 🚀 Real-World CI/CD Pipeline
  4. 🎯 Triggers — When Does It Run?
  5. 🔐 Secrets & Variables
  6. 🔀 Matrix Strategy — Testing Many Versions
  7. 📦 Caching — 10x Faster!
  8. 🧩 Marketplace Actions — Don't Write Your Own!
  9. ♻️ Reusable Workflows — DRY!
  10. 🔒 Security Scanning
  11. 🏠 Self-hosted Runners
  12. 💡 Advanced Patterns
  13. 📋 Best Practices
  14. 💰 Pricing — How Free Is It?
  15. Summary

ในโพสต์ CI/CD Pipeline เราเรียนแนวคิด CI/CD ทั่วไป แต่ถ้าถามว่า CI/CD tool ตัวไหนนิยมที่สุดตอนนี้? คำตอบคือ GitHub Actions — ฟรี, อยู่ใน GitHub เลย, ไม่ต้อง setup server แยก!

วันนี้มาเรียนรู้ GitHub Actions แบบละเอียด ตั้งแต่พื้นฐานไปจนถึง production-grade workflows 🤖🐕


🤖 GitHub Actions คืออะไร?

GitHub Actions คือ CI/CD platform ที่ built-in อยู่ใน GitHub — เขียน workflow เป็น YAML แล้ว GitHub รันให้อัตโนมัติ:

การทำงานของ GitHub Actions เป็นไปตามลำดับขั้นตอนที่ชัดเจน เริ่มตั้งแต่ developer push code ไปจนถึงการ deploy ขั้นสุดท้าย แต่ละขั้นตอนมีบทบาทและความสำคัญแตกต่างกัน ทำให้เราสามารถควบคุมกระบวนการ CI/CD ได้อย่างละเอียดและปลอดภัย ลองดูภาพรวมของกระบวนการทั้งหมด:

📝 git push
Trigger event
→
⚙️ Workflow
.github/workflows/
→
🏃 Jobs
Run on runners
→
✅ Steps
Actions + commands

จากแผนภาพข้างต้น เราจะเห็นว่าเมื่อมีการ push code แล้ว GitHub Actions จะเริ่มต้นกระบวนการโดยอัตโนมัติ โดยอ่านไฟล์ Workflow ที่เราเขียนไว้ จากนั้น Jobs ต่างๆ จะทำงานบน Runner (เครื่องเสมือน) และดำเนินการ Steps ต่างๆ ตามที่กำหนด การไหลของข้อมูลนี้ทำให้เรามั่นใจได้ว่าทุก commit จะผ่านการตรวจสอบและ deploy อย่างถูกต้อง

Key Concepts

ก่อนที่จะเริ่มเขียน Workflow แรก เราต้องเข้าใจแนวคิดหลักของ GitHub Actions เสียก่อน เพราะแต่ละ concept มีบทบาทและความสัมพันธ์กันอย่างชัดเจน การเข้าใจ concepts เหล่านี้จะช่วยให้เราออกแบบ CI/CD pipeline ได้อย่างมีประสิทธิภาพ และแก้ปัญหาได้เมื่อมีอะไรผิดพลาด:

Conceptคืออะไรตัวอย่าง
Workflowไฟล์ YAML ที่กำหนด automation.github/workflows/ci.yml
Eventสิ่งที่ trigger workflowpush, pull_request, schedule
Jobชุดของ steps ที่รันบน runner เดียวbuild, test, deploy
Stepคำสั่งหรือ action ทีละตัวrun: npm test
ActionReusable unit of codeactions/checkout@v4
Runnerเครื่องที่รัน jobubuntu-latest, self-hosted

ตารางข้างต้นแสดงให้เห็นว่า GitHub Actions มีโครงสร้างที่เป็นลำดับชั้น โดย Workflow เป็นระดับสูงสุดที่ประกอบด้วย Jobs หลายตัว แต่ละ Job ประกอบด้วย Steps หลายตัว และแต่ละ Step สามารถเป็น Action หรือ command ธรรมดาก็ได้ การเข้าใจโครงสร้างนี้จะช่วยให้เราเขียน Workflow ที่มีความซับซ้อนได้อย่างเป็นระบบ


📝 Workflow แรก — Hello GitHub Actions

เมื่อเข้าใจ concepts หลักแล้ว ตอนนี้มาลองเขียน Workflow แรกกัน เราจะเริ่มต้นด้วย Workflow ง่ายๆ ที่แสดงข้อมูลพื้นฐาน ซึ่งจะช่วยให้เราเข้าใจโครงสร้างของไฟล์ YAML และวิธีการ trigger แต่ละเหตุการณ์ การเขียน Workflow แรกนี้เป็นพื้นฐานสำคัญที่จะนำไปต่อยอดเป็น CI/CD pipeline ที่ซับซ้อนขึ้น:

# .github/workflows/hello.yml
name: Hello CI

# ─── Triggers ───
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

# ─── Jobs ───
jobs:
  hello:
    runs-on: ubuntu-latest        # ใช้ Ubuntu runner

    steps:
      - name: Checkout code
        uses: actions/checkout@v4  # ดึง code จาก repo

      - name: Say hello
        run: echo "Hello from GitHub Actions! 🤖"

      - name: Show info
        run: |
          echo "Event: ${{ github.event_name }}"
          echo "Branch: ${{ github.ref_name }}"
          echo "Commit: ${{ github.sha }}"
          echo "Actor: ${{ github.actor }}"
💡 ไฟล์ต้องอยู่ใน .github/workflows/ เท่านั้น GitHub จะ detect อัตโนมัติ!

จาก Workflow ตัวอย่างข้างต้น เราจะเห็นองค์ประกอบสำคัญหลายอย่าง: การกำหนด triggers ผ่าน on, การระบุ runner ด้วย runs-on, การใช้ Actions จาก Marketplace เช่น actions/checkout, และการใช้ GitHub context variables อย่าง ${{ github.actor }} สิ่งเหล่านี้เป็นพื้นฐานที่เราจะใช้ใน Workflow ที่ซับซ้อนขึ้น แล้วก็สิ่งสำคัญคือการ organize โครงสร้างไฟล์ให้อยู่ใน folder ที่ถูกต้อง


🚀 Real-World CI/CD Pipeline

หลังจากลองเขียน Workflow พื้นฐานแล้ว ตอนนี้มาดู production-grade CI/CD pipeline ที่ใช้จริงในงาน pipeline นี้จะครอบคลุมตั้งแต่การทดสอบ, build Docker image, ไปจนถึงการ deploy แต่ละ job จะมี dependency กันและมีการ pass ข้อมูลระหว่างกัน การออกแบบ pipeline ดังนี้จึงต้องคำนึงถึงประสิทธิภาพ, ความปลอดภัย, และการจัดการ secrets อย่างถูกต้อง

Node.js — Test + Build + Deploy

# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

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

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

jobs:
  # ─── Job 1: Test ───
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'                    # cache node_modules

      - name: Install dependencies
        run: npm ci                        # clean install (ใช้ lockfile)

      - name: Lint
        run: npm run lint

      - name: Unit Tests
        run: npm test -- --coverage

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

  # ─── Job 2: Build Docker Image ───
  build:
    needs: test                            # รอ test ผ่านก่อน
    runs-on: ubuntu-latest
    if: github.event_name == 'push'        # build เฉพาะ push (ไม่ build PR)

    permissions:
      contents: read
      packages: write

    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}

    steps:
      - uses: actions/checkout@v4

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

      - name: Docker metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=
            type=ref,event=branch

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

  # ─── Job 3: Deploy ───
  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'    # deploy เฉพาะ main
    environment: production                 # require approval

    steps:
      - uses: actions/checkout@v4

      - name: Deploy to production
        run: |
          echo "Deploying ${{ needs.build.outputs.image-tag }}"
          # kubectl set image deployment/app app=$IMAGE_TAG
          # หรือ update ArgoCD manifests

ตัวอย่าง Workflow ข้างต้นแสดงให้เห็นถึง production-grade CI/CD pipeline ที่มีความซับซ้อนและครอบคลุม โดยแบ่งออกเป็น 3 jobs หลัก: test, build, และ deploy แต่ละ job มี responsibility ที่ชัดเจน และใช้ needs เพื่อกำหนดลำดับการทำงาน การใช้ if conditions ช่วยให้เราควบคุมได้ว่า job ไหนควรรันเมื่อไร และการใช้ environment ช่วยให้เรามี approval gate สำหรับ production deployment

CI/CD Pipeline Flow
🧪 Test Job
Lint
Unit Tests
Coverage
→
🐳 Build Job
Docker Build
Push to GHCR
Cache layers
→
🚀 Deploy Job
Production env
Approval gate
Rollout

แผนภาพ architecture ข้างต้นแสดงให้เห็นการไหลของงานใน CI/CD pipeline อย่างชัดเจน เราจะเห็นว่า Test Job เป็น gatekeeper แรกที่ต้องผ่านก่อนถึงจะไปขั้นตอนถัดไป Build Job จะสร้าง Docker image และ push ไปยัง container registry พร้อมกับทำ caching เพื่อเพิ่มความเร็ว และ Deploy Job จะมี approval gate เพื่อความปลอดภัยใน production การออกแบบแบบนี้ช่วยให้เราจับ bugs ได้ตั้งแต่เนิ่นๆ และมั่นใจได้ว่าสิ่งที่ไป production มีคุณภาพ


🎯 Triggers — เมื่อไรจะรัน?

หนึ่งในสิ่งสำคัญที่สุดของ GitHub Actions คือการกำหนด triggers หรือเหตุการณ์ที่จะทำให้ Workflow ทำงาน การเลือก trigger ที่เหมาะสมจะส่งผลต่อประสิทธิภาพและต้นทุนของ CI/CD pipeline มาก เพราะ Workflow บางตัวอาจต้องการทำงานทุกครั้งที่มี code changes บางตัวอาจทำงานแค่ scheduled หรือเมื่อมี manual trigger เท่านั้น การเข้าใจ triggers แต่ละประเภทจะช่วยให้เราออกแบบ automation ที่เหมาะสมกับลักษณะงาน:

on:
  # Push to specific branches
  push:
    branches: [main, develop]
    paths:                               # รันเฉพาะเมื่อไฟล์เหล่านี้เปลี่ยน
      - 'src/**'
      - 'package.json'
    paths-ignore:
      - '**.md'                          # ไม่รันเมื่อแก้ docs

  # Pull Request
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened]

  # Schedule (cron)
  schedule:
    - cron: '0 2 * * 1'                 # ทุกวันจันทร์ 02:00 UTC

  # Manual trigger (ปุ่มกด)
  workflow_dispatch:
    inputs:
      environment:
        description: 'Deploy environment'
        required: true
        default: 'staging'
        type: choice
        options:
          - staging
          - production

  # Tag/Release
  release:
    types: [published]

  # Triggered by another workflow
  workflow_call:                          # reusable workflow

ตัวอย่าง triggers ข้างต้นแสดงให้เห็นถึงความยืดหยุ่นของ GitHub Actions ในการตอบสนองต่อเหตุการณ์ต่างๆ การใช้ paths และ paths-ignore ช่วยให้เราไม่เสียเวลาไปรัน CI เมื่อแก้แค่ documentation การใช้ schedule เหมาะสำหรับ nightly builds หรือ security scans ส่วน workflow_dispatch ให้ flexibility ในการ trigger แบบ manual พร้อม parameters การเลือก triggers ที่เหมาะสมจะช่วยลดการใช้ minutes และทำให้ development workflow ราบรื่นขึ้น


🔐 Secrets & Variables

เมื่อเราต้องทำ deployment หรือเชื่อมต่อกับ external services เราจำเป็นต้องใช้ credentials และ sensitive data ต่างๆ GitHub Actions มี secrets management ที่ช่วยให้เราเก็บข้อมูลเหล่านี้อย่างปลอดภัย โดยไม่ต้องเขียน hardcode ไว้ใน source code การจัดการ secrets อย่างถูกต้องเป็นสิ่งสำคัญมากสำหรับความปลอดภัยของระบบ และยังช่วยให้เราแยก configuration ระหว่าง environment ต่างๆ ได้อย่างชัดเจน

# ตั้ง Secrets ใน GitHub:
# Settings → Secrets and variables → Actions → New repository secret

# ใช้ใน workflow:
jobs:
  deploy:
    steps:
      - name: Deploy
        env:
          API_KEY: ${{ secrets.API_KEY }}
          DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
        run: |
          echo "Deploying with secret credentials..."
          # $API_KEY และ $DB_PASSWORD ใช้ได้ใน step นี้
          # ⚠️ GitHub จะ mask secrets ใน logs อัตโนมัติ

      - name: Use GitHub Token
        # GITHUB_TOKEN สร้างให้อัตโนมัติ ไม่ต้องตั้งเอง!
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh pr list
          gh release create v1.0.0

จากตัวอย่างข้างต้น เราจะเห็นว่า GitHub สร้าง GITHUB_TOKEN ให้อัตโนมัติในทุก Workflow run ซึ่งช่วยให้เราเรียกใช้ GitHub API ได้โดยไม่ต้องสร้าง token เอง การ mask secrets ใน logs เป็นฟีเจอร์ที่สำคัญมาก เพราะป้องกันไม่ให้ sensitive data รั่วไหลแม้ว่าจะมีการ echo หรือ print ออกมาโดยไม่ตั้งใจ การใช้ secrets ในลักษณะนี้ทำให้เรามั่นใจได้ว่า credentials จะอยู่ในระดับการป้องกันที่เหมาะสม

Environment Secrets

นอกจาก repository-level secrets แล้ว GitHub ยังมี environment-specific secrets ที่ช่วยให้เราแยก configuration ระหว่าง staging และ production ได้ Environment secrets มีความสำคัญมากเพราะช่วยให้เราใช้ credentials ที่แตกต่างกันสำหรับแต่ละ environment และยังสามารถตั้ง protection rules เช่น required reviewers หรือ deployment windows ได้อีกด้วย:

jobs:
  deploy-staging:
    environment: staging               # ใช้ secrets ของ staging
    steps:
      - run: echo "DB=${{ secrets.DATABASE_URL }}"
        # → ได้ staging DB URL

  deploy-production:
    environment: production            # ใช้ secrets ของ production
    # + สามารถตั้ง required reviewers ได้!
    steps:
      - run: echo "DB=${{ secrets.DATABASE_URL }}"
        # → ได้ production DB URL (คนละค่ากับ staging)

Environment secrets เป็นเครื่องมือที่ทรงพลังมากสำหรับการแยก configuration ระหว่าง environments ต่างๆ ไม่เพียงแต่ช่วยในเรื่องความปลอดภัยเท่านั้น แต่ยังช่วยให้เรามีการควบคุมการ deploy ได้ดียิ่งขึ้น เช่น การตั้ง required reviewers สำหรับ production environment หรือการจำกัดเวลาที่สามารถ deploy ได้ ซึ่งเป็นสิ่งสำคัญสำหรับ enterprise environments ที่ต้องการ governance และ compliance


🔀 Matrix Strategy — ทดสอบหลาย versions

ในการพัฒนา software หนึ่งในความท้าทายที่พบบ่อยคือการรองรับ multiple platforms, languages versions, หรือ dependencies ต่างๆ การทดสอบแต่ละ combination แบบ manual จะใช้เวลานานมากและเสี่ยงต่อการพลาด GitHub Actions มี Matrix Strategy ที่ช่วยให้เราทดสอบหลาย combinations พร้อมกันอย่างอัตโนมัติ ซึ่งไม่เพียงแต่ประหยัดเวลาเท่านั้น แต่ยังช่วยให้เราจับ compatibility issues ได้เร็วขึ้น:

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        node-version: [18, 20, 22]
        # รัน 3 OS × 3 Node = 9 jobs พร้อมกัน!
      fail-fast: false                  # ไม่หยุดถ้า 1 job fail

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

  # Matrix with include/exclude
  test-python:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python: ['3.10', '3.11', '3.12']
        django: ['4.2', '5.0']
        exclude:
          - python: '3.10'
            django: '5.0'              # Django 5 ไม่รองรับ Python 3.10
        include:
          - python: '3.12'
            django: '5.0'
            experimental: true         # เพิ่ม flag พิเศษ

Matrix Strategy เป็นหนึ่งในฟีเจอร์ที่ทรงพลังที่สุดของ GitHub Actions เพราะช่วยให้เราครอบคลุม test scenarios ได้อย่างครอบคลุม การใช้ fail-fast: false ทำให้เราเห็นผลลัพธ์ของทุก combinations แม้ว่าบางตัวจะ fail การใช้ exclude และ include ช่วยให้เรา fine-tune matrix ได้ตามความต้องการจริง ซึ่งสำคัญมากเพราะ combinations บางตัวอาจไม่มี practical value หรือไม่รองรับจริง ดังนั้นการออกแบบ matrix ที่ดีต้องสมดุลระหว่างความครอบคลุมกับประสิทธิภาพ


📦 Caching — เร็วขึ้น 10x!

หนึ่งในปัญหาที่พบบ่อยใน CI/CD คือเวลารอที่นานเกินไป โดยเฉพาะขั้นตอนการ download และ install dependencies ซึ่งบางครั้งใช้เวลานานกว่าการทำงานจริง GitHub Actions มี caching mechanism ที่ช่วยลดเวลาเหล่านี้ได้อย่างมาก โดยการเก็บ dependencies ที่ download แล้วไว้ใช้ซ้ำ การใช้ caching อย่างถูกต้องสามารถลดเวลา CI/CD ได้จาก minutes ลงเหลือ seconds ทำให้ development feedback loop เร็วขึ้นมาก:

# ❌ ช้า: install ทุกครั้ง (2-3 นาที)
- run: npm ci

# ✅ เร็ว: cache node_modules (10 วินาที)
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'                        # built-in cache!

# หรือ cache manual
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-npm-

# Python
- uses: actions/setup-python@v5
  with:
    python-version: '3.12'
    cache: 'pip'

# Docker layer caching
- uses: docker/build-push-action@v5
  with:
    cache-from: type=gha
    cache-to: type=gha,mode=max

จากตัวอย่างข้างต้นจะเห็นว่า caching มีหลายรูปแบบ โดย setup actions ต่างๆ เช่น setup-node และ setup-python มี built-in caching ที่ใช้งานง่ายมาก แค่ระบุ cache: 'npm' หรือ cache: 'pip' ก็ได้ประโยชน์ทันที สำหรับ Docker caching การใช้ GitHub Actions cache (type=gha) จะช่วยให้ layers ที่ไม่เปลี่ยนแปลงไม่ต้อง build ใหม่ ซึ่งจะช่วยประหยัดเวลาและ bandwidth มากมาย การเข้าใจและใช้ caching ให้ถูกต้องเป็นหนึ่งใน quick wins ที่ทำให้ CI/CD pipeline เร็วขึ้นทันที


🧩 Marketplace Actions — อย่าเขียนเอง!

GitHub Marketplace มี actions สำเร็จรูปมากกว่า 20,000 ตัว — ใช้เลย ไม่ต้องเขียนเอง:

แทนที่จะเสียเวลาเขียน scripts หรือ commands ซ้ำๆ เราสามารถใช้ Actions ที่ community พัฒนาและทดสอบแล้ว ซึ่งไม่เพียงแต่ประหยัดเวลาเท่านั้น แต่ยังได้ความน่าเชื่อถือและ maintainability ที่ดีกว่า การเลือกใช้ Actions ที่มีคนใช้เยอะและมี active maintenance จะช่วยลดความเสี่ยงในระยะยาว นี่คือ Actions ยอดนิยมที่ควรรู้จัก:

หมวดActionทำอะไร
Checkoutactions/checkout@v4ดึง code จาก repo
Node.jsactions/setup-node@v4ติดตั้ง Node.js
Pythonactions/setup-python@v5ติดตั้ง Python
Dockerdocker/build-push-action@v5Build + push image
Cacheactions/cache@v4Cache dependencies
Coveragecodecov/codecov-action@v4Upload test coverage
Deployaws-actions/configure-aws-credentials@v4AWS authentication
Releasesoftprops/action-gh-release@v2สร้าง GitHub Release
Slackslackapi/slack-github-action@v1ส่ง notification
Securitygithub/codeql-action@v3Code scanning (SAST)
# Pin action versions ด้วย SHA (security best practice)
# ❌ ไม่ปลอดภัย
- uses: actions/checkout@v4

# ✅ ปลอดภัยกว่า (pin SHA)
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

ตารางข้างต้นแสดง Actions ที่ใช้บ่อยที่สุด แต่ละตัวเฉพาะทางและมี maintainers ที่เชี่ยวชาญ เช่น actions/setup-node ทีม GitHub เองดูแล หรือ docker/build-push-action ทีม Docker ดูแล การใช้ Actions เหล่านี้ช่วยให้เราได้ best practices และ optimizations ที่ community สั่งสมมา แต่สิ่งสำคัญคือการ pin versions ด้วย SHA เพื่อป้องกัน supply chain attacks ที่อาจเกิดขึ้นหากมีคนแก้ไข Actions ตัวเดิม


♻️ Reusable Workflows — DRY!

เมื่อเรามี repositories หลายตัวหรือ workflows ที่มี patterns คล้ายๆ กัน การ copy-paste YAML จะกลายเป็นปัญหาในการ maintain Reusable Workflows เป็นแนวทางแก้ปัญหานี้โดยการสร้าง workflow template ที่สามารถเรียกใช้ซ้ำได้ พร้อม parameters และ secrets ที่ยืดหยุ่น การใช้ reusable workflows ช่วยให้เรามี central point สำหรับการ update และ improvement แทนที่จะต้องแก้ในทุก repository:

# .github/workflows/reusable-deploy.yml
# Workflow ที่ re-use ได้
name: Deploy

on:
  workflow_call:                         # เปิดให้เรียกจาก workflow อื่น
    inputs:
      environment:
        required: true
        type: string
      image-tag:
        required: true
        type: string
    secrets:
      DEPLOY_KEY:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to ${{ inputs.environment }}
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
        run: |
          echo "Deploying ${{ inputs.image-tag }} to ${{ inputs.environment }}"

จาก reusable workflow ข้างต้น เราจะเห็นการใช้ workflow_call ที่ทำให้ workflow นี้สามารถถูกเรียกจาก workflow อื่นได้ การกำหนด inputs และ secrets ทำให้ workflow มี flexibility ในการรับ parameters ต่างๆ ขณะเดียวกันก็รักษา security ไว้ได้ การออกแบบ reusable workflow ที่ดีควรมี clear interface และ handle edge cases ต่างๆ เพื่อให้ใช้งานได้อย่างน่าเชื่อถือ

# .github/workflows/main.yml
# เรียกใช้ reusable workflow
name: Main Pipeline

on:
  push:
    branches: [main]

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

  deploy-staging:
    needs: test
    uses: ./.github/workflows/reusable-deploy.yml    # เรียก reusable!
    with:
      environment: staging
      image-tag: ${{ github.sha }}
    secrets:
      DEPLOY_KEY: ${{ secrets.STAGING_DEPLOY_KEY }}

  deploy-production:
    needs: deploy-staging
    uses: ./.github/workflows/reusable-deploy.yml    # เรียกซ้ำ!
    with:
      environment: production
      image-tag: ${{ github.sha }}
    secrets:
      DEPLOY_KEY: ${{ secrets.PROD_DEPLOY_KEY }}

ตัวอย่างการใช้ reusable workflow ข้างต้นแสดงให้เห็นถึงประโยชน์ที่ชัดเจน เราสามารถเรียกใช้ workflow เดียวกันสำหรับ staging และ production โดยเปลี่ยนแค่ parameters และ secrets การใช้ needs ช่วยให้เราควบคุมลำดับการ deploy ได้ และการแยก secrets ระหว่าง environments ช่วยให้เรามี appropriate access control reusable workflows นี้เป็น foundation ที่ดีสำหรับการสร้าง standardized deployment processes ทั่วทั้ง organization


🔒 Security Scanning

ในยุคที่ security threats เพิ่มมากขึ้นเรื่อยๆ การทำ security scanning ใน CI/CD pipeline กลายเป็นสิ่งจำเป็น GitHub Actions มี integrations กับ security tools หลากหลายที่ช่วยให้เราจับ vulnerabilities ได้ตั้งแต่ระยะ development ไม่ว่าจะเป็น static analysis, dependency scanning, container scanning, หรือ secret scanning การรัน security checks อย่างอัตโนมัติช่วยให้เรา shift left ในการจัดการความปลอดภัย และลดความเสี่ยงที่จะพบปัญหาใน production:

# .github/workflows/security.yml
name: Security

on:
  push:
    branches: [main]
  schedule:
    - cron: '0 6 * * 1'                 # scan ทุกวันจันทร์

jobs:
  # Code scanning (SAST)
  codeql:
    runs-on: ubuntu-latest
    permissions:
      security-events: write
    steps:
      - uses: actions/checkout@v4
      - uses: github/codeql-action/init@v3
        with:
          languages: javascript
      - uses: github/codeql-action/analyze@v3

  # Dependency scanning
  dependency-review:
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v4
      - uses: actions/dependency-review-action@v4
        with:
          fail-on-severity: high

  # Container scanning
  trivy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build image
        run: docker build -t myapp:test .
      - uses: aquasecurity/trivy-action@master
        with:
          image-ref: myapp:test
          severity: CRITICAL,HIGH
          exit-code: 1                   # fail ถ้ามี critical/high

  # Secret scanning
  trufflehog:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0                 # full history
      - uses: trufflesecurity/trufflehog@main
        with:
          extra_args: --only-verified

จากตัวอย่าง security scanning workflow ข้างต้น เราจะเห็นการใช้ tools หลากหลาย: CodeQL สำหรับ static analysis, dependency review สำหรับตรวจสอบ dependencies ใหม่ใน PR, Trivy สำหรับ container scanning, และ TruffleHog สำหรับ secret scanning แต่ละ tool มี specialty และครอบคลุม attack vectors ที่แตกต่างกัน การรวม tools เหล่านี้เข้าด้วยกันจะให้ comprehensive security coverage และช่วยให้เรา catch issues ได้หลากหลายมากขึ้น


🏠 Self-hosted Runners

ถ้า GitHub-hosted runner ไม่พอ (เร็วไม่พอ, ต้องการ GPU, network access พิเศษ):

แม้ว่า GitHub-hosted runners จะสะดวกและใช้งานง่าย แต่บางครั้งเราอาจต้องการความยืดหยุ่นมากกว่านี้ เช่น การใช้ hardware พิเศษ, การเข้าถึง private networks, หรือการ customize environment แบบเจาะจง Self-hosted runners ให้ freedom ในการควบคุมทุกอย่าง แต่ก็มาพร้อมกับความรับผิดชอบในการ setup, maintain, และ secure infrastructure เอง การตัดสินใจเลือกระหว่าง GitHub-hosted และ self-hosted ต้องชั่งน้ำหนักระหว่าง convenience กับ control:

# ติดตั้ง self-hosted runner
# Settings → Actions → Runners → New self-hosted runner
# ทำตาม instructions: download, configure, run

# ใช้ใน workflow
jobs:
  build:
    runs-on: self-hosted                 # ใช้เครื่องตัวเอง!
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test

  # หรือ label specific
  gpu-test:
    runs-on: [self-hosted, gpu, linux]   # เครื่องที่มี GPU
    steps:
      - run: nvidia-smi                  # check GPU
      - run: python train.py

การ setup self-hosted runner ไม่ยากเมื่อทำตาม instructions ที่ GitHub ให้มา แต่สิ่งสำคัญคือการ label runners อย่างชัดเจนเพื่อให้ workflows สามารถเลือกเครื่องที่เหมาะสมได้ การมี self-hosted runners ช่วยให้เราทำงาน specialized tasks ได้ เช่น ML training ที่ต้องการ GPU, testing บน specific OS versions, หรือการเข้าถึง internal services ที่ไม่ได้ expose ไป public internet

GitHub-hosted vs Self-hosted

FeatureGitHub-hostedSelf-hosted
Setupไม่ต้องต้อง setup + maintain
CostFree tier (2,000 min/month)ค่าเครื่องเอง
Speed2-core, 7GB RAMตามที่มี (ได้ 64-core!)
CustomizationLimitedFull control
NetworkPublic internetPrivate network access
GPU❌✅
เหมาะกับทั่วไป, open sourceEnterprise, ML, heavy builds

ตารางเปรียบเทียบข้างต้นช่วยให้เราเห็นภาพชัดเจนของ trade-offs ระหว่างทั้งสองแนวทาง GitHub-hosted เหมาะสำหรับ standard workloads และ open source projects ที่ต้องการความสะดวกและไม่ต้องการ infrastructure overhead ส่วน self-hosted เหมาะสำหรับ enterprise environments ที่ต้องการ control, performance, หรือมี specific requirements การเลือกใช้แบบผสมผสานก็เป็นไปได้ เช่น ใช้ GitHub-hosted สำหรับ testing และ self-hosted สำหรับ deployment


💡 Advanced Patterns

หลังจากเข้าใจพื้นฐานของ GitHub Actions แล้ว มาดู patterns ขั้นสูงที่จะช่วยให้เรา handle complex scenarios ได้ดีขึ้น การใช้ conditional logic, artifact management, และ concurrency control จะช่วยให้ workflows มี robustness และ efficiency มากขึ้น patterns เหล่านี้มีประโยชน์มากสำหรับ production workflows ที่ต้องรองรับหลากหลายสถานการณ์และมี reliability requirements สูง:

Conditional Steps

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

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

  - name: Comment on PR
    if: github.event_name == 'pull_request'
    uses: actions/github-script@v7
    with:
      script: |
        github.rest.issues.createComment({
          issue_number: context.issue.number,
          owner: context.repo.owner,
          repo: context.repo.repo,
          body: '✅ Tests passed! Ready to merge.'
        })

  - name: Notify on failure
    if: failure()                         # รันเมื่อ step ก่อนหน้า fail
    run: |
      curl -X POST "${{ secrets.SLACK_WEBHOOK }}" \
        -d '{"text":"❌ CI failed on ${{ github.ref }}"}'

Conditional steps เป็น pattern ที่ใช้บ่อยมากใน production workflows เพราะช่วยให้เราควบคุมการทำงานตาม context ต่างๆ ได้ การใช้ if กับ GitHub context variables ช่วยให้เราสร้าง smart workflows ที่ตอบสนองต่อสถานการณ์ต่างๆ อย่างเหมาะสม การใช้ failure() และ status functions อื่นๆ ช่วยให้เรา handle error cases ได้อย่างมีประสิทธิภาพ และการ integrate กับ external services เช่น Slack หรือ GitHub API ทำให้เรามี rich automation ที่ตอบสนองต่อเหตุการณ์ต่างๆ

Artifacts — ส่งไฟล์ระหว่าง Jobs

ในหลายกรณี เราต้องการส่งผลลัพธ์จาก job หนึ่งไปยังอีก job หนึ่ง เช่น build artifacts, test results, หรือ generated files GitHub Actions มี artifact system ที่ช่วยให้เราทำสิ่งนี้ได้อย่างปลอดภัยและมีประสิทธิภาพ การใช้ artifacts ไม่เพียงแต่ช่วยในการ pass data ระหว่าง jobs เท่านั้น แต่ยังเป็นวิธีที่ดีในการ store build outputs สำหรับการ debug หรือ manual verification:

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

      - name: Upload build
        uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist/
          retention-days: 7

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Download build
        uses: actions/download-artifact@v4
        with:
          name: build-output
          path: dist/

      - name: Deploy
        run: aws s3 sync dist/ s3://my-bucket/

Artifacts เป็นเครื่องมือที่ทรงพลังสำหรับการ coordinate ระหว่าง jobs การใช้ upload-artifact และ download-artifact ช่วยให้เราแยก concerns ระหว่าง building และ deploying ได้อย่างชัดเจน การตั้ง retention-days ช่วยลดการใช้ storage และ cost ส่วนการ name artifacts อย่างมีความหมายจะช่วยให้ debug และ troubleshoot ได้ง่ายขึ้น artifacts ยังเป็นประโยชน์สำหรับการ store test results หรือ logs ที่ต้องการ review ภายหลัง

Concurrency — ป้องกัน deploy ซ้อน

เมื่อมีหลายคนทำงานใน team เดียวกัน หรือมีการ trigger workflows บ่อยๆ อาจเกิดปัญหาการ deploy ซ้อนกัน ซึ่งอาจทำให้เกิด conflicts หรือ inconsistent state Concurrency control ช่วยให้เราจัดการปัญหานี้โดยการจำกัดจำนวน workflows ที่สามารถทำงานพร้อมกันได้ และการ cancel workflows เก่าเมื่อมีตัวใหม่:

name: Deploy
concurrency:
  group: deploy-${{ github.ref }}        # 1 deploy per branch
  cancel-in-progress: true               # cancel ตัวเก่า ถ้ามีตัวใหม่

# ป้องกัน: push 3 ครั้งติดกัน → รันแค่ตัวล่าสุด

Concurrency control เป็น pattern ที่สำคัญมากสำหรับ production environments การใช้ group ช่วยให้เรา isolate concurrency ตาม context ต่างๆ เช่น branch หรือ environment การใช้ cancel-in-progress ช่วยประหยัด resources และลดเวลารอ แต่ต้องระวังในกรณีที่การ cancel อาจทำให้เกิดปัญหา เช่น partial deployments ดังนั้นการออกแบบ concurrency strategy ต้องคำนึงถึง nature ของงานและ consequences ที่อาจเกิดขึ้น


📋 Best Practices

การเขียน GitHub Actions ที่ดีไม่ใช่แค่ให้ทำงานได้เท่านั้น แต่ต้องคำนึงถึงความปลอดภัย, ประสิทธิภาพ, maintainability, และ cost optimization ด้วย best practices เหล่านี้เป็นผลสะสมจากประสบการณ์ของ community และ lessons learned จาก production environments การปฏิบัติตาม principles เหล่านี้จะช่วยให้ workflows ของเรามี reliability สูงและลดปัญหาในระยะยาว:

  1. Pin action versions — ใช้ SHA แทน tag (@v4 → @sha)
  2. Use npm ci ไม่ใช่ npm install — ใช้ lockfile, เร็วกว่า, deterministic
  3. Cache dependencies — ลดเวลา install จาก 2 นาที → 10 วินาที
  4. Fail fast — lint ก่อน → unit test → integration → deploy
  5. Use environments — แยก secrets staging/production + require approvals
  6. Limit permissions — permissions: contents: read ไม่ให้ write ถ้าไม่จำเป็น
  7. Concurrency control — ป้องกัน deploy ซ้อนกัน
  8. Reusable workflows — DRY! อย่า copy-paste workflow
  9. Timeout — ตั้ง timeout-minutes: 15 ป้องกัน stuck jobs
  10. Status badges — ใส่ใน README ให้รู้สถานะ CI
# README.md — CI badge
![CI](https://github.com/myorg/myapp/actions/workflows/ci.yml/badge.svg)

Best practices ข้างต้นไม่ใช่แค่ guidelines เท่านั้น แต่เป็นสิ่งที่จำเป็นสำหรับ production-grade workflows การ pin action versions ป้องกัน supply chain attacks การ cache dependencies ลด build time อย่างมาก การใช้ environments และ permissions อย่างถูกต้องช่วยในเรื่องความปลอดภัย และการมี status badges ช่วยให้ทีมเห็นสถานะ CI/CD ได้ทันที การปฏิบัติตาม best practices เหล่านี้จะทำให้เรามี confidence ในระบบ automation และลดเวลาที่เสียไปกับการแก้ปัญหา


💰 Pricing — ฟรีแค่ไหน?

การเข้าใจ pricing model ของ GitHub Actions เป็นสิ่งสำคัญสำหรับการวางแผนและการควบคุมต้นทุน โดยเฉพาะสำหรับ organizations ที่มี repositories เยอะ หรือ workflows ที่รันบ่อยๆ การรู้จัก limits และ optimization techniques จะช่วยให้เราใช้ GitHub Actions ได้อย่างมีประสิทธิภาพโดยไม่เกิน budget ที่ตั้งไว้:

PlanMinutes/monthStorage
Free2,000 min500 MB
Team3,000 min2 GB
Enterprise50,000 min50 GB
Public repos∞ (ไม่จำกัด!)∞
💡 Open source projects ได้ unlimited minutes ฟรี! — อีกเหตุผลที่ open source นิยม GitHub Actions

จากตารางราคาข้างต้น เราจะเห็นว่า GitHub Actions มี generous free tier ที่เหมาะสำหรับ small teams และ personal projects การที่ public repositories ได้ unlimited minutes ทำให้ open source community สามารถใช้ powerful CI/CD ได้โดยไม่ต้องกังวลเรื่องค่าใช้จ่าย สำหรับ enterprise ที่ต้องการ private repositories และ heavy usage ก็มี paid plans ที่ให้ resources มากขึ้น การวางแผนการใช้งานและ optimize workflows จะช่วยให้เราได้ประโยชน์สูงสุดจาก free tier


สรุป

GitHub Actions เป็นหนึ่งใน game-changing tools ที่ทำให้ CI/CD accessibility เพิ่มขึ้นมาก การที่ integrated กับ GitHub repository โดยตรงทำให้ setup และ maintenance ง่ายกว่า traditional CI/CD tools มาก ประกอบกับ rich ecosystem ของ marketplace actions ทำให้เราสามารถสร้าง sophisticated automation ได้โดยไม่ต้องเขียน code มากนัก สิ่งสำคัญคือการเริ่มต้นง่ายๆ แล้วค่อยๆ เพิ่ม complexity เมื่อความต้องการมากขึ้น:

  • GitHub Actions = CI/CD built-in GitHub — เขียน YAML, รันอัตโนมัติ
  • Workflow = Event → Jobs → Steps — ไฟล์อยู่ใน .github/workflows/
  • Triggers = push, PR, schedule, manual, release, workflow_call
  • Secrets = เก็บ credentials ปลอดภัย + Environment secrets แยก staging/prod
  • Matrix = ทดสอบหลาย OS × versions พร้อมกัน
  • Caching = เร็วขึ้น 10x ด้วย dependency cache
  • Marketplace = 20,000+ actions สำเร็จรูป อย่าเขียนเอง!
  • Reusable Workflows = DRY! เรียก workflow ซ้ำได้
  • Security = CodeQL, Trivy, dependency review, secret scanning
  • Self-hosted = เครื่องตัวเอง สำหรับ GPU/heavy builds 🤖🐕

In the CI/CD Pipeline post we covered the general ideas behind CI/CD. But if you ask which CI/CD tool is the most popular right now? the answer is GitHub Actions — free, built right into GitHub, and no separate server to set up!

Today we go through GitHub Actions in detail, from the basics all the way to production-grade workflows 🤖🐕


🤖 What Is GitHub Actions?

GitHub Actions is a CI/CD platform built into GitHub — you write the workflow as YAML and GitHub runs it for you automatically:

GitHub Actions works through a clear sequence of stages, starting the moment a developer pushes code and ending with the final deploy. Each stage plays a different role, and together they let us control the CI/CD process in fine detail and in safety. Here is the whole process at a glance:

📝 git push
Trigger event
→
⚙️ Workflow
.github/workflows/
→
🏃 Jobs
Run on runners
→
✅ Steps
Actions + commands

The diagram shows that once code is pushed, GitHub Actions starts the process automatically by reading the Workflow file we wrote. Jobs then run on a Runner (a virtual machine) and carry out the Steps we defined. That flow is why we can be confident that every commit is checked and deployed correctly.

Key Concepts

Before writing our first Workflow we need to understand the core concepts of GitHub Actions, because each one has a distinct role and a clear relationship to the others. Understanding them lets us design an efficient CI/CD pipeline — and repair it when something goes wrong:

ConceptWhat it isExample
WorkflowThe YAML file that defines the automation.github/workflows/ci.yml
EventThe thing that triggers a workflowpush, pull_request, schedule
JobA set of steps that run on a single runnerbuild, test, deploy
StepOne command or one action at a timerun: npm test
ActionReusable unit of codeactions/checkout@v4
RunnerThe machine that runs a jobubuntu-latest, self-hosted

The table above shows that GitHub Actions has a hierarchical structure: a Workflow is the top level and contains several Jobs, each Job contains several Steps, and each Step can be either an Action or an ordinary command. Once that structure is clear, even a complicated Workflow can be written in an orderly way.


📝 Your First Workflow — Hello GitHub Actions

Now that the core concepts are clear, let us write a first Workflow. We start with something simple that prints basic information, which is enough to show how a YAML file is structured and how each event triggers it. This first Workflow is the foundation we will build on as the CI/CD pipeline grows more complex:

# .github/workflows/hello.yml
name: Hello CI

# ─── Triggers ───
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

# ─── Jobs ───
jobs:
  hello:
    runs-on: ubuntu-latest        # use the Ubuntu runner

    steps:
      - name: Checkout code
        uses: actions/checkout@v4  # pull the code from the repo

      - name: Say hello
        run: echo "Hello from GitHub Actions! 🤖"

      - name: Show info
        run: |
          echo "Event: ${{ github.event_name }}"
          echo "Branch: ${{ github.ref_name }}"
          echo "Commit: ${{ github.sha }}"
          echo "Actor: ${{ github.actor }}"
💡 The file must live in .github/workflows/ and nowhere else — GitHub detects it automatically!

The example Workflow above shows several important pieces: declaring triggers through on, naming the runner with runs-on, using Actions from the Marketplace such as actions/checkout, and reading GitHub context variables like ${{ github.actor }}. These are the foundations we will reuse in more complex Workflows — and the other essential is organizing the files into the correct folder.


🚀 Real-World CI/CD Pipeline

After that basic Workflow, let us look at a production-grade CI/CD pipeline of the kind used in real work. This pipeline covers everything from testing to building a Docker image to deploying. Each job depends on the ones before it and passes data along, so designing a pipeline like this means thinking about performance, security, and correct handling of secrets.

Node.js — Test + Build + Deploy

# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

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

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

jobs:
  # ─── Job 1: Test ───
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'                    # cache node_modules

      - name: Install dependencies
        run: npm ci                        # clean install (uses the lockfile)

      - name: Lint
        run: npm run lint

      - name: Unit Tests
        run: npm test -- --coverage

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

  # ─── Job 2: Build Docker Image ───
  build:
    needs: test                            # wait for test to pass first
    runs-on: ubuntu-latest
    if: github.event_name == 'push'        # build on push only (never on a PR)

    permissions:
      contents: read
      packages: write

    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}

    steps:
      - uses: actions/checkout@v4

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

      - name: Docker metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=
            type=ref,event=branch

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

  # ─── Job 3: Deploy ───
  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'    # deploy from main only
    environment: production                 # require approval

    steps:
      - uses: actions/checkout@v4

      - name: Deploy to production
        run: |
          echo "Deploying ${{ needs.build.outputs.image-tag }}"
          # kubectl set image deployment/app app=$IMAGE_TAG
          # or update the ArgoCD manifests

The Workflow above is a production-grade CI/CD pipeline — complex and comprehensive — split into three main jobs: test, build, and deploy. Each job has a clear responsibility, and needs sets the order in which they run. The if conditions let us control which job runs when, and environment gives us an approval gate for the production deployment.

CI/CD Pipeline Flow
🧪 Test Job
Lint
Unit Tests
Coverage
→
🐳 Build Job
Docker Build
Push to GHCR
Cache layers
→
🚀 Deploy Job
Production env
Approval gate
Rollout

The architecture diagram above makes the flow of work through the CI/CD pipeline plain. The Test Job is the first gatekeeper and must pass before anything else proceeds. The Build Job creates the Docker image and pushes it to the container registry, caching layers along the way for speed. The Deploy Job has an approval gate for safety in production. A design like this catches bugs early and gives us confidence that whatever reaches production has been checked.


🎯 Triggers — When Does It Run?

One of the most important parts of GitHub Actions is deciding on the triggers — the events that make a Workflow run. Choosing the right trigger has a large effect on both the performance and the cost of a CI/CD pipeline, because some Workflows need to run on every code change, while others should run only on a schedule or from a manual trigger. Knowing each kind of trigger helps us design automation that fits the shape of the work:

on:
  # Push to specific branches
  push:
    branches: [main, develop]
    paths:                               # run only when these files change
      - 'src/**'
      - 'package.json'
    paths-ignore:
      - '**.md'                          # skip when only the docs change

  # Pull Request
  pull_request:
    branches: [main]
    types: [opened, synchronize, reopened]

  # Schedule (cron)
  schedule:
    - cron: '0 2 * * 1'                 # every Monday at 02:00 UTC

  # Manual trigger (a button)
  workflow_dispatch:
    inputs:
      environment:
        description: 'Deploy environment'
        required: true
        default: 'staging'
        type: choice
        options:
          - staging
          - production

  # Tag/Release
  release:
    types: [published]

  # Triggered by another workflow
  workflow_call:                          # reusable workflow

The triggers above show how flexibly GitHub Actions can respond to different events. Using paths and paths-ignore means we do not burn CI time on a documentation-only change. schedule suits nightly builds or security scans, while workflow_dispatch gives us the flexibility of a manual trigger with parameters. Choosing the right triggers reduces the minutes we spend and makes the development workflow smoother.


🔐 Secrets & Variables

When we deploy or connect to external services we need credentials and other sensitive data. GitHub Actions has secrets management that lets us store these safely, without hardcoding them into the source. Handling secrets correctly matters enormously for the security of the system, and it also lets us cleanly separate configuration between environments.

# Set Secrets in GitHub:
# Settings → Secrets and variables → Actions → New repository secret

# Use them in a workflow:
jobs:
  deploy:
    steps:
      - name: Deploy
        env:
          API_KEY: ${{ secrets.API_KEY }}
          DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
        run: |
          echo "Deploying with secret credentials..."
          # $API_KEY and $DB_PASSWORD are available inside this step
          # ⚠️ GitHub masks secrets in the logs automatically

      - name: Use GitHub Token
        # GITHUB_TOKEN is created for you automatically — you never set it!
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh pr list
          gh release create v1.0.0

The example above shows that GitHub creates GITHUB_TOKEN automatically for every Workflow run, so we can call the GitHub API without minting a token ourselves. Masking secrets in the logs is a very important feature, because it stops sensitive data leaking even when something is echoed or printed by accident. Using secrets this way gives us confidence that credentials sit behind an appropriate level of protection.

Environment Secrets

Beyond repository-level secrets, GitHub also has environment-specific secrets that let us separate configuration between staging and production. Environment secrets matter a great deal, because they let us use different credentials for each environment and also let us set protection rules such as required reviewers or deployment windows:

jobs:
  deploy-staging:
    environment: staging               # uses the staging secrets
    steps:
      - run: echo "DB=${{ secrets.DATABASE_URL }}"
        # → returns the staging DB URL

  deploy-production:
    environment: production            # uses the production secrets
    # + you can require reviewers here!
    steps:
      - run: echo "DB=${{ secrets.DATABASE_URL }}"
        # → returns the production DB URL (a different value from staging)

Environment secrets are a very powerful tool for separating configuration between environments. They help with more than security: they also give us better control over deploys — requiring reviewers for the production environment, say, or limiting the hours in which a deploy may happen, which matters for enterprise environments that need governance and compliance.


🔀 Matrix Strategy — Testing Many Versions

One of the recurring challenges in software development is supporting multiple platforms, language versions, or dependencies. Testing every combination by hand takes a very long time and is easy to get wrong. GitHub Actions has a Matrix Strategy that runs many combinations at once, automatically — which not only saves time but also catches compatibility issues sooner:

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        node-version: [18, 20, 22]
        # runs 3 OS × 3 Node = 9 jobs in parallel!
      fail-fast: false                  # do not stop when one job fails

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

  # Matrix with include/exclude
  test-python:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python: ['3.10', '3.11', '3.12']
        django: ['4.2', '5.0']
        exclude:
          - python: '3.10'
            django: '5.0'              # Django 5 does not support Python 3.10
        include:
          - python: '3.12'
            django: '5.0'
            experimental: true         # add an extra flag

Matrix Strategy is one of the most powerful features in GitHub Actions, because it gives us broad coverage of test scenarios. Setting fail-fast: false means we see the result of every combination even when some of them fail. exclude and include let us fine-tune the matrix to what we actually need — which matters, because some combinations have no practical value or are genuinely unsupported. A good matrix balances coverage against efficiency.


📦 Caching — 10x Faster!

A common complaint about CI/CD is the waiting, especially the step that downloads and installs dependencies — which sometimes takes longer than the real work. GitHub Actions has a caching mechanism that cuts that time dramatically by keeping already-downloaded dependencies for reuse. Used correctly, caching takes a CI/CD run from minutes down to seconds, and makes the development feedback loop far faster:

# ❌ slow: install every time (2-3 minutes)
- run: npm ci

# ✅ fast: cache node_modules (10 seconds)
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: 'npm'                        # built-in cache!

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

# Python
- uses: actions/setup-python@v5
  with:
    python-version: '3.12'
    cache: 'pip'

# Docker layer caching
- uses: docker/build-push-action@v5
  with:
    cache-from: type=gha
    cache-to: type=gha,mode=max

The examples show that caching comes in several forms. The setup actions — setup-node, setup-python and friends — have built-in caching that is trivial to use: naming cache: 'npm' or cache: 'pip' is enough to get the benefit immediately. For Docker, using the GitHub Actions cache (type=gha) means unchanged layers never rebuild, which saves a great deal of time and bandwidth. Understanding and applying caching correctly is one of the quick wins that makes a CI/CD pipeline faster straight away.


🧩 Marketplace Actions — Don't Write Your Own!

The GitHub Marketplace holds more than 20,000 ready-made actions — use them, do not write your own:

Rather than spending time writing the same scripts and commands over and over, we can use Actions the community has already built and tested — which saves time and also gives us better reliability and maintainability. Choosing Actions that are widely used and actively maintained reduces the long-term risk. Here are the popular ones worth knowing:

CategoryActionWhat it does
Checkoutactions/checkout@v4Pull the code from the repo
Node.jsactions/setup-node@v4Install Node.js
Pythonactions/setup-python@v5Install Python
Dockerdocker/build-push-action@v5Build + push image
Cacheactions/cache@v4Cache dependencies
Coveragecodecov/codecov-action@v4Upload test coverage
Deployaws-actions/configure-aws-credentials@v4AWS authentication
Releasesoftprops/action-gh-release@v2Create a GitHub Release
Slackslackapi/slack-github-action@v1Send a notification
Securitygithub/codeql-action@v3Code scanning (SAST)
# Pin action versions to a SHA (security best practice)
# ❌ not safe
- uses: actions/checkout@v4

# ✅ safer (pinned SHA)
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

The table above lists the most frequently used Actions. Each is specialized and has expert maintainers — actions/setup-node is looked after by GitHub's own team, docker/build-push-action by Docker's. Using them gives us the best practices and optimizations the community has accumulated. What matters, though, is pinning versions to a SHA, to guard against the supply-chain attack that becomes possible if someone modifies an existing Action.


♻️ Reusable Workflows — DRY!

Once we have several repositories, or workflows that follow similar patterns, copy-pasting YAML becomes a maintenance problem. Reusable Workflows solve it by letting us build a workflow template that can be called again and again, with flexible parameters and secrets. They give us a single central place to update and improve, instead of editing every repository:

# .github/workflows/reusable-deploy.yml
# A workflow that can be re-used
name: Deploy

on:
  workflow_call:                         # allow other workflows to call this one
    inputs:
      environment:
        required: true
        type: string
      image-tag:
        required: true
        type: string
    secrets:
      DEPLOY_KEY:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to ${{ inputs.environment }}
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
        run: |
          echo "Deploying ${{ inputs.image-tag }} to ${{ inputs.environment }}"

In the reusable workflow above, workflow_call is what allows another workflow to call this one. Declaring inputs and secrets gives the workflow the flexibility to accept parameters while keeping its security intact. A well-designed reusable workflow should have a clear interface and handle its edge cases, so that it can be relied on.

# .github/workflows/main.yml
# Calling the reusable workflow
name: Main Pipeline

on:
  push:
    branches: [main]

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

  deploy-staging:
    needs: test
    uses: ./.github/workflows/reusable-deploy.yml    # call the reusable one!
    with:
      environment: staging
      image-tag: ${{ github.sha }}
    secrets:
      DEPLOY_KEY: ${{ secrets.STAGING_DEPLOY_KEY }}

  deploy-production:
    needs: deploy-staging
    uses: ./.github/workflows/reusable-deploy.yml    # call it again!
    with:
      environment: production
      image-tag: ${{ github.sha }}
    secrets:
      DEPLOY_KEY: ${{ secrets.PROD_DEPLOY_KEY }}

The example above makes the benefit plain: we call the same workflow for staging and for production, changing only the parameters and the secrets. needs lets us control the order of the deploys, and separating the secrets between environments gives us appropriate access control. Reusable workflows are a good foundation for standardized deployment processes across an entire organization.


🔒 Security Scanning

In an era of ever-growing security threats, security scanning in the CI/CD pipeline has become a necessity. GitHub Actions integrates with a wide range of security tools that help us catch vulnerabilities during development — static analysis, dependency scanning, container scanning, secret scanning. Running these checks automatically is how we shift security left, and how we reduce the risk of discovering the problem in production:

# .github/workflows/security.yml
name: Security

on:
  push:
    branches: [main]
  schedule:
    - cron: '0 6 * * 1'                 # scan every Monday

jobs:
  # Code scanning (SAST)
  codeql:
    runs-on: ubuntu-latest
    permissions:
      security-events: write
    steps:
      - uses: actions/checkout@v4
      - uses: github/codeql-action/init@v3
        with:
          languages: javascript
      - uses: github/codeql-action/analyze@v3

  # Dependency scanning
  dependency-review:
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v4
      - uses: actions/dependency-review-action@v4
        with:
          fail-on-severity: high

  # Container scanning
  trivy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build image
        run: docker build -t myapp:test .
      - uses: aquasecurity/trivy-action@master
        with:
          image-ref: myapp:test
          severity: CRITICAL,HIGH
          exit-code: 1                   # fail if anything is critical/high

  # Secret scanning
  trufflehog:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0                 # full history
      - uses: trufflesecurity/trufflehog@main
        with:
          extra_args: --only-verified

The security scanning workflow above uses a range of tools: CodeQL for static analysis, dependency review to check new dependencies in a PR, Trivy for container scanning, and TruffleHog for secret scanning. Each tool has its own specialty and covers a different set of attack vectors. Putting them together gives comprehensive security coverage and catches a much wider variety of issues.


🏠 Self-hosted Runners

When a GitHub-hosted runner is not enough (not fast enough, needs a GPU, needs special network access):

GitHub-hosted runners are convenient and easy to use, but sometimes we need more flexibility — special hardware, access to a private network, or a very specific customized environment. Self-hosted runners give us the freedom to control everything, but they also bring the responsibility of setting up, maintaining and securing that infrastructure ourselves. Choosing between GitHub-hosted and self-hosted means weighing convenience against control:

# Install a self-hosted runner
# Settings → Actions → Runners → New self-hosted runner
# Follow the instructions: download, configure, run

# Use it in the workflow
jobs:
  build:
    runs-on: self-hosted                 # your own machine!
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm test

  # or target specific labels
  gpu-test:
    runs-on: [self-hosted, gpu, linux]   # a machine that has a GPU
    steps:
      - run: nvidia-smi                  # check GPU
      - run: python train.py

Setting up a self-hosted runner is not hard if you follow GitHub's instructions, but what matters is labelling the runners clearly so that workflows can pick the right machine. Self-hosted runners are what let us do specialized work: ML training that needs a GPU, testing on specific OS versions, or reaching internal services that are not exposed to the public internet.

GitHub-hosted vs Self-hosted

FeatureGitHub-hostedSelf-hosted
SetupNoneYou set it up and maintain it
CostFree tier (2,000 min/month)Your own hardware bill
Speed2-core, 7GB RAMWhatever you have (64-core if you like!)
CustomizationLimitedFull control
NetworkPublic internetPrivate network access
GPU❌✅
Best forGeneral use, open sourceEnterprise, ML, heavy builds

The comparison above makes the trade-offs between the two approaches clear. GitHub-hosted suits standard workloads and open source projects that want convenience without infrastructure overhead. Self-hosted suits enterprise environments that need control, performance, or have specific requirements. A mixed approach is entirely possible too — GitHub-hosted for testing, self-hosted for deployment.


💡 Advanced Patterns

With the basics of GitHub Actions in hand, let us look at the advanced patterns that help us handle complex scenarios. Conditional logic, artifact management and concurrency control all make workflows more robust and more efficient. These patterns matter most in production workflows that have to cope with a wide range of situations and carry high reliability requirements:

Conditional Steps

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

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

  - name: Comment on PR
    if: github.event_name == 'pull_request'
    uses: actions/github-script@v7
    with:
      script: |
        github.rest.issues.createComment({
          issue_number: context.issue.number,
          owner: context.repo.owner,
          repo: context.repo.repo,
          body: '✅ Tests passed! Ready to merge.'
        })

  - name: Notify on failure
    if: failure()                         # runs when an earlier step fails
    run: |
      curl -X POST "${{ secrets.SLACK_WEBHOOK }}" \
        -d '{"text":"❌ CI failed on ${{ github.ref }}"}'

Conditional steps are a very common pattern in production workflows, because they let us control behavior by context. Using if with GitHub context variables lets us build smart workflows that respond appropriately to each situation. Using failure() and the other status functions lets us handle error cases efficiently, and integrating with external services such as Slack or the GitHub API gives us rich automation that reacts to whatever happens.

Artifacts — Passing Files Between Jobs

In many cases we need to hand results from one job to another — build artifacts, test results, generated files. GitHub Actions has an artifact system that does this safely and efficiently. Artifacts are not only a way to pass data between jobs; they are also a good way to store build output for debugging or manual verification:

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

      - name: Upload build
        uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist/
          retention-days: 7

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Download build
        uses: actions/download-artifact@v4
        with:
          name: build-output
          path: dist/

      - name: Deploy
        run: aws s3 sync dist/ s3://my-bucket/

Artifacts are a powerful tool for coordinating between jobs. upload-artifact and download-artifact let us separate the concerns of building and deploying cleanly. Setting retention-days reduces storage use and cost, and naming artifacts meaningfully makes debugging and troubleshooting much easier. Artifacts are also useful for storing test results or logs we want to review later.

Concurrency — Preventing Overlapping Deploys

When several people work in the same team, or workflows are triggered frequently, deploys can end up overlapping — which can cause conflicts or an inconsistent state. Concurrency control handles this by limiting how many workflows may run at once, and by cancelling older runs when a newer one arrives:

name: Deploy
concurrency:
  group: deploy-${{ github.ref }}        # 1 deploy per branch
  cancel-in-progress: true               # cancel the old run when a new one arrives

# prevents: 3 pushes in a row → only the latest one runs

Concurrency control is a very important pattern for production environments. group lets us isolate concurrency by context — by branch, or by environment. cancel-in-progress saves resources and cuts waiting time, but take care where cancelling could cause its own problems, such as a partial deployment. A concurrency strategy has to be designed with the nature of the work, and its consequences, in mind.


📋 Best Practices

Writing good GitHub Actions is not only about making them work: it also means thinking about security, performance, maintainability and cost optimization. The practices below are the accumulated experience of the community and the lessons learned in production environments. Following them gives our workflows high reliability and fewer problems in the long run:

  1. Pin action versions — use a SHA instead of a tag (@v4 → @sha)
  2. Use npm ci, not npm install — it uses the lockfile, it is faster, it is deterministic
  3. Cache dependencies — install time drops from 2 minutes → 10 seconds
  4. Fail fast — lint first → unit tests → integration → deploy
  5. Use environments — separate staging/production secrets + require approvals
  6. Limit permissions — permissions: contents: read, no write unless it is genuinely needed
  7. Concurrency control — stop deploys from overlapping
  8. Reusable workflows — DRY! Do not copy-paste a workflow
  9. Timeout — set timeout-minutes: 15 to guard against stuck jobs
  10. Status badges — put one in the README so everyone can see the CI status
# README.md — CI badge
![CI](https://github.com/myorg/myapp/actions/workflows/ci.yml/badge.svg)

These best practices are not merely guidelines; they are what production-grade workflows require. Pinning action versions guards against supply-chain attacks. Caching dependencies cuts build time dramatically. Using environments and permissions correctly is a matter of security. And status badges let the team see the state of CI/CD at a glance. Following them gives us confidence in our automation and saves the time we would otherwise lose to fixing problems.


💰 Pricing — How Free Is It?

Understanding the GitHub Actions pricing model matters for planning and cost control, especially for organizations with many repositories or workflows that run often. Knowing the limits and the optimization techniques is what lets us use GitHub Actions effectively without going over budget:

PlanMinutes/monthStorage
Free2,000 min500 MB
Team3,000 min2 GB
Enterprise50,000 min50 GB
Public repos∞ (unlimited!)∞
💡 Open source projects get unlimited minutes, free! — another reason open source has settled on GitHub Actions

The pricing table shows that GitHub Actions has a generous free tier, suitable for small teams and personal projects. Giving public repositories unlimited minutes is what lets the open source community use powerful CI/CD without worrying about the bill. For enterprises that need private repositories and heavy usage there are paid plans with more resources. Planning your usage and optimizing your workflows is how you get the most out of the free tier.


Summary

GitHub Actions is one of those game-changing tools that made CI/CD far more accessible. Being integrated directly into the GitHub repository makes it much easier to set up and maintain than traditional CI/CD tools, and the rich ecosystem of marketplace actions means we can build sophisticated automation without writing much code. The important thing is to start simple and add complexity only as the requirements grow:

  • GitHub Actions = CI/CD built into GitHub — write YAML, it runs automatically
  • Workflow = Event → Jobs → Steps — the file lives in .github/workflows/
  • Triggers = push, PR, schedule, manual, release, workflow_call
  • Secrets = store credentials safely + Environment secrets to separate staging/prod
  • Matrix = test many OS × versions at once
  • Caching = 10x faster with a dependency cache
  • Marketplace = 20,000+ ready-made actions — do not write your own!
  • Reusable Workflows = DRY! Call the same workflow again and again
  • Security = CodeQL, Trivy, dependency review, secret scanning
  • Self-hosted = your own machine, for GPU and heavy builds 🤖🐕
บทความจากซีรีส์ DevOps & Vibe Coding 2026From the DevOps & Vibe Coding 2026 series
← Previous
SRE — Site Reliability Engineering ที่ DevOps ต้องรู้
Next →
Code Quality & Review — เขียน Code ดีๆ ที่ทีมอยากอ่าน