4 คะแนน โดย GN⁺ 2025-04-09 | 2 ความคิดเห็น | แชร์ทาง WhatsApp
  • shell ของ GitHub Actions เป็นการตั้งค่าที่กำหนดวิธีรันบล็อก run: แต่ในทางปฏิบัติไม่ได้จำกัดอยู่แค่ รายการเชลล์ที่กำหนดไว้ และยังสามารถชี้ไปยังไฟล์ปฏิบัติการใน $PATH ได้ด้วย
  • ใน workflow เป็นตัวเลือกที่ไม่บังคับ แต่ใน definition ของ action เป็นค่าที่ต้องระบุ และค่าเริ่มต้นจะแตกต่างกันตาม runner เช่น bash บน Linux/macOS และ pwsh บน Windows
  • เมื่อระบุชัดเจนอย่าง shell: bash GitHub จะเติม flag เพิ่มเติม เช่น bash --noprofile --norc -eo pipefail ให้ แต่ตัวเป้าหมายที่ใช้รันเองสามารถเป็นโปรแกรมใดก็ได้
  • หากไฟล์ปฏิบัติการไม่ได้รับอินพุตเป็นไฟล์เดี่ยวโดยตรง ต้องใส่อาร์กิวเมนต์ {0} ในค่า shell โดย GitHub จะแทนที่ด้วย path ของไฟล์ชั่วคราวที่มีบล็อก run อยู่
  • ชื่อที่คุ้นเคยอย่าง bash ก็ถูก resolve จาก $PATH เช่นกัน ดังนั้นการเปลี่ยน $GITHUB_PATH หรือไฟล์ปฏิบัติการปลอมอาจส่งผลต่อวิธีรัน step ถัดไปได้

พฤติกรรมพื้นฐานของคีย์เวิร์ด shell

  • คีย์เวิร์ด shell ของ GitHub Actions ใช้ระบุว่าจะรันบล็อก run: ใดด้วย เชลล์ แบบไหน
  • ใน workflow เป็นตัวเลือกที่ไม่บังคับ แต่ใน definition ของ action ต้องใช้เสมอ
  • เชลล์เริ่มต้นถูกกำหนดตามสภาพแวดล้อมของ runner
    • บน Linux และ macOS โดยทั่วไปคือ bash
    • บน Windows โดยทั่วไปคือ pwsh

การระบุเชลล์อย่างชัดเจนและ flag เพิ่มเติม

  • GitHub ระบุไว้ในเอกสาร defaults.run.shell ว่าเมื่อระบุเชลล์อย่างชัดเจน GitHub จะใช้ flag ที่เลือกไว้ร่วมด้วย
  • ตัวอย่างเช่น หากระบุ shell: bash รูปแบบการรันจะเป็นดังนี้
    • bash --noprofile --norc -eo pipefail
  • หากดูเฉพาะพฤติกรรมนี้ อาจเข้าใจได้ง่ายว่า GitHub ดูแล รายการค่าเชลล์ที่ถูกต้องแบบจำกัด และใส่ flag พิเศษให้เฉพาะค่าเหล่านั้น

ไฟล์ปฏิบัติการใดๆ ใน $PATH ก็เป็นเชลล์ได้

  • ในความเป็นจริง สามารถระบุ ไฟล์ปฏิบัติการใดๆ ที่อยู่ใน $PATH ให้กับ shell ได้
  • GitHub จะรันบล็อก run ด้วยไฟล์ปฏิบัติการที่ระบุ
  • หากคำสั่งนั้นไม่รับอินพุตเป็นไฟล์เดี่ยวโดยตรง ต้องส่ง {0} ในค่า shell
    • GitHub จะเปลี่ยน {0} เป็น path ของไฟล์ชั่วคราว
    • ไฟล์ชั่วคราวนี้มีเนื้อหาของบล็อก run หลังผ่านการขยาย template แล้ว

ตัวอย่างการใช้ C เป็น step runner

  • เครื่องมือที่ทำงานเหมือนคอมไพเลอร์/อินเทอร์พรีเตอร์ C ก็สามารถใช้รัน step ของ GitHub Actions ได้
  • ตัวอย่างที่ระบุ tcc -run {0} เป็น shell ทำงานได้ตามปกติ
- run: sudo apt install -y tcc
- shell: tcc -run {0}
  run: |
    #include <stdio.h>
    int main() {
      printf("Hello, world!\n");
      return 0;
    }

