นอกจากเราควรต้องเอาใจใส่ต่อการใช้ Git ที่เหมาะสมกับทีมแล้ว อีกสิ่งหนึ่งที่ขาดไม่ได้ก็คือการเอาใจใส่กับฟีเจอร์ต่างๆ ที่ git repository provider แต่ละเจ้านั้นมีให้เราใช้ ซึ่งฟีเจอร์หนึ่งที่ควรเรียนรู้เสียอย่างหลีกเลี่ยงไม่ได้ก็คืองาน automation และ GitHub เองก็มีสิ่งที่เรียกว่า GitHub Actions มาให้เราใช้งานเช่นกัน
GitHub Actions ทำให้เราสามารถทำอะไรบางอย่างได้แบบอัตโนมัติ หลังจากเกิดเหตุการณ์ (event) ที่เราสนใจใน Repository ของเรา เช่น เมื่อมีคนเปิด pull request ก็จะให้รัน test ให้เราก่อนว่า pull request นั้นทำอะไรของเราพังไปบ้าง หรือถ้ามีคน push เข้ามาที่ branch main ก็ให้ deploy ไปที่ host ที่เราต้องการโดยอัตโนมัติทันที เป็นต้น
สิ่งเหล่านี้ที่กล่าวมา เราเรียกมันว่า workflow แต่ละ workflow ประกอบไปด้วยงาน (job) ต่างๆ ซึ่งแต่ละงานอธิบายว่ามีขั้นตอน (step) อะไรที่ต้องทำ (action) บ้าง
แปลว่าถ้าเราต้องการให้ทำอะไรสักอย่างหลังจากมีเหตุการณ์ที่เราสนใจเกิดขึ้น เราต้องสร้าง workflow
สร้าง Workflow
workflow จะทำงาน (job) ต่างๆ ตาม event ที่เรากำหนดไว้ ดังนั้นเมื่อใดที่มี event ที่ว่า เมื่อนั้น workflow ทำงาน
เราจะสร้าง workflow ไว้ใน .github/workflows/
โดยเขียนไว้ในไฟล์ .yml
ยกตัวอย่าง ถ้าเราต้องการสร้าง workflow ที่จะทำงานก็ต่อเมื่อมีใครสักคน push code ขึ้นมาที่ branch master
ก็จะสร้างไฟล์ .yml
ได้ดังนี้
name: My Workflow
on:
push:
branches:
- master
หรือถ้าจะให้ทำงานทุกครั้งที่มีการ push โดยไม่กำหนด branch ก็เขียนได้สั้นลงได้แบบนี้
name: My Workflow
on: [push]
สร้างงานให้ workflow
workflow ต้องทำงาน (job) และแต่ละงานก็ต้องบอกด้วยว่า มีขั้นตอน (step) ว่าต้องทำอะไรบ้าง ยกตัวอย่างเช่น ถ้าเราต้องการสร้าง งาน (job) ชื่อ build และงานนี้จะรันบน Ubuntu เราสามารถเขียน ได้ดังนี้
name: My Workflow
on: [push]
jobs:
build:
runs-on: ubuntu-latest
สร้างขั้นตอนการทำงาน
หลังจากเรานิยามไปแล้วว่าเราจะทำงานชื่อ build
เราก็ต้องบอกด้วยว่างานนี้มีขั้นตอนการทำงานอย่างไรบ้าง โดยการใช้คำสั่ง steps
ในการกำหนดขั้นตอนการทำงานดังนี้
name: My workflow
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- run: npm install -g bats
- run: bats -v
จากโค้ดจะเห็นว่ามีขั้นตอนการทำงานทั้ง 4 ขั้นตอน แต่ละขั้นตอนจะทำงานตามลำดับก่อนหลัง โดยที่ 2 ขั้นตอนแรกใช้คำสั่ง uses
และสองขั้นตอนหลังใช้คำสั่ง run
การใช้คำสั่ง uses
มีความหมายว่าให้รัน action ตามที่ระบุไว้ กรณีนี้คือ ขั้นตอนแรกให้รัน action จาก actions/checkout@v2
พอรันเสร็จแล้ว ขั้นตอนที่สองก็ให้รัน action จาก actions/setup-node@
v1 ต่อทันที
ขั้นตอนที่ 3 และ 4 เป็นการบอกให้ Runner รันคำสั่ง npm install -g bats
และ bats -v
ตามลำดับ
Runner คือคนที่กำลังรัน Job นั้นๆ อยู่ ซึ่งนั่นแปลว่า ถ้าใน workflow ใดๆ มี Job มากกว่า 1 Job ก็จะมี Runner มากกว่า 1 Runner แต่ละ Runner ก็รับผิดชอบ Job ของใครของมันแยกกัน
แต่ละ Job ใน workflow ใดๆ จะทำงาน parallel กันโดย default แต่ถ้าเราต้องการให้ job หนึ่งจะทำงานได้ก็ต่อเมื่ออีก job หนึ่งทำงานเสร็จก่อน เราสามารถใช้คำสั่ง needs
ได้ ยกตัวอย่างเช่น เราต้องการให้ job setup เสร็จก่อน แล้ว job build ถึงจะทำงานต่อได้ ก็เขียนได้ดังนี้
jobs:
setup:
runs-on: ubuntu-latest
steps:
- run: ./setup_server.sh
build:
needs: setup
steps:
- run: ./build_server.sh
Marketplace
มาถึงตรงนี้ เราจะพบว่า ในแต่ละขั้นตอน (step) นั้น เราสามารถสั่งให้รัน action หรือรัน command ก็ได้ ซึ่งการรัน command นั้นก็คือ command ทั่วไปที่อยู่ใน Virtual Machine กรณีที่ยกตัวอย่างมาก็คือ Ubuntu
ส่วน action นั้นเราจะรู้ได้อย่างไรว่ามี action อะไรไว้ให้เราใช้แล้วบ้าง?
เราสามารถไปหา action ต่างๆ ที่มีคนทำไว้ให้เราแล้วได้ที่ GitHub Marketplace ยกตัวอย่างเช่นหากเราต้องการ อัพโหลดโปรเจ็คของเราไปวางไว้บน server ที่เราต้องการ พอลองหาดู เราก็พบว่า น่าจะใช้ action ชื่อ ssh deploy ตัวนี้ก็ได้
แต่ละ action สามารถรับ input หรือกำหนดค่าต่างๆ ก่อนเริ่มทำงานได้ และ action ssh deploy ตัวนี้ก็เช่นเดียวกัน เมื่อเราไปดูตรงส่วนของวิธีใช้งาน เราก็จะพบว่านอกจากเราต้องใช้คำสั่ง uses
เพื่อบอกว่าให้รัน action ssh deploy ตัวนี้แล้ว เรายังต้องกำหนดค่าให้กับ env
อีก ดังนี้
- name: Deploy to Staging server
uses: easingthemes/ssh-deploy@v2.1.5
env:
SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}
ARGS: "-rltgoDzvO"
SOURCE: "dist/"
REMOTE_HOST: ${{ secrets.REMOTE_HOST }}
REMOTE_USER: ${{ secrets.REMOTE_USER }}
TARGET: ${{ secrets.REMOTE_TARGET }}
Secrets
จากโค้ดที่ยกตัวอย่างไปเราจะเห็นการใช้ตัวแปร secrets
ใน workflow ทั้งที่จริงๆ แล้วตรงนี้เราสามารถกำหนดเป็นค่าคงที่ลงไปได้เลย แต่บางค่าที่ค่อนข้าง sensitive เราจะเข้าไปกำหนดไว้ใน Settings → Secrets ของ GitHub project เรา หลังจากนั้นเราจะสามารถเรียกใช้ค่าเหล่านี้ได้ผ่านตัวแปร secrets
ค่าที่เรากำหนดไว้ใน Secrets นั้นเมื่อกำหนดแล้วจะไม่สามารถเปิดดูได้ว่ากำหนดค่าอะไรไป ทำได้แค่ลบ หรืออัพเดทเป็นค่าใหม่เท่านั้น
สร้าง Action เอง
หาก action ต่างๆ ใน GitHub Marketplace นั้นยังไม่ถูกใจเรา เราก็สามารถสร้าง action ขึ้นมาเองได้ โดยที่ action จะแบ่งออกเป็น 3 แบบได้แก่ action ที่รันใน Docker container, รันด้วย JavaScript และ action ที่รัน command ตามปกติ
แต่ละ action เราจำเป็นต้องมีไฟล์ action.yml
ทำหน้าที่เป็น metadata file ที่ใช้กำหนด input, output และ environment variable ของ action
ไม่ว่า action ที่เราสร้างจะเป็น Docker container action หรือ JavaScript action หรือ command line action ก็จะมีเนื้อหาในไฟล์ action.yml
เกี่ยวกับวิธีกำหนด inputs, outputs แบบเดียวกัน แต่จุดที่ต่างกันหลักๆ ก็คือตอนสั่งรันด้วยคำสั่ง runs
using
:
ตัวอย่างตรงคำสั่ง runs
ถ้าเป็น Docker action
runs:
using: 'docker'
image: 'Dockerfile'
args:
- ${{ inputs.who-to-greet }}
ตัวอย่างตรงคำสั่ง runs
ถ้าเป็น JavaScript action
runs:
using: 'node12'
main: 'index.js'
ตัวอย่างตรงคำสั่ง runs
ถ้าเป็น command line ปกติ
runs:
using: "composite"
steps:
- run: echo Hello ${{ inputs.who-to-greet }}.
shell: bash
- run: ${{ github.action_path }}/goodbye.sh
shell: bash
ต่อไปนี้คือตัวอย่าง เต็มๆ ของ metadata file ของ JavaScript Action ที่เราประกาศส่วน inputs
, outputs
และ runs
name: 'Hello World'
description: 'Greet someone and record the time'
inputs:
who-to-greet: # id of input
description: 'Who to greet'
required: true
default: 'World'
outputs:
time: # id of output
description: 'The time we greeted you'
runs:
using: 'node12'
main: 'index.js'
who-to-greet
คือชื่อ input ที่รับเข้ามา ซึ่งเราต้องใช้ตอนเรียกใช้ action นี้ใน workflow และ time
คือชื่อ output ที่เราสามารถอ้างดึงได้ใน workflow และ action นี้รันไฟล์ index.js
JavaScript action จะมี package มาช่วยให้เราสร้าง action ได้ง่ายๆ 2 ตัวคือ @actions/core
และ @actions/github
ดังนั้นเวลาเราเขียน action เราต้อง import มันเข้ามาใช้งานด้วย ดังนี้
const core = require('@actions/core');
const github = require('@actions/github');
try {
// `who-to-greet` input defined in action metadata file
const nameToGreet = core.getInput('who-to-greet');
console.log(`Hello ${nameToGreet}!`);
const time = (new Date()).toTimeString();
core.setOutput("time", time);
// Get the JSON webhook payload for the event that triggered the workflow
const payload = JSON.stringify(github.context.payload, undefined, 2)
console.log(`The event payload: ${payload}`);
} catch (error) {
core.setFailed(error.message);
}
เราจะเห็นว่า action นี้อ่าน input ชื่อ who-to-greet
เข้ามา และบันทึกผลลัพธ์ไว้ในตัวแปร time
สอดคล้องกับในไฟล์ action.yaml
ที่ได้นิยาม input/output เอาไว้ตอนแรก
ตอนใช้งาน เราก็สามารถกำหนด input และอ้างดึง output ได้เลย โดยที่เราจำเป็นต้องกำหนด id
ใก้กับ action ไว้ด้วย เพื่อให้เราสามารถอ้างถึง output ได้
เช่น เรากำหนด id เป็น hello
เวลาเราจะอ้างถึง output ของ action ที่มี id นี้เราก็สามารถเขียนได้ว่า
steps.hello.outputs
ตัวอย่างการใช้งาน action และการอ้างถึง output ของ action
steps:
- name: Hello world action step
uses: ./ # Uses an action in the root directory
id: hello
with:
who-to-greet: 'Mona the Octocat'
- name: Get the output time
run: echo "The time was ${{ steps.hello.outputs.time }}"
ส่งค่าระหว่าง Job
หากใน workflow เดียวกันมี job มากกว่า 1 job เช่นมี job แรกชื่อ build และ job ที่ 2 คือ deploy ซึ่ง job deploy นี้จะต้องอัพโหลดผลที่ได้จาก job build เราจะใช้ Artifacts เก็บผลลัพธ์ที่ได้จาก job build
เราจะใช้ Artifacts เก็บผลที่ได้จาก job build โดยใช้ action ชื่อ actions/upload-artifact@v2
สำหรับเก็บผลลัพธ์ และ job deploy จะใช้ action/download-artifact@v2
สำหรับอ่านค่าออกมาใช้งาน
ตัวอย่างต่อไปนี้คือการสร้าง 2 job โดยที่ job แรกชื่อ build และได้ผลลัพธ์ออกมาเป็น directory ชื่อ dist
จานั้นบันทึกเก็บไว้ใน Artifacts
และเมื่อ build เสร็จแล้ว job deploy จะอ่านไฟล์ dist
ออกมาจาก Artifacts และอัพโหลดขึ้นไป deploy บน server
name: Auto Deploy to my server
on:
push:
branches:
- master
jobs:
build:
name: 'Build App'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: 'Install dependency'
run: |
npm install
npm run build
- name: 'Archive production artifacts'
uses: actions/upload-artifact@v2
with:
name: 'dist'
path: dist
deploy:
needs: build
name: 'Deploy to production server'
runs-on: ubuntu-latest
steps:
- name: 'Download artifact'
uses: actions/download-artifact@v2
with:
name: dist
- name: 'Deploy to Production Server'
uses: easingthemes/ssh-deploy@v2.1.5
env:
SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}
ARGS: "-rltgoDzvO --delete"
SOURCE: "."
REMOTE_HOST: ${{ secrets.REMOTE_HOST }}
REMOTE_USER: ${{ secrets.REMOTE_USER }}
TARGET: ${{ secrets.REMOTE_TARGET }}
สรุปเรื่อง GitHub Actions
ในบทความนี้ได้แนะนำการใช้งาน GitHub Actions โดยอธิบายให้เห็นภาพว่า เราสามารถสร้าง workflow ขึ้นมา โดยแต่ละ workflow จะ trigger ตาม event ที่กำหนดไว้
ใน workflow หากเรามีข้อมูลที่ sensitive และไม่อยาก push ขึ้นไปใน repository เราสามารถไปกำหนดไว้ได้ที่ Settings → Secret และเมื่อกำหนดแล้วเราจะไม่สามารถกลับไปเปิดดูค่าได้อีก นอกจากจะลบ หรืออัพเดทใหม่เท่านั้นที่ทำได้
แต่ละ workflow ประกอบไปด้วย Job อย่างน้อย 1 Job แต่ละ Job รัน parallel กัน แต่ก็สามารถให้รอกันได้ โดยใช้คำสั่ง needs
แต่ละ Job ประกอบไปด้วย steps ที่แต่ละ step ก็จะบอกว่าให้ทำอะไร รันคำสั่งอะไร หรือรัน action ไหน
แต่ละ action เราสามารถกำหนด input, environment variable ให้ได้ก่อนรัน และสามารถอ่าน output ของ action ออกมาใช้งานต่อได้
เราจะหา action ที่คนอื่นทำไว้แล้วได้ใน GitHub Marketplace มาใช้ได้เลย หรือจะสร้าง action ขึ้นมาเองก็ได้
การสร้าง action จะต้องมี metadata file ชื่อ action.yml
เพื่อนิยามชนิดของ action, input และ output
ใช้ Artifacts ในการส่งผลลัพธ์ให้กันระหว่าง Job
ส่งท้าย
ในบทความนี้เป็นเพียงไกด์ไลน์สรุปย่อเกี่ยวกับการใช้งาน GitHub Actions เพื่อให้สามารถทำความเข้าใจและจับจุดได้เท่านั้น ยังมีอีกหลายเรื่องที่น่าสนใจที่ยังไม่ได้กล่าวถึง เช่น Security hardening และ Managing workflow runs เป็นต้น
1 comment
[…] GitHub Action คืออะไร […]
Comments are closed.