ทำไมต้องไปต่อจากพื้นฐาน
ถ้าตั้ง GitHub Actions ครั้งแรก แล้วใช้สักพัก คุณจะเริ่มเจอปัญหา:
- CI ช้า —
npm installใช้ 2 นาทีทุกครั้ง - อยาก test หลาย Node version — ต้อง copy job ซ้ำ
- มี repo หลายตัวที่ workflow คล้ายกัน — copy ไปทุก repo เปลี่ยนทีต้องแก้ทุกที่
- อยากให้ deploy ต้อง approve ก่อน
บทความนี้แก้ปัญหาพวกนี้ทีละข้อ
1. Cache dependencies — ทำให้ CI เร็วขึ้น 5-10 เท่า
actions/setup-node มี cache built-in — เปิดด้วย option เดียว:
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # หรือ 'yarn', 'pnpm'
cache จะเก็บ ~/.npm ไว้ใน GitHub workflow ถัดไป hit cache ใช้เวลา 5-10 วินาทีแทน 60-120
ถ้าต้องการ cache อย่างอื่น (เช่น Next.js build cache):
- uses: actions/cache@v4
with:
path: |
.next/cache
~/.cache/Cypress
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
key ตรง = hit cache เต็ม ๆ
restore-keys ใช้เมื่อ key ไม่ตรงเป๊ะ = partial cache ดีกว่าไม่มี
2. Matrix — รัน test หลาย version พร้อมกัน
ทดสอบกับ Node 18, 20, 22 พร้อมกัน + แต่ละ OS:
jobs:
test:
strategy:
fail-fast: false # ตัวอื่นรันต่อได้ถ้ามีตัวพัง
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [18, 20, 22]
exclude:
- os: windows-latest
node: 18
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: 'npm'
- run: npm ci
- run: npm test
GitHub จะรันทุก combo (3 OS × 3 Node = 9 job) พร้อมกัน บน runner คนละตัว
fail-fast: false สำคัญ — default ถ้า job ใดพัง job อื่นจะถูก cancel
3. Conditional steps
steps:
- run: npm test
- name: Upload coverage
if: success() && matrix.node == '20' && matrix.os == 'ubuntu-latest'
run: npm run coverage:upload
- name: Notify on failure
if: failure()
run: |
curl -X POST $SLACK_WEBHOOK -d '{"text":"Build failed"}'
useful function:
success()— งานทั้งหมดก่อนหน้าผ่าน (default)failure()— มีบางอันล้มalways()— รันเสมอcancelled()— ถ้างานถูก cancel
4. Job dependencies
jobs:
lint:
runs-on: ubuntu-latest
steps: ...
test:
runs-on: ubuntu-latest
steps: ...
build:
needs: [lint, test] # รันต่อเมื่อทั้งสองผ่าน
runs-on: ubuntu-latest
steps: ...
deploy:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps: ...
graph แบบนี้:
lint ─┐
├─→ build → deploy
test ─┘
5. Artifacts — แชร์ไฟล์ระหว่าง job
build ใน job หนึ่ง ใช้ใน job อื่น:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- run: ./deploy.sh
6. Reusable workflow — แชร์ระหว่าง repo
สร้าง repo myorg/.github มี .github/workflows/node-ci.yml:
name: Reusable Node CI
on:
workflow_call:
inputs:
node-version:
type: string
default: '20'
secrets:
NPM_TOKEN:
required: false
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- run: npm ci
- run: npm test
ใช้จาก repo อื่นใน org เดียวกัน:
# .github/workflows/ci.yml ใน repo อื่น
jobs:
ci:
uses: myorg/.github/.github/workflows/node-ci.yml@main
with:
node-version: '20'
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
แก้ครั้งเดียวที่ repo .github ใช้ได้กับทุก repo
7. Composite actions — แชร์ steps
ถ้าแค่ 2-3 step ที่ใช้ซ้ำ ไม่ต้องสร้าง reusable workflow ใช้ composite action:
ที่ myorg/setup-app/action.yml:
name: Setup App
description: Checkout, install Node, install deps
inputs:
node-version:
default: '20'
runs:
using: composite
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- run: npm ci
shell: bash
ใช้:
- uses: myorg/setup-app@v1
with:
node-version: '20'
8. Environments — ต้อง approve ก่อน deploy
ตั้งใน Settings → Environments → New environment
- ชื่อ "production"
- เปิด Required reviewers เลือกคนที่อนุมัติได้
- จำกัดเฉพาะ branch
main
ใช้ใน workflow:
deploy-prod:
runs-on: ubuntu-latest
environment:
name: production
url: https://yourdomain.com
steps:
- run: ./deploy-prod.sh
push เข้า main → workflow รันถึง deploy-prod → รอคนกด approve → deploy
9. Manual trigger พร้อม input
on:
workflow_dispatch:
inputs:
environment:
description: 'Where to deploy'
type: choice
options: [staging, production]
default: staging
version:
description: 'Version to deploy'
required: true
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- run: |
echo "Deploying ${{ inputs.version }} to ${{ inputs.environment }}"
ใน UI กด Run workflow เลือก option ได้
10. Concurrency — กัน run พร้อมกัน
ป้องกัน 2 deploy พร้อมกันที่ environment เดียวกัน:
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: false # รอตัวเก่าเสร็จก่อน
ถ้า cancel-in-progress: true ตัวเก่าจะถูก cancel — ใช้กับ CI ที่อยาก run แค่ commit ล่าสุด
11. ลด cost ด้วย self-hosted runner
GitHub-hosted runner ฟรีจำกัด (free tier ปกติ 2,000 min/เดือน) ถ้า private repo ใช้เยอะ — ตั้ง self-hosted runner บน VPS ของตัวเอง
runs-on: self-hosted
ตั้ง: Settings → Actions → Runners → New self-hosted runner ทำตามคำสั่งที่ให้
ดี: ฟรี, เร็วถ้ามี cache local; เสีย: ต้อง maintain เอง security ต้องดูแล
12. Security ของ workflow
Pin action เป็น SHA (ไม่ใช่ tag) สำหรับ third-party action:
# ❌ tag เปลี่ยนได้
- uses: some-author/action@v1
# ✅ SHA เปลี่ยนไม่ได้
- uses: some-author/action@a1b2c3d4e5f6...
action ของ GitHub เอง (actions/*) ใช้ tag ได้ — เขา audit แล้ว
ใช้ minimum permission:
permissions:
contents: read # อ่านอย่างเดียว
pull-requests: write # comment PR ได้
ตัวอย่าง workflow ที่ครบครัน
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
test:
strategy:
matrix:
node: [20, 22]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm test
build:
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci && npm run build
- uses: actions/upload-artifact@v4
with: { name: dist, path: dist/ }
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: production
url: https://yourdomain.com
steps:
- uses: actions/download-artifact@v4
with: { name: dist, path: dist/ }
- uses: appleboy/[email protected]
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www/myapp && git pull && pm2 reload ecosystem.config.js
สรุป
เริ่มจาก simple workflow แล้วค่อยใส่ feature ตามที่เจอปัญหาจริง — อย่า over-engineer ตั้งแต่แรก
ที่ส่งผลมากที่สุด: cache (เร็วขึ้นทันที) + environment (กัน deploy ผิด) + reusable workflow (เมื่อ scale ทีม)