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 ความคิดเห็น
ดูแค่รีโป github/runner-image ก็จะเห็นว่ามีแพ็กเกจที่ติดตั้งมาให้และใช้งานได้ทันทีอยู่ค่อนข้างเยอะ....
พอสร้างอิมเมจก็ปาไป 1GB แบบสบาย ๆ....
ความคิดเห็นจาก Hacker News
-xมาก่อน เพื่อให้ bash แสดงทุกคำสั่งที่รันในเวิร์กโฟลว์ Actions และมันช่วยเรื่อง การดีบัก ได้ค่อนข้างมากhttps://github.com/jstrieb/just.sh/blob/2da1e2a3bfb51d583be0...
pipefail ก็ไม่ได้ช่วยกันสถานะข้อผิดพลาดที่ซับซ้อนกว่านั้นด้วย ตัวอย่างเช่น ในขั้นตอน
curl ... | sudo tar ...ของการตั้งค่า กรณีอย่างtarแตกไฟล์ล้มเหลว,sudoล้มเหลว, หรือ shell error จะปรากฏให้เห็น แต่ถ้าcurlล้มเหลวกลางคันเพราะ network errortarอาจคลายไฟล์ไบนารีjustออกมาแค่ครึ่งเดียว แล้ว shell ก็จบการทำงานทันทีได้ ตอนนั้นจะไม่มีข้อความผิดพลาด และไฟล์ executable ที่เสียหายจะยังค้างอยู่บนดิสก์ ถ้าเปิดการข้ามเมื่อผิดพลาดไว้ ก็อาจถึงขั้นพยายามรันมันด้วยrepository_dispatchได้เขียนได้ประมาณ
on: repository_dispatch: - security_scan - security_scan::*ถ้าทำ release pipeline ให้เป็นศูนย์กลาง ก็สามารถบังคับให้แต่ละ repository ผ่าน reusable workflow ที่กำหนดไว้ได้ และถ้าส่ง event อย่างsecurity_scan::$product_name::$versionก็จะดูในแท็บ Actions ของ repository release กลางได้ง่ายขึ้นมากว่า workflow ของผลิตภัณฑ์และเวอร์ชันไหนกำลังรันอยู่เพิ่งเข้าร่วมองค์กรที่พยายามทำอะไรคล้าย ๆ กัน แต่ในทางปฏิบัติดูแทบไม่มีประโยชน์เลย template มักพังบ่อย และหลายครั้งก็เขียนโดยสมมติวิธี build แบบใดแบบหนึ่งไว้โดยไม่มีเอกสารว่าควร build โค้ดอย่างไร
โดยปกติจะชอบใส่ logic ไว้ใน build system อย่าง Make แล้วให้ GitHub Actions แค่เรียกใช้ หรือไม่ก็เขียนโปรแกรม command-line เล็ก ๆ แล้วเรียกใช้ สิ่งพวกนี้ debug บนเครื่อง local ได้ง่ายกว่าใน CI มาก ทริกนี้น่าสนใจ แต่ยังไม่แน่ใจว่าจะมีประโยชน์ตรงไหน
make buildกับmake testประมาณนั้นหลังถูกซื้อกิจการ พอได้ดูไฟล์ workflow ของบริษัทที่เข้ามาซื้อ ก็เห็นว่ายาวหลายร้อยบรรทัดและมีส่วนซ้ำเยอะ จะเรียกว่าโบราณก็ได้ แต่ใน หมู่บ้าน YAML ผมอยากออกมาให้เร็วที่สุดเท่าที่จะทำได้
การรู้ว่าระบบทำอะไรได้บ้างมีประโยชน์ต่อความปลอดภัยและการดีบัก แม้จะเป็นฟีเจอร์ที่ไม่ได้วางแผนจะใช้จริงก็ตาม
โดยเฉพาะเวลาที่ “สำรวจ” self-hosted runner
คนรุ่นถัดไปจะสั่นกลัวเวลาได้รับคำขอให้สร้างวินัยให้กับ deployment ที่ทำด้วย GitHub Actions
ถ้าอธิบายได้ ผมคิดว่าในองค์กรของเราน่าจะทำให้เกิดขึ้นจริงได้
bashให้รัน โปรแกรมใด ๆ ก็ได้ ด้วยหลายครั้ง shell script ที่เริ่มจากการย้ายคำสั่งที่พิมพ์มือไม่กี่บรรทัดมาแทบจะตรง ๆ กลายเป็นสัตว์ประหลาดยาวเกินร้อยบรรทัด และทุกครั้งก็อยากให้มี array กับ type จริง ๆ รวมถึงความสามารถแบบ batteries included ของ Python standard library แต่ถึงอย่างนั้นผมก็คงไม่ implement build Action ของบริษัทด้วย elisp หรอก
expose ผ่านเมธอด
ScriptHandlerHelpers.GetScriptArgumentsFormatในScriptHandler.csมีโค้ดเตรียม environment ของ process และ argument ส่วนโค้ดเริ่ม process จริงอยู่ที่นี่: https://github.com/actions/runner/blob/main/src/Runner.Worke...โดยรวมแล้วผมประหลาดใจในทางที่ดีกับ ความเรียบง่าย ของโค้ดนี้ มันเป็นเชิงขั้นตอนมากและจัดการ edge case จำนวนมาก แต่ดูเข้าใจและดีบักได้ง่าย
assembly ก็น่าจะทำได้ด้วย
แต่ 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
go run github.com/dolmen-go/goeval@v1 - < file.goไม่ได้หรือ?ในบริบทนี้ ถ้าจะโปรโมต toy project น่าจะแสดงตัวอย่างที่เกี่ยวข้องมากกว่า “พิมพ์ hello” จะได้ไม่ต้องเสียคลิก
run: pipeline.shตรงไหน?สามารถรันงานพร้อมกันบนหลาย 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 Actions ที่คนอื่นทำไว้มาใช้ใน pipeline ได้ง่าย, ทำ workflow ให้เป็น modular และควบคุม dependency กับ parallel execution ได้ น่าจะมีมากกว่านี้ แต่ข้อดีหลักคือไม่ต้อง implement สิ่งเหล่านี้เอง