ส่งผล CI และการ deploy จาก GitHub Actions เข้า Telegram ทุกรอบ

job notify ส่งข้อความเข้า Telegram ทุกครั้งที่ CI จบ บอกว่า test ผ่านไหม deploy แล้วหรือยัง พร้อมลิงก์ไปหน้ารอบนั้น โพสต์นี้อธิบายการสร้าง bot การหา chat id ทำไมต้องใช้ if: always() การแปลงผลของแต่ละ job เป็นข้อความ และการให้ job notify แดงตามผลจริง

เผยแพร่
เวลาอ่าน
4 นาที
ในโพสต์นี้ 6 หัวข้อ
  1. สร้าง bot และหา chat id
  2. job notify ต้องมี if: always()
  3. แปลงผลของแต่ละ job เป็นข้อความ
  4. ส่งด้วย curl แล้วจบด้วยผลจริง
  5. เครื่องที่ส่งต้องออกเน็ตไปหา Telegram ได้
  6. AI agent ไม่ต้องนั่งรอ CI

GitHub ส่งอีเมลหาเราเองเมื่อ workflow ที่เราเป็นคนสั่งรันไม่ผ่าน แต่อีเมลพวกนี้มักไปกองรวมกับอีเมลอื่น และไม่บอกอะไรเลยตอนที่ deploy สำเร็จ เราอยากได้ข้อความสั้น ๆ บนมือถือทุกรอบ ผ่านหรือไม่ผ่านก็ส่ง ข้อความหน้าตาแบบนี้

✅ my-app — tests passed · deployed
main · เพิ่มหน้ารายงานยอดขาย
https://github.com/my-org/my-app/actions/runs/123456789

เราเคยส่งเข้า LINE Notify แต่ LINE ปิดบริการไปตั้งแต่ 31 มีนาคม 2568 ทางที่เหลือฝั่ง LINE คือ Messaging API ซึ่งต้องมี LINE OA และนับโควตาข้อความ push ทุกเดือน ส่วน Telegram Bot API ไม่มีค่าใช้จ่าย ส่งด้วย HTTP request เดียว และไม่ต้องมีบัญชีธุรกิจ งานแจ้งผล CI ของเราจึงใช้ Telegram

สร้าง bot และหา chat id

  1. เปิด Telegram คุยกับ @BotFather พิมพ์ /newbot ตั้งชื่อ bot แล้วจะได้ token รูปแบบ 123456789:AA… มา
  2. ส่งข้อความอะไรก็ได้หา bot ตัวนั้น bot ส่งข้อความหาเราก่อนไม่ได้ จนกว่าเราจะเริ่มคุยด้วย
  3. เปิด https://api.telegram.org/bot<TOKEN>/getUpdates ในเบราว์เซอร์ หา "chat":{"id":…} ในผลลัพธ์ ตัวเลขนั้นคือ chat id

ถ้าอยากให้ส่งเข้ากลุ่ม ให้เชิญ bot เข้ากลุ่มก่อน แล้วพิมพ์ /start ในกลุ่ม chat id ของกลุ่มเป็นเลขติดลบ ค่าเริ่มต้นของ bot ในกลุ่มคืออ่านเฉพาะคำสั่งที่ขึ้นต้นด้วย / ข้อความธรรมดาจึงไม่โผล่ใน getUpdates

เก็บสองค่านี้เป็น secret ของ repo ห้ามเขียนลงไฟล์ workflow

gh secret set TELEGRAM_BOT_TOKEN -R my-org/my-app
gh secret set TELEGRAM_CHAT_ID -R my-org/my-app

gh จะถามค่าแบบซ่อนตัวอักษร ค่าไม่ค้างอยู่ใน history ของ shell

job notify ต้องมี if: always()

  notify:
    needs: [test, deploy]
    if: always()
    runs-on: ubuntu-latest
    steps:
      - name: Send Telegram notification
        env:
          TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
          TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
          TEST_RESULT: ${{ needs.test.result }}
          DEPLOY_RESULT: ${{ needs.deploy.result }}
          COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
        run: |
          # script อยู่ในสองหัวข้อถัดไป

ปกติ GitHub ข้าม job ที่ needs ไม่ผ่าน ถ้าไม่มี if: always() รอบที่ test แดงจะไม่มีข้อความมาเลย ซึ่งเป็นรอบที่เราอยากรู้ที่สุด

ค่าทุกตัวส่งเข้า script ผ่าน env: ข้อความ commit เป็นสิ่งที่คน push พิมพ์เอง ถ้าเขียน ${{ github.event.head_commit.message }} ลงใน script ตรง ๆ ข้อความนั้นอาจกลายเป็นคำสั่ง shell ได้ เหมือนที่เล่าไว้ในโพสต์เรื่อง lint เฉพาะไฟล์ที่ PR แก้

แปลงผลของแต่ละ job เป็นข้อความ

needs.<job>.result มีสี่ค่า คือ success, failure, cancelled และ skipped เราจับคู่ผลของ test กับ deploy ด้วย case

if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then
  echo "TELEGRAM_* secrets are not set on this repo — skipping the notification."
  exit 0
fi

