0Pricing
Linux Command Line & Bash Scripting Mastery · บทเรียน

การเรียกใช้การทดสอบ Shell ในไปป์ไลน์ CI

เชื่อม ShellCheck และ Bats เข้ากับ GitHub Actions เพื่อให้ทุกการเปลี่ยนแปลงของ Shell ต้องผ่านการตรวจสอบที่สำเร็จ

การเรียกใช้การทดสอบ Shell ในไปป์ไลน์ CI เป็นบทเรียน Linux Command Line & Bash Scripting Mastery ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Linux Command Line & Bash Scripting Mastery และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Linux Command Line & Bash Scripting Mastery มีบทเรียนทั้งหมด 4 บทเรียน

เหตุใด CI จึงสำคัญต่อสคริปต์ Shell

สคริปต์ Shell ก็คือโค้ด — และเช่นเดียวกับโค้ดทั้งหมด สคริปต์เหล่านี้ควรมีด่านตรวจสอบคุณภาพแบบอัตโนมัติ หากไม่มี CI การพิมพ์ผิดในสคริปต์ปรับใช้ก็อาจไปถึงระบบจริงอย่างเงียบ ๆ และทำให้ระบบหยุดให้บริการตอนตีสาม

pipeline CI ที่รัดกุมสำหรับโครงการ Bash จะบังคับใช้สองสิ่งในทุก pull request:

  • การวิเคราะห์แบบสถิต ผ่าน ShellCheck — ตรวจจับข้อผิดพลาดทางไวยากรณ์ รูปแบบที่ไม่ปลอดภัย และปัญหาความสามารถในการพกพาตาม POSIX ก่อนที่สคริปต์จะถูกรัน
  • การทดสอบระดับหน่วย/การผสานรวม ผ่าน Bats (ระบบทดสอบอัตโนมัติของ Bash) — เรียกใช้ฟังก์ชันของคุณและยืนยันพฤติกรรมที่ถูกต้อง

เมื่อใช้ร่วมกัน เครื่องมือทั้งสองจะเป็นตาข่ายนิรภัยที่ทำให้คุณปรับโครงสร้างโค้ดได้อย่างมั่นใจและช่วยให้สมาชิกใหม่เริ่มงานได้เร็วขึ้น บทเรียนนี้จะเชื่อมเครื่องมือทั้งสองเข้ากับ GitHub Actions ซึ่งเป็นแพลตฟอร์ม CI ฟรีที่ใช้กันแพร่หลายที่สุดสำหรับโครงการโอเพนซอร์สและทีมขนาดเล็ก

บทนำ GitHub Actions สำหรับโครงการ Shell

GitHub Actions คือ CI/CD ที่ขับเคลื่อนด้วยเหตุการณ์และมีอยู่ใน GitHub โดยตรง เวิร์กโฟลว์ คือไฟล์ YAML ที่เก็บไว้ใน .github/workflows/ เวิร์กโฟลว์จะทำงานเมื่อเกิดเหตุการณ์ต่าง ๆ (push, pull_request และอื่น ๆ) และเรียกใช้งานบนตัวเรียกใช้ที่โฮสต์ไว้

แนวคิดสำคัญที่คุณจำเป็นต้องรู้:

  • on: — ตัวกระตุ้น (เช่น push, pull_request)
  • jobs: — หน่วยงานที่ทำงานขนานกัน โดยแต่ละหน่วยทำงานบน VM ใหม่
  • steps: — คำสั่ง Shell ตามลำดับหรือ แอ็กชัน ที่นำกลับมาใช้ใหม่ได้ภายในงาน
  • runs-on: — อิมเมจของตัวเรียกใช้ (เราใช้ ubuntu-latest)

ไฟล์เวิร์กโฟลว์ต้องถูก commit ไว้ใน repository โดย GitHub จะตรวจพบไฟล์เหล่านี้โดยอัตโนมัติ — ไม่ต้องตั้งค่าภายนอกเพิ่มเติม

# Minimal skeleton — .github/workflows/ci.yml
name: CI

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

jobs:
  shell-checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: echo "Steps go here"

ติดตั้ง ShellCheck ในเวิร์กโฟลว์

ShellCheck ติดตั้งไว้ล่วงหน้าบนตัวเรียกใช้ ubuntu-latest ดังนั้นโดยมากคุณจึงไม่ต้องมีขั้นตอนการติดตั้งเลย อย่างไรก็ตาม รุ่นที่ติดตั้งไว้ล่วงหน้าอาจตามหลังรุ่นล่าสุด เพื่อให้ build ทำซ้ำได้เหมือนเดิม ควรกำหนดรุ่นที่แน่นอน

