Thuta Learning
CI/CD with GitHub Actions
ProjectsDevOps & Toolsbeginner

Project: Node App တစ်ခုအတွက် ပြည့်စုံတဲ့ CI Pipeline

ဒီခန်းပြီးရင် ဘာတတ်သွားမလဲ

  • Project: Node App တစ်ခုအတွက် ပြည့်စုံတဲ့ CI Pipeline concept ကို နားလည်ရှင်းပြနိုင်ရန်
  • Diagram ကို ဖတ်ပြီး pipeline ထဲမှာ job တွေ ဘယ်လိုစီးဆင်းပြီး ဘာက gate လုပ်သလဲ ခြေရာခံနိုင်ရန်
  • Workflow file ကို ဖတ်ပြီး ဘယ် job တွေ ဘယ်အစဉ်လိုက် run မလဲ ကြိုတင်ပြောနိုင်ရန်

နားလည်ထားရမယ့် အချက်

ဒါဟာ ဒီ tutorial ထဲမှာ တကယ့် repository တစ်ခုထဲ ကူးထည့်ပြီး အဲဒီအတိုင်း ထားလိုက်လို့ရတဲ့ ပထမဆုံး workflow ပါ။ ထဲမှာပါတဲ့ အစိတ်အပိုင်းတိုင်းကို အရင် သင်ခန်းစာတွေမှာ တစ်ခုချင်း တွေ့ခဲ့ပြီးပါပြီ။ Project သင်ခန်းစာရဲ့ တာဝန်က အဲဒီအပိုင်းတွေ တစ်ခုနဲ့တစ်ခု ဘယ်လို ချည်နှောင်နေလဲဆိုတာ ပြဖို့ပါ။

Trigger ကနေ စကြည့်ရအောင်။ main ကို push တဲ့အခါရော pull_request တက်တဲ့အခါရော run တယ်ဆိုတော့၊ change တစ်ခု merge မဖြစ်ခင်မှာလည်း စစ်ပြီး၊ merge ဖြစ်သွားပြီးနောက် ရလဒ်ကိုလည်း ထပ်စစ်ပါတယ်။ Branch protection rule တစ်ခုကို ယုံကြည်လို့ရဖို့ ဒီနှစ်ခုလုံး လိုပါတယ်။ concurrency block က branch တစ်ခုတည်းပေါ်မှာ နောက်ကျန်ခဲ့တဲ့ run အဟောင်းတွေကို cancel လုပ်ပေးလို့၊ အလုပ်များနေတဲ့ pull request တစ်ခုက အသုံးမဝင်တော့တဲ့ run ငါးခု တန်းစီမနေတော့ပါဘူး။

နောက်တစ်ခုက graph ရဲ့ ပုံသဏ္ဌာန်ပါ။ lint, typecheck, test သုံးခုဟာ တစ်ခုနဲ့တစ်ခု မှီခိုမှု မရှိလို့ needs မထည့်ဘဲ သီးခြား job သုံးခုအဖြစ် ပြိုင်တူ run ပါတယ်။ ဒါက တမင် ရွေးထားတဲ့ ဆုံးဖြတ်ချက်ပါ — job တစ်ခုတည်းထဲမှာ သုံးခုလုံး အစဉ်လိုက် run ရင် အချိန်က သုံးခုပေါင်းလဒ် ဖြစ်သွားပြီး၊ lint fail တာက သင် တကယ်ကြည့်ချင်တဲ့ test ရလဒ်ကို ဖုံးကွယ်ပစ်ပါတယ်။ Job ခွဲထားတော့ check run သီးသန့်စီ ရလို့၊ pull request စာမျက်နှာက ဘယ်ဟာ ပျက်သွားလဲ တန်းပြောပြပါတယ်။