ตัวอย่างการเปลี่ยนการ resolve เชลล์ด้วย $GITHUB_PATH

  • หากแก้ไข $PATH แบบไดนามิกผ่าน $GITHUB_PATH ค่า shell: bash ในภายหลังอาจชี้ไปยังไฟล์ปฏิบัติการที่ต่างจากที่คาดไว้
  • ตัวอย่างนี้สร้างไฟล์ปฏิบัติการชื่อ bash ในไดเรกทอรีปัจจุบัน และเพิ่มไดเรกทอรีปัจจุบันเข้าไปใน $GITHUB_PATH
  • หลังจากนั้นเมื่อใช้ shell: bash GitHub อาจรัน bash ปลอม ที่พบจาก $PATH
- run: |
    touch ./bash
    chmod +x ./bash
    echo '#!/bin/sh' > ./bash
    echo 'echo hello from fake bash' >> ./bash
    echo "${PWD}" >> "${GITHUB_PATH}"
- run: |
    echo "this doesn't do what you expect"
  shell: bash

จุดที่อาจผิดจากความคาดหมายด้านความปลอดภัย

  • ยากที่จะฟันธงว่าพฤติกรรมนี้มีความสำคัญด้านความปลอดภัยหรือไม่
  • ใน GitHub Actions มีเส้นทางจำนวนมากอยู่แล้วที่การเขียนไฟล์นำไปสู่การรันได้ และ GITHUB_ENV ก็เป็นหนึ่งในกรณีเช่นนั้น
  • อย่างไรก็ตาม จุดที่ GitHub lookup ผ่าน $PATH แม้กับ ค่าเชลล์ที่รู้จักกันดี อย่าง bash อาจต่างจากที่คาดไว้
  • โดยเฉพาะหากมีการ inject flag บรรทัดคำสั่งตามค่าเชลล์ที่รู้จักกันดี อาจคาดหวังได้ว่า bash จะถูกตรึงไว้กับ path เฉพาะอย่าง /bin/bash
  • โดยทั่วไปกว่านั้น อาจคิดว่า GitHub อนุญาตเฉพาะเครื่องมือที่ลงทะเบียนไว้ล่วงหน้าใน tool cache แต่พฤติกรรมที่สังเกตได้คือการใช้ไฟล์ปฏิบัติการจาก $PATH

2 ความคิดเห็น

 
tujuc 2025-04-09

ดูแค่รีโป github/runner-image ก็จะเห็นว่ามีแพ็กเกจที่ติดตั้งมาให้และใช้งานได้ทันทีอยู่ค่อนข้างเยอะ....

