Skip to content
CodeSook
CodeSook

22. Composite Actions


Composite Actions คืออะไร

เวลาเรามี Workflows หลายๆอัน เราก็มักจะมี steps ที่ทำงานซ้ำๆกัน เราก็อาจจะ copy steps ไปมา

Compostie actions จะช่วยให้เรารวม steps ต่างๆที่ต้องใช้ซ้ำในหลายๆจุด มาไว้ที่เดียว ที่ Workflow ก็เรียกใช้งานแค่จุดเดียว steps เหล่านี้ก็จะทำงาน ถ้าเปรียบกับการโค้ดมันก็เหมือนเรา refactor น่ะแหละ

ยกตัวอย่างเช่น
เรามีโปรเจคที่ใช้ Node.js เราก็อาจจะมี 1 Workflow ที่มีหลายๆ Jobs เช่น Job Lint, Job Test, Job Deploy
ซึ่งแต่ละ Job จะมี steps ที่คล้ายๆกันเช่น

  1. checkout
  2. install dependencies
  3. caching
  4. run node command ที่เกี่ยวข้องกับ Job นั้นๆ

ก็สามารถเอา 3 steps แรก checkout, install dependencies, caching มารวมไว้ที่ custom action แบบ Composite actions แล้วแต่ละ Job ก็เรียกใช้ custom action ตัวนี้ แล้วตามด้วย run command ทำให้ steps ใน Job สั้นลง ไปยาวที่ custom action แทน ฮ่าๆ

พอใช้ซ้ำแบบนี้ได้ ทำให้เราเขียน Workflows ได้เร็วขึ้นด้วย

โครงสร้างของ Composite actions

Terminal window
.github
├── actions
└── <folder your action name>
└── action.yaml
└── workflows
├── deploy.yaml
└── lint.yaml

Hands-on

มาลงมือทำกันเลย
ยังอยู่ที่โปรเจคเดิมนะ

starter workflow

จะเริ่มต้นด้วยการสร้าง workflow 2 อัน

  1. Lint
  2. Deploy

เราแค่ 2 ก่อนละกัน

.github/workflows/lint.yaml
lint.yaml
name: ESLint
on:
push:
workflow_dispatch:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Get Code
uses: actions/checkout@v4
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.8
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
- name: Install Dependencies
run: bun i
- name: Run ESLint
run: bun run lint
.github/workflows/deploy.yaml
deploy.yaml
name: Deploy Project
on:
push:
workflow_dispatch:
# pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Get Code
uses: actions/checkout@v4
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.18
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
- name: Install Dependencies
run: bun i
- name: Build
run: bun run build
- name: upload dist folder
uses: actions/upload-artifact@v4
with:
name: dist
path: |
dist
public
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: download dist folder
uses: actions/download-artifact@v4
with:
name: dist
- name: List all files
run: ls -R
- name: Deploy
run: |
echo "Deploying version ${{ needs.build.outputs.package-version }}..."
echo "Commit: ${{ needs.build.outputs.build-hash }}"

make Composite Action

เราจะเอา steps

  1. install bun
  2. cache dependencies
  3. install dependencies

มาดู code กันก่อนเลย

.github/actions/install-and-caching/action.yaml
install-and-caching/action.yaml
name: Install and Cache Dependencies
description: Install and cache dependencies
runs:
using: composite
steps:
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.8
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
- name: Install Dependencies
shell: bash # when use `run:` you need to specify shell
run: bun i

เราจะต้องใส่

  • name ชื่อแหละ
  • description คำอธิบาย
  • runs ตัวแปรที่จะรับเข้ามา
    • using: composite คือเราจะสร้าง composite action
    • steps: ตรงนี้เป็นการเขียน steps เหมือนเดิมเลย
      • run: ตรงที่ใช้ run คือเราจะใส่ shell command เองใช่มะ ตรงนี้เราจำเป็นต้องใส่ว่าจะใช้ shell ยี่ห้ออะไร ส่วนใหญ่ก็จะใช้ bash แหละ

How to use composite actions

ทีนี้มาดูกันว่าพอมี Composite actions แล้ว เราจะเอามาใช้ยังไงดี