กลยุทธ์การติดตั้งสองแบบ:

  • ใช้ไบนารีที่ติดตั้งไว้ล่วงหน้า — ง่ายที่สุดและเพียงพอสำหรับโครงการส่วนใหญ่
  • ติดตั้งรุ่นที่กำหนดแน่นอน ผ่าน tarball รุ่นเผยแพร่อย่างเป็นทางการของ GitHub — รับประกันว่าใช้รุ่นตัวตรวจสอบเดียวกันทั้งในเครื่องและใน CI

ขั้นตอนด้านล่างแสดงแนวทางการกำหนดรุ่นแน่นอน โดยใช้สตริงรุ่นคงที่ที่เก็บเป็นตัวแปรสภาพแวดล้อม ทำให้การอัปเกรดเปลี่ยนเพียงบรรทัดเดียว

# .github/workflows/ci.yml  — ShellCheck install step
- name: Install ShellCheck
  env:
    SC_VERSION: v0.10.0
  run: |
    curl -sSfL \
      "https://github.com/koalaman/shellcheck/releases/download/${SC_VERSION}/shellcheck-${SC_VERSION}.linux.x86_64.tar.xz" \
      | tar -xJf - --strip-components=1 -C /usr/local/bin shellcheck-${SC_VERSION}/shellcheck
    shellcheck --version

เรียกใช้ ShellCheck กับสคริปต์ทุกไฟล์

หลังการติดตั้ง คุณต้องมีขั้นตอนที่ค้นหาและตรวจสอบสคริปต์ Shell ทั้งหมดใน repository ใช้ find เพื่อค้นหาไฟล์ แล้วส่งต่อให้ shellcheck

ตัวเลือกสำคัญที่ควรรู้:

  • -e SC2034 — ไม่รวมกฎที่ระบุ (ควรใช้อย่างจำกัดและใส่ความคิดเห็นกำกับ)
  • --severity=warning — ทำให้ไม่ผ่านเฉพาะคำเตือนและระดับที่รุนแรงกว่า (ไม่นับคำแนะนำด้านรูปแบบ)
  • -x — ติดตามคำสั่ง source เพื่อตรวจสอบไฟล์ที่ถูก source มาด้วย

หาก shellcheck พบปัญหาใด ๆ มันจะจบการทำงานด้วยค่าที่ไม่ใช่ศูนย์ ซึ่งจะทำให้ขั้นตอน CI ไม่ผ่านโดยอัตโนมัติ — ไม่ต้องเขียนตรรกะเพิ่มเติม

