ตั้ง GitHub Actions ให้โปรเจกต์ Laravel รัน test ก่อน deploy แล้วแจ้งผลทุกรอบ

workflow เดียวแบ่งเป็นสาม job คือ test, deploy และ notify ให้ deploy รอจน test ผ่าน และให้ notify รายงานผลทุกครั้ง โพสต์นี้อธิบายลำดับขั้นใน job test การข้าม CI เมื่อแก้แค่เอกสารด้วย paths-ignore และการตั้ง concurrency ไม่ให้รอบเก่าค้างคิว

เผยแพร่
เวลาอ่าน
4 นาที
ในโพสต์นี้ 6 หัวข้อ
  1. workflow คือไฟล์ YAML ไฟล์หนึ่งใน repo
  2. แบ่งงานเป็นสาม job
  3. ลำดับใน job test
  4. ข้าม CI เมื่อแก้แค่เอกสาร
  5. ยกเลิกรอบเก่าเมื่อมี push ใหม่
  6. ให้ AI agent อ่านผล CI แทนการนั่งรอ

ทางที่ใช้กันมาตลอดคือ ssh เข้า server แล้วพิมพ์ git pull, composer install, php artisan migrate ทีละบรรทัด บางคนเก็บเป็น script ไว้รันครั้งเดียว ก็ใช้ได้นะ จนวันที่ลืมรัน test ก่อน หรือ push โค้ดที่ test ยังแดงขึ้นไปตอนรีบ

CI/CD คือการให้เครื่องทำขั้นตอนพวกนี้แทนเราทุกครั้งที่ push

  • CI (continuous integration) รัน test ให้ทุก push ทุก branch เราจะรู้ตั้งแต่ก่อน merge ว่าโค้ดพังหรือเปล่า
  • CD (continuous deployment) deploy ขึ้น production ให้เองเมื่อ test ของ main ผ่าน

โพสต์นี้ใช้ GitHub Actions เพราะอยู่ใน GitHub ที่เราเก็บโค้ดอยู่แล้ว ไม่ต้องสมัครบริการเพิ่ม

workflow คือไฟล์ YAML ไฟล์หนึ่งใน repo

GitHub อ่านทุกไฟล์ในโฟลเดอร์ .github/workflows ไฟล์หนึ่งคือ workflow หนึ่งตัว คำที่ต้องรู้มีสี่คำ

  • workflow ไฟล์หนึ่งไฟล์ บอกว่ารันเมื่อไร (on:) และมี job อะไรบ้าง
  • job กลุ่มของขั้นตอนที่รันบนเครื่องเดียวกัน job ต่าง ๆ รันพร้อมกันได้ ถ้าไม่ได้บอกให้รอกัน
  • step ขั้นตอนหนึ่งใน job เป็นคำสั่ง shell (run:) หรือ action สำเร็จรูป (uses:) เช่น actions/checkout
  • runner เครื่องที่รับ job ไปรัน จะเป็นเครื่องของ GitHub หรือเครื่องของเราเองก็ได้ เรื่องนี้มีรายละเอียดอยู่ในโพสต์เรื่อง self-hosted runner

repo สาธารณะใช้เครื่องของ GitHub ได้ไม่คิดเงิน ส่วน repo ส่วนตัวบนแพ็กเกจ Free ได้ 2,000 นาทีต่อเดือน (ราคาของ GitHub Actions) โปรเจกต์เล็กที่ push วันละไม่กี่ครั้งใช้ไม่หมด แต่ถ้า CI รันรอบละหลายนาทีและ push บ่อย ก็หมดก่อนสิ้นเดือนได้

แบ่งงานเป็นสาม job

สาม job ใน workflow เดียว test ต้องผ่านก่อน deploy ส่วน notify รันทุกครั้ง

job deploy รอ test ส่วน notify รอทั้งสองตัว แล้วรันเสมอ

name: CI

on:
  push:
    branches: ['**']
  pull_request:
    branches: [main]
  workflow_dispatch:

jobs:
  test:
    runs-on: ubuntu-latest
    steps: # ...

  deploy:
    needs: [test]
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps: # ...

  notify:
    needs: [test, deploy]
    if: always()
    runs-on: ubuntu-latest
    steps: # ...
  • needs: [test] ทำให้ deploy รอจน test จบ ถ้า test ไม่ผ่าน GitHub จะข้าม deploy ไปเลย
  • if: ของ deploy เช็กสองอย่าง push เท่านั้น และต้องเป็น main เท่านั้น pull request กับการกดรันเองจากหน้าเว็บ (workflow_dispatch) จึงรัน test ได้แต่ไม่ deploy
  • if: always() ของ notify สำคัญกว่าที่เห็น ปกติ GitHub ข้าม job ที่ needs ไม่ผ่าน ซึ่งก็คือรอบที่เราอยากรู้ผลที่สุด รายละเอียดของ job นี้อยู่ในโพสต์เรื่องส่งผลเข้า Telegram

ส่วนขั้นตอนใน job deploy มีลำดับที่ต้องคิดหลายจุด จึงแยกไปอธิบายในโพสต์เรื่องลำดับการ deploy

ลำดับใน job test

  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:18
        env:
          POSTGRES_USER: testing
          POSTGRES_PASSWORD: testing
          POSTGRES_DB: app_testing
        ports: ['5432:5432']
        options: >-
          --health-cmd pg_isready --health-interval 10s
          --health-timeout 5s --health-retries 5
    steps:
      - uses: actions/checkout@v7
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.5'
      - uses: actions/setup-node@v7
        with:
          node-version: '24'

      - run: cp .env.testing .env
      - run: composer install --no-interaction --prefer-dist --no-progress
      - run: php artisan optimize:clear
      - run: php artisan key:generate
      - run: composer analyse
      - run: npm ci && npm run build
      - run: npm test
      - run: php artisan snapshot:load testing --env=testing --force
      - run: php artisan migrate --env=testing --force
      - run: php artisan test --compact