ไม่ยากเลย เราแค่ใช้ uses: แล้วใส่ path ไปที่ action ของเรา

ตอนนี้เรามี files แบบนี้

Terminal window
.github
├── actions
└── install-and-caching
└── action.yaml
└── workflows
├── deploy.yaml
└── lint.yaml

เราแก้ workflow แบบนี้

.github/workflows/lint.yaml
name: ESLint
on:
push:
workflow_dispatch:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Get Code
uses: actions/checkout@v4
- name: Install and cache dependencies
uses: ./.github/actions/install-and-caching
- name: Run ESLint
run: bun run lint
.github/workflows/deploy.yaml
name: Deploy Project
on:
push:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Get Code
uses: actions/checkout@v4
- name: Install and cache dependencies
uses: ./.github/actions/install-and-caching
- name: Build
run: bun run build
- name: upload dist folder
uses: actions/upload-artifact@v4
with:
name: dist
path: |
dist
public
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: download dist folder
uses: actions/download-artifact@v4
with:
name: dist
- name: List all files
run: ls -R
- name: Deploy
run: |
echo "Deploying version ${{ needs.build.outputs.package-version }}..."
echo "Commit: ${{ needs.build.outputs.build-hash }}"

Add inputs to custom actions

เราสามารถส่ง parameters เข้ามายัง custom actions ของเราผ่าน inputs ได้ โดยเราจะต้องระบุใน action.yaml

ใน custom actions ที่เราได้สร้างไปก่อนหน้า เราใช้ cache dependencies ด้วยใช่มะ
เราจะเพิ่ม parameters เข้าไปเพื่อให้ Workflow ที่เอา custom action นี้ไปใช้ สามารถกำหนดได้ว่าต้องการใช้ caching หรือไม่ โดยถ้าไม่กำหนดจะถือว่าต้องการ caching

.github/actions/install-and-caching/action.yaml
name: Install and Cache Dependencies
description: Install and cache dependencies
inputs:
caching:
description: Enable caching (default = true)
required: false
default: "true"
runs:
using: composite
steps:
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.8
- name: Cache dependencies
if: ${{ fromJSON(inputs.caching) == true }}
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
- name: Install Dependencies
shell: bash # when use `run:` you need to specify shell
run: bun i

ตรงนี้เราเพิ่ม inputs: เข้าไป

  • caching คือชื่อจะตั้งอะไรก็ได้เลยตามชอบ
  • description เป็นคำอธิบายสำหรับ action นี้แบบสั้นๆ
  • required: ใช้กำหนดว่า input นี้บังคับให้คนที่เอา custom action ตัวนี้ไปใช้นั้น ต้องระบุค่า inputs นี้หรือเปล่า
  • default: อันนี้เป็นค่า default ของ inputs นี้ ถ้าไม่บอกก็จะใช้ค่านี้เลย

มาดู Workflow กันบ้าง

เราเพิ่มแค่ with: แล้วก็ใส่ชื่อ inputs ให้ตรงกัน ก็จะใช้ได้แล้ว

.github/workflows/lint.yaml
name: ESLint
on:
push:
workflow_dispatch:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Get Code
uses: actions/checkout@v4
- name: Install and cache dependencies
uses: ./.github/actions/install-and-caching
with:
caching: false
- name: Run ESLint
run: bun run lint

ส่วน deploy workflow เราจะไม่แก้อะไร นั่นคือเราไม่ใส่ with: ก็จะทำให้มันใช้ค่า default ละ

ตรงนี้ถ้าลอง push code ดู แล้วเปิดดู lint workflow มันทำงาน
เราอาจจะตกใจว่า เฮ้ย มี cache hit อยู่ใน log ด้วยนะ นั่นหมายความว่าที่เราตั้ง caching: false ไว้มันไม่ทำงานอย่างนั้นหรอ ทำไมยังเจอ cache-hit อยู่ละ

ทีแรกผมก็งง
แต่ว่า จริงๆแล้วที่ทำไปนั้นถูกต้องแล้ว แล้ว cache action ก็ไม่ได้ทำงานเพราะเข้าเงื่อนไข if นั่นแหละ ทั้งหมดถูกต้อง