# .github/workflows/ci.yml  — ShellCheck lint step
- name: Lint shell scripts
  run: |
    # Find all .sh files and files with a bash/sh shebang
    mapfile -t scripts < <(
      find . -type f -name '*.sh' -not -path './.git/*'
    )
    if [[ ${#scripts[@]} -eq 0 ]]; then
      echo 'No shell scripts found — skipping.'
      exit 0
    fi
    echo "Linting ${#scripts[@]} file(s)..."
    shellcheck --severity=warning -x "${scripts[@]}"

Bats คืออะไรและทำงานอย่างไร

Bats (ระบบทดสอบอัตโนมัติของ Bash) คือเฟรมเวิร์กการทดสอบสำหรับ Bash ที่สอดคล้องกับ TAP ไฟล์ test แต่ละไฟล์เป็นไฟล์ .bats ที่ประกอบด้วยบล็อก @test

test จะผ่านเมื่อส่วนเนื้อหาจบการทำงานด้วย 0 และไม่ผ่านเมื่อจบด้วยค่าที่ไม่ใช่ศูนย์ Bats มีตัวแปรและฟังก์ชันตัวช่วยให้ใช้:

  • $status — รหัสการจบการทำงานของคำสั่ง run ล่าสุด
  • $output — stdout และ stderr ที่รวมกันของคำสั่ง run ล่าสุด
  • $lines — อาร์เรย์ของบรรทัดเอาต์พุต
  • run <cmd> — เรียกใช้คำสั่งโดยไม่ทำให้ test ไม่ผ่านเมื่อจบด้วยค่าที่ไม่ใช่ศูนย์

ตัวช่วย run มีความสำคัญอย่างยิ่ง — หากไม่มี ตัวคำสั่งที่ล้มเหลวจะยุติ test ก่อนที่คุณจะตรวจสอบ $status ได้

#!/usr/bin/env bats
# tests/greet.bats

setup() {
  # Runs before every @test block
  source "${BATS_TEST_DIRNAME}/../lib/greet.sh"
}

@test "greet outputs hello with the given name" {
  run greet "Alice"
  [ "$status" -eq 0 ]
  [ "$output" = "Hello, Alice!" ]
}

@test "greet fails when no argument is provided" {
  run greet
  [ "$status" -eq 1 ]
  [[ "$output" == *"Usage"* ]]
}

ติดตั้ง Bats-Core ผ่าน Git Submodule

วิธีมาตรฐานในการเพิ่ม Bats ลงในโครงการคือใช้เป็น Git submodule วิธีนี้จะตรึง commit ที่เฉพาะเจาะจง ทำให้รุ่นของตัวเรียกใช้เหมือนกับรุ่นที่ใช้พัฒนาในเครื่อง และไม่ต้องพึ่งตัวจัดการแพ็กเกจ

เรียกใช้คำสั่งเหล่านี้ครั้งเดียวในเครื่อง แล้ว commit ผลลัพธ์:

  • git submodule add https://github.com/bats-core/bats-core test/bats
  • git submodule add https://github.com/bats-core/bats-support test/test_helper/bats-support
  • git submodule add https://github.com/bats-core/bats-assert test/test_helper/bats-assert

ใน CI ให้กู้คืน submodule ด้วย actions/checkout@v4 และตัวเลือก submodules: recursive ขั้นตอนด้านล่างแสดงการตั้งค่าการ checkout แบบสมบูรณ์

# .github/workflows/ci.yml  — checkout with submodules
- name: Checkout repository
  uses: actions/checkout@v4
  with:
    submodules: recursive   # restores bats-core + helpers

เรียกใช้ Bats Tests ใน CI

เมื่อมี Bats พร้อมใช้งานแล้ว (ผ่าน submodule หรือการติดตั้งแพ็กเกจ) การเรียกใช้ test จะใช้เพียงคำสั่งเดียว ระบุไดเรกทอรีให้ Bats แล้ว Bats จะค้นหาไฟล์ .bats ทุกไฟล์แบบย้อนลงไปในไดเรกทอรีย่อยด้วยตัวเลือก --recursive

ตัวเลือก --formatter tap จะแสดงผลในรูปแบบ TAP (Test Anything Protocol) ซึ่งระบบ CI จำนวนมากสามารถแยกวิเคราะห์เพื่อรายงานผล test ได้ ส่วนตัวจัดรูปแบบ pretty ที่เป็นค่าเริ่มต้นเหมาะกับการอ่านโดยมนุษย์ในบันทึกดิบมากกว่า

ใช้ --timing เพื่อค้นหา test ที่ทำงานช้าตั้งแต่เนิ่น ๆ — test ที่ใช้เวลานานเกิน 5 วินาทีมักบ่งชี้ว่ามีการเรียกเครือข่ายโดยไม่ต้องการหรือไม่มี mock

# .github/workflows/ci.yml  — Bats test step
- name: Run Bats tests
  run: |
    # If installed as a submodule:
    ./test/bats/bin/bats \
      --recursive \
      --timing \
      tests/

    # If installed via apt or brew (alternative):
    # bats --recursive --timing tests/

เวิร์กโฟลว์สมบูรณ์: ShellCheck + Bats

ตอนนี้ให้นำทุกอย่างมารวมเป็นไฟล์เวิร์กโฟลว์ไฟล์เดียวที่พร้อมใช้จริง แนวปฏิบัติที่ดีที่สุดที่นำมาใช้มีดังนี้:

  • งานที่แยกจากกันสองงาน (lint และ test) ทำงานขนานกัน ทำให้ได้รับผลตอบกลับเร็วขึ้น
  • งาน test ระบุ needs: lint ดังนั้น test จะทำงานหลังจากการตรวจสอบผ่านแล้วเท่านั้น — ช่วยไม่ให้เสียเวลาของตัวเรียกใช้ไปกับโค้ดที่ผิดพลาดอย่างเห็นได้ชัด
  • รุ่นของแอ็กชันที่กำหนดแน่นอน (@v4) ป้องกันความเสียหายที่ไม่คาดคิดจากการอัปเดตต้นทาง
  • บล็อก permissions: จำกัดโทเคนของเวิร์กโฟลว์ให้มีสิทธิ์เท่าที่จำเป็น
# .github/workflows/ci.yml
name: Shell CI

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

jobs:
  lint:
    name: ShellCheck
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run ShellCheck
        run: |
          mapfile -t scripts < <(find . -name '*.sh' -not -path './.git/*')
          [[ ${#scripts[@]} -gt 0 ]] && shellcheck --severity=warning -x "${scripts[@]}"

  test:
    name: Bats Tests
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - name: Run tests
        run: ./test/bats/bin/bats --recursive --timing tests/

แคช Dependencies เพื่อให้การเรียกใช้เร็วขึ้น

เมื่อมีการติดตั้งตัวช่วยของ Bats หรือเครื่องมืออื่นผ่านตัวจัดการแพ็กเกจภายในเวิร์กโฟลว์ การแคชจะช่วยเร่งการเรียกใช้ครั้งถัดไปได้อย่างมาก GitHub Actions มีแอ็กชัน actions/cache สำหรับงานนี้

ประเด็นสำคัญของการแคชอย่างมีประสิทธิภาพ:

  • ใช้คีย์แคชที่มี OS ชื่อเครื่องมือ และแฮชของไฟล์ล็อก — เพื่อให้แคชหมดอายุโดยอัตโนมัติเมื่อ dependencies เปลี่ยนแปลง
  • ทางเลือก restore-keys ช่วยให้เวิร์กโฟลว์ใช้แคชเก่าแทนการเริ่มใหม่ทั้งหมดเมื่อไม่พบแคชที่ตรงกัน
  • สำหรับ Git submodule แทบไม่จำเป็นต้องแคช เพราะการ checkout submodule ทำได้รวดเร็ว แคชมีประโยชน์สูงสุดกับการติดตั้ง npm, pip หรือเครื่องมือแบบคอมไพล์
# .github/workflows/ci.yml  — cache step example
- name: Cache Bats npm helpers
  uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-bats-${{ hashFiles('package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-bats-

- name: Install helpers
  run: npm ci  # uses cache when available

การป้องกันสาขา: บังคับใช้การตรวจสอบที่ผ่านทั้งหมด

เวิร์กโฟลว์ CI ที่ไม่บล็อกการผสาน อย่างมากก็เป็นเพียงคำแนะนำ กฎการป้องกันสาขาของ GitHub จะเปลี่ยนการตรวจสอบของคุณให้เป็นด่านบังคับ

วิธีตั้งค่า: ไปที่ Settings → Branches → Add rule สำหรับ main แล้วเปิดใช้:

  • Require status checks to pass before merging — เลือก ShellCheck และ Bats Tests ตามชื่อ
  • Require branches to be up to date before merging — ป้องกันไม่ให้ PR ที่ผ่านการตรวจสอบบนฐานโค้ดเก่า นำโค้ดที่เสียเข้าสู่สาขาหลัก
  • Do not allow bypassing the above settings — บังคับใช้กฎแม้กับผู้ดูแล repository

เมื่อมีกฎเหล่านี้แล้ว เส้นทางเดียวที่จะผสานได้คือ PR ที่งาน CI ทั้งหมดผ่าน — ซึ่งเป็นตาข่ายนิรภัยที่คุณต้องการอย่างแท้จริง

แก้ไขข้อผิดพลาดของขั้นตอน CI ในเครื่อง

เมื่อการเรียกใช้ CI ล้มเหลว วงจรการแก้ไขที่เร็วที่สุดคือจำลองข้อผิดพลาดในเครื่องก่อน push commit อื่น เทคนิคมีสองแบบ:

  • เรียกใช้คำสั่งเดียวกันทุกประการจากขั้นตอนที่ล้มเหลวในเทอร์มินัล — CI เรียกใช้ Shell ธรรมดา ดังนั้นคำสั่งจึงคัดลอกไปเรียกใช้ซ้ำได้
  • ใช้ act — เครื่องมือที่เรียกใช้เวิร์กโฟลว์ GitHub Actions ในเครื่องภายใน Docker ทำให้สภาพแวดล้อมใกล้เคียงกับตัวเรียกใช้ที่โฮสต์ไว้มากที่สุด

สาเหตุทั่วไปของข้อผิดพลาดที่เกิดเฉพาะใน CI คือรุ่นของเครื่องมือไม่ตรงกันระหว่าง Mac ของคุณ (เช่น find แบบ BSD บน macOS กับ find แบบ GNU บน Ubuntu) ควรทดสอบด้วยตัวเลือก --posix เสมอ หรือใช้ act เพื่อเรียกใช้อิมเมจ Ubuntu ในเครื่อง

#!/usr/bin/env bash
# run_ci_locally.sh  — mimic the CI lint step on your machine
set -euo pipefail

echo '=== ShellCheck ==='
mapfile -t scripts < <(find . -name '*.sh' -not -path './.git/*')
if [[ ${#scripts[@]} -eq 0 ]]; then
  echo 'No .sh files found.'
else
  shellcheck --severity=warning -x "${scripts[@]}"
  echo "Linted ${#scripts[@]} file(s) — OK"
fi

echo '=== Bats ==='
./test/bats/bin/bats --recursive --timing tests/

ตรวจสอบความรู้: แนวคิด Pipeline ของ CI

ทดสอบความเข้าใจของคุณเกี่ยวกับการเชื่อม ShellCheck และ Bats เข้ากับ GitHub Actions

ทบทวน: CI ของ Shell ด้วย ShellCheck และ Bats

ในบทเรียนนี้ คุณได้สร้าง pipeline CI ที่สมบูรณ์สำหรับโครงการ Bash โดยใช้ GitHub Actions สิ่งที่ครอบคลุมมีดังนี้:

  • พื้นฐาน GitHub Actions — YAML ของเวิร์กโฟลว์อยู่ใน .github/workflows/ ทำงานเมื่อเกิด push และ pull_request และเรียกใช้งานบนตัวเรียกใช้ ubuntu-latest
  • ShellCheck — ติดตั้งไว้ล่วงหน้าบนตัวเรียกใช้ Ubuntu ใช้ find เพื่อค้นหาสคริปต์ และใช้ --severity=warning -x เป็นด่านตรวจสอบโค้ดที่เหมาะกับการใช้งานจริง
  • Bats ผ่าน submodule — ตรึง bats-core และตัวช่วยเป็น Git submodule แล้วกู้คืนใน CI ด้วย submodules: recursive ในแอ็กชัน checkout
  • ลำดับงาน — ใช้ needs: เพื่อให้ test ทำงานหลังการตรวจสอบผ่านแล้วเท่านั้น ช่วยให้ได้รับผลตอบกลับเร็วและไม่เสียทรัพยากรการประมวลผลโดยเปล่าประโยชน์
  • การป้องกันสาขา — บังคับใช้การตรวจสอบสถานะในการตั้งค่า GitHub เพื่อไม่ให้ PR ใดเข้าสู่สาขาหลักโดยไม่มี CI ที่ผ่านทั้งหมด
  • การจำลองในเครื่อง — คัดลอกคำสั่ง CI ไปยังเทอร์มินัลโดยตรง หรือใช้ act เพื่อแก้ไขข้อผิดพลาดโดยไม่ต้องสร้าง commit เพิ่ม

เมื่อมี pipeline นี้แล้ว การเปลี่ยนแปลง Shell ทุกครั้งจะได้รับการตรวจสอบโดยอัตโนมัติก่อนแตะต้องสาขาหลักของคุณ

คำถามที่พบบ่อย

บทเรียน “การเรียกใช้การทดสอบ Shell ในไปป์ไลน์ CI” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “การเรียกใช้การทดสอบ Shell ในไปป์ไลน์ CI” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Linux Command Line & Bash Scripting Mastery ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Linux Command Line & Bash Scripting Mastery มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “การเรียกใช้การทดสอบ Shell ในไปป์ไลน์ CI”

เชื่อม ShellCheck และ Bats เข้ากับ GitHub Actions เพื่อให้ทุกการเปลี่ยนแปลงของ Shell ต้องผ่านการตรวจสอบที่สำเร็จ คุณปฏิบัติ Linux Command Line & Bash Scripting Mastery ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Linux Command Line & Bash Scripting Mastery หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน Linux Command Line & Bash Scripting Mastery บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน

บทเรียน “การเรียกใช้การทดสอบ Shell ในไปป์ไลน์ CI” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน Linux Command Line & Bash Scripting Mastery นี้ได้ไหม

ได้ บทเรียน Linux Command Line & Bash Scripting Mastery ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. การทดสอบฟังก์ชันแบบยูนิตด้วย Bats-core
  2. การจำลองคำสั่งและการทำเครื่องมือภายนอกเป็นสตับ
  3. ฟิกซ์เจอร์ สภาพแวดล้อมชั่วคราว และความครอบคลุม
  4. การเรียกใช้การทดสอบ Shell ในไปป์ไลน์ CI
← กลับไปที่ Linux Command Line & Bash Scripting Mastery