Job တိုင်းဟာ runner အသစ် ရတာမို့ node_modules ကို job တစ်ခုကနေ နောက်တစ်ခုဆီ လက်ဆင့်ကမ်းလို့ မရပါဘူး။ job တိုင်း ကိုယ့်ဟာကိုယ် install လုပ်ရပါတယ်။ အဲဒါ ဈေးပေါအောင် လုပ်ပေးတာက cache ပါ။ ~/.npm ကို hashFiles('package-lock.json') နဲ့ key ထားလိုက်တော့၊ dependency စာရင်း ပြောင်းမှသာ key ပြောင်းပါတယ်။ restore-keys prefix က ပြောင်းလိုက်တဲ့ ပထမတစ်ကြိမ်မှာတောင် တစ်ဝက်တစ်ပျက် cache ကို ပြန်သုံးခွင့် ပေးပါတယ်။

Test တွေကို Node version matrix အောက်မှာ run တာက၊ version အများကြီး support လုပ်တယ်ဆိုတဲ့ ကတိကို အမြဲတမ်း ထိန်းထားရလို့ပါ။ fail-fast: false က ပထမဆုံး နီသွားတဲ့ version မှာ မရပ်ဘဲ version အားလုံးရဲ့ ရလဒ်ကို အစီရင်ခံစေပါတယ်။

နောက်ဆုံးအနေနဲ့ artifact upload ပေါ်က if: always() ပါ။ Test step fail သွားရင် သူ့နောက်က step အားလုံး skip ဖြစ်တာ ပုံမှန်ပါ — ဒါပေမယ့် အဲဒီအချိန်ဟာ report ကို အလိုအပ်ဆုံး အချိန်ပါပဲ။ ပြီးတော့ build မှာ needs: [lint, typecheck, test] ပါလို့၊ check အားလုံး စိမ်းမလာမချင်း ဘာမှ package မလုပ်ပါဘူး။

text
NODE CI JOB GRAPH
-----------------
  push to main  /  pull_request into main
                              |
          +-------------------+----------------------+
          v                   v                      v
  +---------------+   +---------------+   +---------------------+
  |     lint      |   |   typecheck   |   |  test  (matrix)     |
  |    node 20    |   |    node 20    |   |  node 18 / 20 / 22  |
  | cache ~/.npm  |   | cache ~/.npm  |   |  cache ~/.npm       |
  +---------------+   +---------------+   |  fail-fast: false   |
          |                   |           +---------------------+
          |                   |                      |
          |                   |                      +--> upload-artifact
          |                   |                      |    test-report-node-NN
          |                   |                      |    (if: always)
          |                   |                      |
          +-------------------+----------------------+
                              |
                              |  needs: [lint, typecheck, test]
                              |  all three green, or build is skipped
                              v
                     +-----------------+
                     |      build      |
                     |  npm run build  |
                     |  upload dist/   |
                     +-----------------+

လက်တွေ့ scenario နဲ့ ချိတ်ကြည့်မယ်

ဒီ workflow ကို .github/workflows/ci.yml အဖြစ် သိမ်းပါ။ Job လေးမျိုး ရှိပေမယ့် အမှန်တကယ် run တဲ့ job အရေအတွက်က ခြောက်ခုပါ — matrix က test ကို သုံးခု ဖြစ်သွားစေလို့ပါ။ lint, typecheck နဲ့ test သုံးမျိုးလုံး တစ်ပြိုင်တည်း စတင်ပါတယ်။ ဘယ်သူ့ကိုမှ မစောင့်ရပါဘူး။

Job တိုင်းက တူညီတဲ့ လေးဆင့်ကို ထပ်လုပ်ပါတယ် — checkout, setup-node, cache restore, npm ci။ ထပ်နေတာ ရှုပ်တယ်လို့ ထင်ရပေမယ့် ဒါက runner သီးသန့်စီ ဖြစ်နေလို့ မရှောင်လွှဲနိုင်တဲ့ အခြေအနေပါ။ ဒီနေရာမှာ လက်တွေ့ ဂရုစိုက်ရမှာက cache key ပါ။ test job က node-version ကို key ထဲ ထည့်ထားပါတယ်။ native module တွေဟာ Node version အလိုက် compile ဖြစ်တာမို့၊ version အားလုံးကို key တစ်ခုတည်းနဲ့ မျှသုံးရင် Node 18 က Node 22 အတွက် build ထားတဲ့ binary ကို ရသွားနိုင်ပါတယ်။