สิ่งที่เราต้องรู้คือ action setup bun มันทำ caching ให้ด้วย ฉนั้นที่เราเห็นว่า cache-hit นั้น มันเป็นของ action setup bun ต่างหากละ

ผมแคปรูปมาให้ดูเป็นตัวอย่าง

lint workflow ที่เราใส่ caching: false

ที่ลูกศรสีแดง เรา setup bun แล้วก็มา bun install ต่อเลย

ลูกศรสีเหลืองมี cache key ให้เห็นด้วย


มาดู deploy workflow กันบ้าง อันนี้เราใช้ค่า default นั่นคือ cache จะต้องทำงาน

ลูกศรสีแดง จะเห็นว่ามี bun setup แล้วก็ต่อด้วย cache แล้วจึงเจอ bun install ตรงนี้จะเห็นว่า action cache ทำงานแล้ว

ลูกศรสีเหลือง จะเห็น cache key เหมือนกัน

ลูกศรสีเขียว จะเห็น cache key แต่อันนี้คือ key ที่ cache action มันใช้งาน

Custom action outputs

เมื่อ custom action ทำงานเสร็จ เราสามารถกำหนด outputs ได้ด้วย คล้ายๆกับการที่เรามี function แล้วใส่ return value ในตอนจบ
เผื่อว่าคนที่เอา custom action ของเราไปใช้งาน อยากจะได้ outputs อะไรบางอย่างไปใช้งานต่อ ส่วนมากก็จะเป็น text ธรรมดานี่แหละ
outputs จะเก็บ data ในรูปของ Key - Value นะ

add cache-hit output

เราจะให้ custom action ของเราส่ง cache-hit: boolean ออกมาหลังทำงานเสร็จ

อันนี้จะซับซ้อนหน่อยนะ จะค่อยๆแก้ไปทีละส่วนนะ

.github/actions/install-and-caching/action.yaml
action.yaml
name: Install and Cache Dependencies
description: Install and cache dependencies
inputs:
caching:
description: Enable caching (default = true)
required: false
default: "true"
outputs:
cache-hit: # define output key
description: Cache hit
value: unknown # this will change later
runs:
using: composite
steps:
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.8
- name: Cache dependencies
id: cache-deps # add id for future reference
if: ${{ fromJSON(inputs.caching) == true }}
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
- name: Install Dependencies
shell: bash
run: bun i
- name: set output cache-hit
id: set-cache-hit
shell: bash
run: echo "cache-hit=${{ steps.cache-deps.outputs.cache-hit || 'false' }}" >> $GITHUB_OUTPUT

ผมจะค่อยๆอธิบายสิ่งที่เพิ่มเติมเข้ามานะครับ

  • outputs
    • cache-hit อันนี้เป็นชื่อ output ซึ่งคนที่อยากจะได้ค่าไปใช้ต่อจะใช้อ้างอิงที่ชื่อตรงนี้แหละ
      • description คำอธิบายของ output นี้
      • value เป็นค่าของ output ที่เราจะใส่ ในที่นี้เดี๋ยวจะมาใส่อีกที มันซับซ้อนอะ

มาที่ส่วน steps กันต่อ

จะมี steps ที่เราใช้ action actions/cache@v4
ซึ่ง action ตัวนี้จะให้ output ที่ชื่อว่า cache-hit ซึ่งผมไปอ่านใน doc ของเค้ามา
ที่ step นี้เราจะเพิ่ม id เข้าไปด้วย เพื่อใช้อ้างอิง output cache-hit นี้ใน step อื่นๆต่อไป
เราไม่สามารถใช้ name ในการอ้างอิงเพื่อเข้าถึง output ได้นะ

มาที่ step ด้านล่างสุด
สร้าง step เพื่อส่ง output ของ custom action ของเรา

- name: set output cache-hit
id: set-cache-hit
shell: bash
run: echo "cache-hit=${{ steps.cache-deps.outputs.cache-hit || 'false' }}" >> $GITHUB_OUTPUT

เราจะส่ง output ผ่าน echo ธรรมดาเลย ฉนั้นเราก็จะใช้ run นี่แหละ
พอใช้ run เราก็ต้องระบุ shell ด้วยนะ ส่วนวิธีการส่ง output เราจะใช้