case "$TEST_RESULT:$DEPLOY_RESULT" in
  success:success) icon="✅"; state="tests passed · deployed"; failed=0 ;;
  success:skipped) icon="✅"; state="tests passed";            failed=0 ;;
  success:*)       icon="❌"; state="deploy $DEPLOY_RESULT";   failed=1 ;;
  *)               icon="❌"; state="tests $TEST_RESULT";      failed=1 ;;
esac

subject="$(printf '%s' "$COMMIT_MESSAGE" | head -1)"
[ -n "$subject" ] || subject="${GITHUB_SHA:0:7}"
  • success:skipped นับเป็นผ่าน branch อื่นที่ไม่ใช่ main ไม่ deploy อยู่แล้ว deploy จึงเป็น skipped ทุกรอบ
  • ใช้บรรทัดแรกของข้อความ commit ข้อความที่ยาวหลายบรรทัดจะได้ไม่ล้นจอ
  • head_commit ว่างใน pull request และการกดรันเอง event สองแบบนี้ไม่มี commit ที่ push เข้ามา ก็ใช้ SHA เจ็ดตัวแรกแทน
  • ไม่มี secret ก็จบด้วย exit 0 repo ที่ยังไม่ได้ตั้ง Telegram จะได้ไม่แดงเพราะเรื่องนี้ แต่ข้อเสียคือถ้าลืมตั้ง ก็จะไม่มีข้อความมาโดยไม่มีอะไรเตือน ตั้ง secret แล้วสั่ง gh secret list เช็กว่ามีครบสองตัวทุกครั้ง

ส่งด้วย curl แล้วจบด้วยผลจริง

text="$(printf '%s %s — %s\n%s · %s\n%s/%s/actions/runs/%s' \
  "$icon" "${GITHUB_REPOSITORY##*/}" "$state" \
  "$GITHUB_REF_NAME" "$subject" \
  "$GITHUB_SERVER_URL" "$GITHUB_REPOSITORY" "$GITHUB_RUN_ID")"

curl -sS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
  --data-urlencode "chat_id=${TELEGRAM_CHAT_ID}" \
  --data-urlencode "text=${text}" \
  --data-urlencode 'link_preview_options={"is_disabled":true}' > /dev/null

exit "$failed"
  • --data-urlencode เข้ารหัสข้อความให้ถูกต้อง ข้อความ commit ที่มีภาษาไทย & หรือขึ้นบรรทัดใหม่ก็ส่งได้ครบ
  • link_preview_options ปิดการ์ดตัวอย่างของลิงก์ ข้อความจะได้สั้นอยู่บรรทัดเดียวในหน้าแชต (เอกสารของ sendMessage)
  • exit "$failed" ทำให้ job notify แดงตามรอบที่พัง ถ้า notify เขียวอยู่ข้าง deploy ที่แดง ตอนกวาดตาดูรายการ job จะดูเหมือนว่ามีบางอย่างผ่าน แล้วก็มองข้าม deploy ที่พังไปได้ง่าย เราส่งข้อความก่อนแล้วค่อยจบด้วยผลจริง รอบที่ผ่านทุก job ก็ยังเขียวตามเดิม

เครื่องที่ส่งต้องออกเน็ตไปหา Telegram ได้

ถ้า job notify รันบน self-hosted runner ที่อยู่ในเครือข่ายของบริษัท firewall อาจกัน api.telegram.org หรือดักการเชื่อมต่อ HTTPS ไว้ curl จะขึ้น error เรื่อง certificate แล้วข้อความไม่ไปถึง ก่อนวาง job ไว้บนเครื่องไหน ลอง curl -sv https://api.telegram.org/ จากเครื่องนั้นดูก่อนว่าได้ certificate ของ Telegram จริง

ถ้าเครื่องในออฟฟิศออกไปไม่ได้ ก็ให้ job notify รันบน ubuntu-latest ของ GitHub แทน job นี้ไม่ต้อง checkout โค้ดและไม่ต้องเข้า server ใช้เวลาไม่กี่วินาที แต่ GitHub ปัดเวลาของแต่ละ job ขึ้นเป็นนาทีเต็ม รอบหนึ่งจึงกินนาทีของแพ็กเกจหนึ่งนาที

AI agent ไม่ต้องนั่งรอ CI

ข้อดีที่ไม่ได้คิดไว้ตอนแรกคือเรื่อง AI agent ถ้าไม่มี job นี้ agent ที่ push โค้ดเสร็จมักสั่ง gh run watch แล้วรอจนจบรอบ บางรอบก็หลายนาที พอมีข้อความเข้า Telegram เราก็บอก agent ได้ว่า push แล้วจบงานได้เลย ผลจะมาถึงมือถือเราเอง

ถ้าข้อความเป็น ❌ ลิงก์ในข้อความจะพาไปหน้ารอบนั้น หรือส่ง run id ให้ agent อ่าน log เฉพาะ step ที่พังด้วย gh run view <run-id> --log-failed แล้วแก้ต่อได้เลย

จบซีรีส์ CI/CD ใน Laravel แล้ว

ดูทั้ง 5 ตอน →

อ่านต่อ

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

ล่าสุด

ดูทั้งหมด →