Test step က report ကို reports/ ထဲ ရေးပြီး၊ နောက် step က if: always() နဲ့ အဲဒီ folder ကို artifact အဖြစ် တင်ပါတယ်။ Node version တစ်ခုစီအတွက် artifact အမည် မတူအောင် ခွဲထားရပါတယ် — upload-artifact@v4 မှာ နာမည်တူ artifact နှစ်ခု တင်ရင် ပဋိပက္ခ ဖြစ်ပြီး job fail ပါတယ်။

Production အရေးပါမှုက build job မှာပါ။ needs: [lint, typecheck, test] က gate တစ်ခုပါ။ Upstream job တစ်ခုခု fail ရင် build ဟာ fail မဟုတ်ဘဲ skip ဖြစ်သွားလို့၊ ဘယ်တော့မှ ပျက်နေတဲ့ code ကနေ artifact ထွက်မလာပါဘူး။ Branch protection မှာ ဒီ check တွေကို required အဖြစ် သတ်မှတ်လိုက်ရင် workflow က အကြံပြုချက်ကနေ စည်းမျဉ်း ဖြစ်သွားပါပြီ။

အတူတူ စမ်းရေးကြည့်မယ်

yaml
name: Node CI

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

concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'
      - name: Cache npm downloads
        uses: actions/cache@v4
        with:
          path: ~/.npm
          key: npm-${{ runner.os }}-node20-${{ hashFiles('package-lock.json') }}
          restore-keys: |
            npm-${{ runner.os }}-node20-
      - run: npm ci
      - run: npm run lint

  typecheck:
    name: Typecheck
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'
      - name: Cache npm downloads
        uses: actions/cache@v4
        with:
          path: ~/.npm
          key: npm-${{ runner.os }}-node20-${{ hashFiles('package-lock.json') }}
          restore-keys: |
            npm-${{ runner.os }}-node20-
      - run: npm ci
      - run: npm run typecheck

  test:
    name: Test on Node ${{ matrix.node-version }}
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        node-version: ['18', '20', '22']
    steps:
      - uses: actions/checkout@v4
      - name: Set up Node
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - name: Cache npm downloads
        uses: actions/cache@v4
        with:
          path: ~/.npm
          key: npm-${{ runner.os }}-node${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }}
          restore-keys: |
            npm-${{ runner.os }}-node${{ matrix.node-version }}-
      - run: npm ci
      - name: Run tests
        run: npm test -- --reporter=junit --outputFile=reports/junit.xml
      - name: Upload test report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-report-node-${{ matrix.node-version }}
          path: reports/
          retention-days: 7
          if-no-files-found: warn

  build:
    name: Build
    needs: [lint, typecheck, test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'
      - name: Cache npm downloads
        uses: actions/cache@v4
        with:
          path: ~/.npm
          key: npm-${{ runner.os }}-node20-${{ hashFiles('package-lock.json') }}
          restore-keys: |
            npm-${{ runner.os }}-node20-
      - run: npm ci
      - run: npm run build
      - name: Upload build output
        uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/
          retention-days: 7
          if-no-files-found: error
You should see
Push သို့မဟုတ် pull request တစ်ခုတိုင်းမှာ job ခြောက်ခု စတင်ပါတယ် — lint, typecheck နဲ့ test job သုံးခု (Node 18, 20, 22) ဟာ တစ်ပြိုင်တည်း အစပြုပြီး၊ လွတ်နေတဲ့ runner ရသလောက် ပြိုင်တူ run ပါတယ်။ Job တိုင်းက ~/.npm cache ကို ပြန်ယူပြီး npm ci လုပ်ပါတယ်။ Lockfile မပြောင်းသေးရင် cache hit ဖြစ်ပြီး၊ ပြောင်းသွားရင် restore-keys prefix နဲ့ တစ်ပိုင်းတစ်စ ပြန်ရကာ run အဆုံးမှာ cache အသစ် သိမ်းပါတယ်။ Test job တစ်ခုစီက ရလဒ် ဘယ်လိုဖြစ်ဖြစ် reports/ ကို artifact အဖြစ် တင်ပါတယ် — test-report-node-18, -20, -22 ဆိုပြီး သီးခြားစီ ဖြစ်ပါတယ်။ fail-fast: false ကြောင့် version တစ်ခု fail ပေမယ့် ကျန်နှစ်ခု ဆက်သွားပြီး၊ ဘယ် version တွေမှာ ပြဿနာရှိလဲ တစ်ကြိမ်တည်းနဲ့ သိရပါတယ်။ lint, typecheck, test အားလုံး အောင်မြင်မှသာ build job စပါတယ်။ တစ်ခုခု fail ရင် build က fail မဟုတ်ဘဲ skipped အဖြစ် ပြပြီး dist artifact ထွက်မလာပါဘူး။ Run တစ်ခုလုံးရဲ့ အခြေအနေက ကျရှုံးတဲ့ job ရှိရင် failed ဖြစ်ပြီး၊ pull request ပေါ်မှာ check run သီးသန့်စီ မြင်ရလို့ ဘယ်အပိုင်း ပျက်သွားလဲ ချက်ချင်း သိပါတယ်။

၅ မိနစ် စမ်းကြည့်

ဒီ workflow ကို repository တစ်ခုမှာ ထည့်ပြီး နှစ်ခု တိုးချဲ့ကြည့်ပါ။ ပထမ — test job ကို operating system အလိုက်ပါ ကျယ်ပြန့်အောင် matrix ထဲ os: [ubuntu-latest, windows-latest] ထည့်ပြီး runs-on: ${{ matrix.os }} လုပ်ပါ။ cache key ထဲမှာ runner.os ပါပြီးသားမို့ ဘာမှ ထပ်ပြင်စရာ မလိုတာ သတိပြုပါ။ ဒုတိယ — build job ရဲ့ artifact retention-days ကို 7 ကနေ 1 ပြောင်းပြီး၊ ဘာကြောင့် test report နဲ့ build output ဟာ သက်တမ်း မတူသင့်လဲ စဉ်းစားပါ။ ပြီးရင် test တစ်ခုကို တမင် ပျက်အောင်လုပ်ပြီး push ပါ။ build job က failed လား skipped လား၊ test report artifact က ဒါတောင်မှ download လုပ်လို့ ရသေးလား စစ်ပါ။

သတိလေးတစ်ချက်

node_modules folder ကိုယ်တိုင်ကို cache လုပ်ပြီး matrix အားလုံးမှာ key တစ်ခုတည်း သုံးမိတာ။ Native module တွေက Node version အလိုက် compile ဖြစ်လို့၊ Node 22 အတွက် build ထားတဲ့ binary ကို Node 18 job က ပြန်ရပြီး ရှင်းမရတဲ့ crash ဖြစ်တတ်ပါတယ်။ ~/.npm ကိုသာ cache လုပ်ပြီး npm ci ကို job တိုင်းမှာ run ပါ။

Artifact upload step မှာ if: always() မထည့်မိတာ။ Test fail တဲ့အခါ နောက် step တွေ အလိုအလျောက် skip ဖြစ်သွားလို့၊ report အလိုအပ်ဆုံး အခြေအနေမှာ report မရှိဘဲ ဖြစ်နေပါတယ်။

GitHub Docs - Building and testing Node.jsCI/CD with GitHub Actions

ဒီနေရာမှာ လူအများမှားတတ်တယ်

  • node_modules folder ကိုယ်တိုင်ကို cache လုပ်ပြီး matrix အားလုံးမှာ key တစ်ခုတည်း သုံးမိတာ။ Native module တွေက Node version အလိုက် compile ဖြစ်လို့၊ Node 22 အတွက် build ထားတဲ့ binary ကို Node 18 job က ပြန်ရပြီး ရှင်းမရတဲ့ crash ဖြစ်တတ်ပါတယ်။ ~/.npm ကိုသာ cache လုပ်ပြီး npm ci ကို job တိုင်းမှာ run ပါ။
  • Artifact upload step မှာ if: always() မထည့်မိတာ။ Test fail တဲ့အခါ နောက် step တွေ အလိုအလျောက် skip ဖြစ်သွားလို့၊ report အလိုအပ်ဆုံး အခြေအနေမှာ report မရှိဘဲ ဖြစ်နေပါတယ်။
  • Workflow ကို production branch ပေါ် တိုက်ရိုက်မစမ်းဘဲ branch (သို့) test repository တွင် အရင်အတည်ပြုပါ။

လေ့ကျင့်ခန်း

ဒီ workflow ကို repository တစ်ခုမှာ ထည့်ပြီး နှစ်ခု တိုးချဲ့ကြည့်ပါ။ ပထမ — test job ကို operating system အလိုက်ပါ ကျယ်ပြန့်အောင် matrix ထဲ os: [ubuntu-latest, windows-latest] ထည့်ပြီး runs-on: ${{ matrix.os }} လုပ်ပါ။ cache key ထဲမှာ runner.os ပါပြီးသားမို့ ဘာမှ ထပ်ပြင်စရာ မလိုတာ သတိပြုပါ။ ဒုတိယ — build job ရဲ့ artifact retention-days ကို 7 ကနေ 1 ပြောင်းပြီး၊ ဘာကြောင့် test report နဲ့ build output ဟာ သက်တမ်း မတူသင့်လဲ စဉ်းစားပါ။ ပြီးရင် test တစ်ခုကို တမင် ပျက်အောင်လုပ်ပြီး push ပါ။ build job က failed လား skipped လား၊ test report artifact က ဒါတောင်မှ download လုပ်လို့ ရသေးလား စစ်ပါ။

You'll know it worked when: Push သို့မဟုတ် pull request တစ်ခုတိုင်းမှာ job ခြောက်ခု စတင်ပါတယ် — lint, typecheck နဲ့ test job သုံးခု (Node 18, 20, 22) ဟာ တစ်ပြိုင်တည်း အစပြုပြီး၊ လွတ်နေတဲ့ runner ရသလောက် ပြိုင်တူ run ပါတယ်။ Job တိုင်းက ~/.npm cache ကို ပြန်ယူပြီး npm ci လုပ်ပါတယ်။ Lockfile မပြောင်းသေးရင် cache hit ဖြစ်ပြီး၊ ပြောင်းသွားရင် restore-keys prefix နဲ့ တစ်ပိုင်းတစ်စ ပြန်ရကာ run အဆုံးမှာ cache အသစ် သိမ်းပါတယ်။ Test job တစ်ခုစီက ရလဒ် ဘယ်လိုဖြစ်ဖြစ် reports/ ကို artifact အဖြစ် တင်ပါတယ် — test-report-node-18, -20, -22 ဆိုပြီး သီးခြားစီ ဖြစ်ပါတယ်။ fail-fast: false ကြောင့် version တစ်ခု fail ပေမယ့် ကျန်နှစ်ခု ဆက်သွားပြီး၊ ဘယ် version တွေမှာ ပြဿနာရှိလဲ တစ်ကြိမ်တည်းနဲ့ သိရပါတယ်။ lint, typecheck, test အားလုံး အောင်မြင်မှသာ build job စပါတယ်။ တစ်ခုခု fail ရင် build က fail မဟုတ်ဘဲ skipped အဖြစ် ပြပြီး dist artifact ထွက်မလာပါဘူး။ Run တစ်ခုလုံးရဲ့ အခြေအနေက ကျရှုံးတဲ့ job ရှိရင် failed ဖြစ်ပြီး၊ pull request ပေါ်မှာ check run သီးသန့်စီ မြင်ရလို့ ဘယ်အပိုင်း ပျက်သွားလဲ ချက်ချင်း သိပါတယ်။

Project: Node App တစ်ခုအတွက် ပြည့်စုံတဲ့ CI Pipeline | Thuta Learning