services: เปิด PostgreSQL ใน container ให้ job นี้ใช้ ถ้าโปรเจกต์ใช้ MySQL ก็เปลี่ยนเป็น image mysql:8.4 แล้วตั้งตัวแปร MYSQL_* ตามเอกสารของ image นั้นแทน ส่วนลำดับของ step มีเหตุผลอยู่สามจุด

  • static analysis ขึ้นก่อน composer analyse คือ PHPStan ซึ่งไม่ต้องใช้ฐานข้อมูลและจบเร็ว ถ้าเจอบั๊กก็หยุดตรงนี้ ไม่ต้องรอ test ชุดใหญ่ (PHPStan ทำอะไรบ้าง)
  • build ก่อน test ฝั่ง PHP feature test ที่ render หน้าต้องอ่าน Vite manifest และถ้าใช้ Wayfinder ไฟล์ route ฝั่ง JavaScript ก็มาจากขั้น build เหมือนกัน ถ้าสลับลำดับ test จะแดงด้วยเรื่องที่ไม่เกี่ยวกับโค้ดเลย Vitest จึงรันต่อจาก build ทันที
  • โหลดฐานข้อมูลตั้งต้นก่อน migrate ทุกรอบเริ่มจากไฟล์ SQL ชุดเดียวกัน เหตุผลอยู่ในโพสต์เรื่อง snapshot ถ้าโปรเจกต์ยังใช้ RefreshDatabase ก็ข้ามขั้น snapshot:load ไปได้

ข้าม CI เมื่อแก้แค่เอกสาร

commit ที่แก้แค่ README หรือโน้ตใน repo ไม่ได้เปลี่ยนโค้ด แต่ CI ก็ยังรันเต็มชุดและ deploy ซ้ำ paths-ignore บอกให้ข้ามรอบที่ไฟล์ที่เปลี่ยนทุกไฟล์ตรงกับรายการ

on:
  push:
    branches: ['**']
    paths-ignore: ['docs/**', '**/*.md']
  pull_request:
    branches: [main]
    paths-ignore: ['docs/**', '**/*.md']

ถ้า commit แก้ทั้ง .md และไฟล์ PHP ก็ยังรันตามปกติ เพราะไม่ใช่ทุกไฟล์ที่ตรงกับรายการ

มีสองจุดที่ทำให้งงได้

  • ถ้าตั้ง required status check ไว้ใน branch protection PR ที่แก้แค่เอกสารจะ merge ไม่ได้ เพราะ check ค้างเป็น Pending รอ workflow ที่ไม่ได้รัน (เอกสารของ GitHub) ทางแก้ที่ใช้กันคือไม่ใช้ paths-ignore กับ workflow ที่เป็น required check หรือย้ายการเช็กไฟล์ไปไว้ใน step แทน
  • branches: ['*'] ไม่รวม branch ที่มี / เช่น feature/login เพราะ * ไม่ข้าม / ถ้าตั้งชื่อ branch แบบนี้ให้ใช้ '**' แทน

ยกเลิกรอบเก่าเมื่อมี push ใหม่

ถ้า push สามครั้งติดกันใน branch เดียว GitHub จะรัน CI สามรอบ ทั้งที่เราอยากรู้แค่ผลของ commit ล่าสุด concurrency จัดให้รอบที่อยู่ group เดียวกันรันทีละรอบ และ cancel-in-progress ยกเลิกรอบเก่าเมื่อรอบใหม่มาถึง

concurrency:
  group: ci-${{ github.ref == 'refs/heads/main' && github.run_id || github.ref }}
  cancel-in-progress: true

นิพจน์นี้ให้ branch อื่นใช้ชื่อ branch เป็น group รอบใหม่จึงยกเลิกรอบเก่าได้ ส่วน main ได้ group ของตัวเองทุกรอบ (run_id ไม่ซ้ำกัน) เพราะถ้า GitHub ยกเลิกรอบของ main รอบนั้นก็ไม่ได้ deploy ไปด้วย ถ้า push สองครั้งติดกัน แล้วรอบที่สองพังเพราะเหตุอื่น commit แรกก็จะไม่ได้ขึ้น production เลย

job deploy มี group แยกของตัวเองอีกชั้น ตั้ง cancel-in-progress: false ให้รอคิวแทนการยกเลิก เพราะ deploy ที่หยุดกลางทางแย่กว่า deploy ที่ต้องรอ

  deploy:
    concurrency:
      group: deploy-production
      cancel-in-progress: false

ให้ AI agent อ่านผล CI แทนการนั่งรอ

พอ CI รันเองทุก push งานที่เหลือคือดูผล ถ้าใช้ Claude Code หรือ agent ตัวอื่นเขียนโค้ด ไม่ต้องให้ agent สั่ง gh run watch รอจนจบ job notify ส่งผลมาให้เราอยู่แล้ว ถ้ารอบไหนแดง ค่อยให้ agent อ่านเฉพาะ log ของ step ที่พัง

gh run view --log-failed

ได้ log เฉพาะ step ที่ไม่ผ่าน สั้นพอให้ agent อ่านแล้วแก้ต่อได้เลย ไม่ต้องไล่ log ทั้งรอบ

ตอนถัดไปของซีรีส์ CI/CD ใน Laravel

อ่านต่อ

ถูกอ้างถึงใน

ล่าสุด

ดูทั้งหมด →