พอสร้างอิมเมจก็ปาไป 1GB แบบสบาย ๆ....

 
GN⁺ 2025-04-09
ความคิดเห็นจาก Hacker News
  • ผมเคยใช้แฟล็ก -x มาก่อน เพื่อให้ bash แสดงทุกคำสั่งที่รันในเวิร์กโฟลว์ Actions และมันช่วยเรื่อง การดีบัก ได้ค่อนข้างมาก
    https://github.com/jstrieb/just.sh/blob/2da1e2a3bfb51d583be0...
    • อ้างอิงเพิ่มเติม ถ้าเปิด pipefail ไว้ ขั้นตอนจะล้มเหลวเมื่อมีสักคำสั่งใน pipe ล้มเหลว แต่จะไม่มี output ข้อผิดพลาด จนอาจไม่รู้ว่าล้มเหลวเพราะอะไร
      pipefail ก็ไม่ได้ช่วยกันสถานะข้อผิดพลาดที่ซับซ้อนกว่านั้นด้วย ตัวอย่างเช่น ในขั้นตอน curl ... | sudo tar ... ของการตั้งค่า กรณีอย่าง tar แตกไฟล์ล้มเหลว, sudo ล้มเหลว, หรือ shell error จะปรากฏให้เห็น แต่ถ้า curl ล้มเหลวกลางคันเพราะ network error tar อาจคลายไฟล์ไบนารี just ออกมาแค่ครึ่งเดียว แล้ว shell ก็จบการทำงานทันทีได้ ตอนนั้นจะไม่มีข้อความผิดพลาด และไฟล์ executable ที่เสียหายจะยังค้างอยู่บนดิสก์ ถ้าเปิดการข้ามเมื่อผิดพลาดไว้ ก็อาจถึงขั้นพยายามรันมันด้วย
  • ทริก GitHub Actions แบบ private ที่เจ๋งที่เคยเห็นในที่ทำงานคือ สามารถใช้ wildcard เพื่อ match ชื่อ event ของ repository_dispatch ได้
    เขียนได้ประมาณ on: repository_dispatch: - security_scan - security_scan::* ถ้าทำ release pipeline ให้เป็นศูนย์กลาง ก็สามารถบังคับให้แต่ละ repository ผ่าน reusable workflow ที่กำหนดไว้ได้ และถ้าส่ง event อย่าง security_scan::$product_name::$version ก็จะดูในแท็บ Actions ของ repository release กลางได้ง่ายขึ้นมากว่า workflow ของผลิตภัณฑ์และเวอร์ชันไหนกำลังรันอยู่
    • อยากรู้ว่า แนวทางรวมศูนย์ แบบนี้ใช้งานได้ดีจริงไหม องค์กรบังคับให้ทุกอย่าง build ด้วยวิธีเดียวกันเป๊ะ ๆ หรือเปล่า?
      เพิ่งเข้าร่วมองค์กรที่พยายามทำอะไรคล้าย ๆ กัน แต่ในทางปฏิบัติดูแทบไม่มีประโยชน์เลย template มักพังบ่อย และหลายครั้งก็เขียนโดยสมมติวิธี build แบบใดแบบหนึ่งไว้โดยไม่มีเอกสารว่าควร build โค้ดอย่างไร
  • ผมมองว่ายิ่งทำสิ่งต่าง ๆ ใน GitHub Actions น้อยเท่าไรก็ยิ่งดี
    โดยปกติจะชอบใส่ logic ไว้ใน build system อย่าง Make แล้วให้ GitHub Actions แค่เรียกใช้ หรือไม่ก็เขียนโปรแกรม command-line เล็ก ๆ แล้วเรียกใช้ สิ่งพวกนี้ debug บนเครื่อง local ได้ง่ายกว่าใน CI มาก ทริกนี้น่าสนใจ แต่ยังไม่แน่ใจว่าจะมีประโยชน์ตรงไหน
    • workflow ของเราโดยพื้นฐานก็แค่ make build กับ make test ประมาณนั้น
      หลังถูกซื้อกิจการ พอได้ดูไฟล์ workflow ของบริษัทที่เข้ามาซื้อ ก็เห็นว่ายาวหลายร้อยบรรทัดและมีส่วนซ้ำเยอะ จะเรียกว่าโบราณก็ได้ แต่ใน หมู่บ้าน YAML ผมอยากออกมาให้เร็วที่สุดเท่าที่จะทำได้
    • บทความนี้ดูเหมือนไม่ใช่ว่าผู้เขียนแนะนำให้ทำแบบนี้ แต่กำลังบอกว่า มันทำได้
      การรู้ว่าระบบทำอะไรได้บ้างมีประโยชน์ต่อความปลอดภัยและการดีบัก แม้จะเป็นฟีเจอร์ที่ไม่ได้วางแผนจะใช้จริงก็ตาม
    • ตามที่ผมตีความ นี่ไม่มีประโยชน์ และชัดเจนว่าไม่มีประโยชน์ แต่มี ความเสี่ยงด้านความปลอดภัย ที่อาจเกิดขึ้นได้
      โดยเฉพาะเวลาที่ “สำรวจ” self-hosted runner
  • คนรุ่นเราสั่นกลัวเวลาได้รับคำขอให้ย้าย spreadsheet ที่เปลี่ยนไปเรื่อย ๆ ให้เป็นโค้ด
    คนรุ่นถัดไปจะสั่นกลัวเวลาได้รับคำขอให้สร้างวินัยให้กับ deployment ที่ทำด้วย GitHub Actions
    • ตอนนี้กำลังทำงานย้าย enterprise ขนาดใหญ่ไป GH Actions อยู่ ซึ่งให้ตรงกว่านั้นคือ “pipeline แบบ YAML ที่ผูกกับ git” แล้ว วินัย ในที่นี้ควรหน้าตาเป็นอย่างไร?
      ถ้าอธิบายได้ ผมคิดว่าในองค์กรของเราน่าจะทำให้เกิดขึ้นจริงได้
    • อยากรู้ว่าทำไมนี่ถึงไม่มีวินัย
  • ยังสามารถหลอก shell เริ่มต้นอย่าง bash ให้รัน โปรแกรมใด ๆ ก็ได้ ด้วย
  • ถ้าคนอื่นที่อ่าน Action รู้ว่าเกิดอะไรขึ้น ก็ดูมีประโยชน์ทีเดียว
    หลายครั้ง shell script ที่เริ่มจากการย้ายคำสั่งที่พิมพ์มือไม่กี่บรรทัดมาแทบจะตรง ๆ กลายเป็นสัตว์ประหลาดยาวเกินร้อยบรรทัด และทุกครั้งก็อยากให้มี array กับ type จริง ๆ รวมถึงความสามารถแบบ batteries included ของ Python standard library แต่ถึงอย่างนั้นผมก็คงไม่ implement build Action ของบริษัทด้วย elisp หรอก
  • โค้ดของ Github Actions Runner อ่านค่อนข้างง่าย จุดที่กำหนด argument เริ่มต้นของ shell/binary ยอดนิยมอยู่ที่นี่: https://github.com/actions/runner/blob/main/src/Runner.Worke...
    expose ผ่านเมธอด ScriptHandlerHelpers.GetScriptArgumentsFormat ใน ScriptHandler.cs มีโค้ดเตรียม environment ของ process และ argument ส่วนโค้ดเริ่ม process จริงอยู่ที่นี่: https://github.com/actions/runner/blob/main/src/Runner.Worke...
    โดยรวมแล้วผมประหลาดใจในทางที่ดีกับ ความเรียบง่าย ของโค้ดนี้ มันเป็นเชิงขั้นตอนมากและจัดการ edge case จำนวนมาก แต่ดูเข้าใจและดีบักได้ง่าย
  • ในที่สุดก็ใช้ C ใน production CI/CD แล้วเรียกว่า “งานระบบระดับต่ำ” ได้เสียที
    assembly ก็น่าจะทำได้ด้วย
  • เห็นแบบนี้แล้วเริ่มมีความหวังว่าอาจใช้ goeval [1] เพื่อรัน โค้ด Go โดยตรงเป็นงาน CI จากไฟล์ GitHub workflow YAML ได้ง่ายขึ้น
    แต่ goeval ยังไม่รองรับ input เป็นไฟล์โดยตรง รองรับแค่ standard input จึงต้องใช้ shell trick ตอนนี้ใช้แบบ go run github.com/dolmen-go/goeval@v1 - <<'EOF' ... EOF ซึ่งต้องมี boilerplate นิดหน่อย อ้างอิงว่าเป็นผู้เขียน goeval เอง
    [1] https://github.com/dolmen-go/goeval
    • ไม่เข้าใจว่าทำไมต้องใช้ “ทริก” ของ shell ด้วย go run github.com/dolmen-go/goeval@v1 - < file.go ไม่ได้หรือ?
    • ผมเข้าไปดู repository ว่าเป็นโปรแกรมแปลงช่องว่างเป็น tab อัตโนมัติหรือเปล่า แต่ดูแล้วเหมือนจะเน้นไปทาง expression บรรทัดเดียว มากกว่า
      ในบริบทนี้ ถ้าจะโปรโมต toy project น่าจะแสดงตัวอย่างที่เกี่ยวข้องมากกว่า “พิมพ์ hello” จะได้ไม่ต้องเสียคลิก
  • GitHub CI YAML ดีกว่า bash script อย่าง run: pipeline.sh ตรงไหน?
    • มี GitHub API token ที่จัดการให้อัตโนมัติ จึงมีประโยชน์กับ release automation การ publish artifact และ container
      สามารถรันงานพร้อมกันบนหลาย operating system และ CPU architecture ได้ และยังได้ข้อมูล context เกี่ยวกับ event ที่ trigger งานกับสถานะของ repository ด้วย สะดวกสำหรับงานแยกตาม PR หรือ release automation และยัง integrate กับ GitHub web UI ได้ เช่น linter แสดงปัญหาตามแต่ละบรรทัดใน PR หรือ render test failure บนหน้าเว็บ ใช้ cache เล็ก ๆ เพื่อไม่ต้องดาวน์โหลดหรือ build ไฟล์ที่ไม่เปลี่ยนซ้ำได้ด้วย ในอุดมคติ ควรใส่ให้มากที่สุดไว้ในเครื่องมือทั่วไปที่รัน local ได้ และให้ config ของ GitHub CI รับผิดชอบแค่ glue code ที่จำเป็นสำหรับ trigger, caching และ integration กับ GitHub
    • ดูภาพรวมของแต่ละขั้นตอนใน GitHub UI และขยายหรือยุบ output รายขั้นตอนได้
      สามารถนำ GitHub Actions ที่คนอื่นทำไว้มาใช้ใน pipeline ได้ง่าย, ทำ workflow ให้เป็น modular และควบคุม dependency กับ parallel execution ได้ น่าจะมีมากกว่านี้ แต่ข้อดีหลักคือไม่ต้อง implement สิ่งเหล่านี้เอง
    • จากมุมมองของ GitHub ข้อดีคือผู้ใช้มีโอกาสย้ายไป Git forge อื่นน้อยลง