Terminal window
echo <key>=<value> >> $GITHUB_OUTPUT

จากตัวอย่างของเราก็จะเป็น

Terminal window
echo "cache-hit=${{ steps.cache-deps.outputs.cache-hit || 'false' }}" >> $GITHUB_OUTPUT

ในส่วนของ value ก็จะใช้ expression โดยเราจะเข้าถึง steps -> step's id -> output -> cache-hit

  • step's id จะใช้ id ที่เราพึ่งใส่เข้าไป cache-deps
  • output ก็คือเราจะถึง output จาก step นั้นๆมาใช้งาน
  • cache-hit เราตั้งชื่อ key ว่า cache-hit ซึ่งจะเป็นอะไรก็ได้นะ
  • || 'false' ถ้าไม่มี output ก็ให้ใส่ false ไปซะ ซึ่งปกติควรจะมี true หรือ false
  • >> $GITHUB_OUTPUT เป็นวิธีที่ github action กำหนดมาเวลาเราอยากส่ง output ออกไปจาก custom action ของเรา

ต่อมา เราจะเอาใส่ value จากที่ค้างไว้

name: Install and Cache Dependencies
6 collapsed lines
description: Install and cache dependencies
inputs:
caching:
description: Enable caching (default = true)
required: false
default: "true"
outputs:
cache-hit:
description: Cache hit
value: ${{ steps.set-cache-hit.outputs.cache-hit }}
runs:
20 collapsed lines
using: composite
steps:
- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.2.8
- name: Cache dependencies
id: cache-deps
if: ${{ fromJSON(inputs.caching) == true }}
uses: actions/cache@v4
with:
path: |
~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
- name: Install Dependencies
shell: bash # when use `run:` you need to specify shell
run: bun i
- name: set output cache-hit
id: set-cache-hit
shell: bash
run: echo "cache-hit=${{ steps.cache-deps.outputs.cache-hit || 'false' }}" >> $GITHUB_OUTPUT

เราต้องระบุด้วยว่า value ที่จะใช้งานจะมาจากไหน
ในที่นี้คือมาจาก output ใน step set-cache-hit
แล้วก็เข้าถึง outputs.cache-hit

ที่ต้องทำแบบนี้เพราะ Composite action จะไม่สามารถเข้าถึง output ของ step ได้โดยตรง
และบังคับให้เราต้องระบุ output แบบชันเจน

เราจะใช้ output ยังไง

เราจะเรียกใช้ output ที่ workflow lint กับ deploy

มาดู lint workflow ก่อน
ใน lint workflow นี้เราระบุว่าไม่ต้องใช้ cache ใน with: caching: false เราเพิ่ม id แล้วอ้างอิงไปที่ output ง่ายๆเลย

name: ESLint
11 collapsed lines
on:
push:
workflow_dispatch:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Get Code
uses: actions/checkout@v4
- name: Install and cache dependencies
id: cache-deps
uses: ./.github/actions/install-and-caching
with:
caching: false
- name: Report cache hit
run: echo ${{ steps.cache-deps.outputs.cache-hit }}
3 collapsed lines
- name: Run ESLint
run: bun run lint

ส่วน deploy workflow ก็จะเขียนเหมือนกันเลย เราไม่ได้ระบุว่าไม่ต้องใช้ cache แสดงว่า workflow นี้จะใช้ cache ได้

name: Deploy Project
12 collapsed lines
on:
push:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Get Code
uses: actions/checkout@v4
- name: Install and cache dependencies
id: cache-deps
uses: ./.github/actions/install-and-caching
- name: Report cache hit
run: echo ${{ steps.cache-deps.outputs.cache-hit }}
28 collapsed lines
- name: Build
run: bun run build
- name: upload dist folder
uses: actions/upload-artifact@v4
with:
name: dist
path: |
dist
public
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: download dist folder
uses: actions/download-artifact@v4
with:
name: dist
- name: List all files
run: ls -R
- name: Deploy
run: |
echo "Deploying version ${{ needs.build.outputs.package-version }}..."
echo "Commit: ${{ needs.build.outputs.build-hash }}"

ลอง push code แล้วดูที่หน้าเว็ป

lint workflow เราจะเห็น false

deploy workflow เราจะเห็น true