22. Composite Actions
Composite Actions คืออะไร
เวลาเรามี Workflows หลายๆอัน เราก็มักจะมี steps ที่ทำงานซ้ำๆกัน เราก็อาจจะ copy steps ไปมา
Compostie actions จะช่วยให้เรารวม steps ต่างๆที่ต้องใช้ซ้ำในหลายๆจุด มาไว้ที่เดียว ที่ Workflow ก็เรียกใช้งานแค่จุดเดียว steps เหล่านี้ก็จะทำงาน ถ้าเปรียบกับการโค้ดมันก็เหมือนเรา refactor น่ะแหละ
ยกตัวอย่างเช่น
เรามีโปรเจคที่ใช้
ซึ่งแต่ละ Job จะมี steps ที่คล้ายๆกันเช่น
- checkout
- install dependencies
- caching
- 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
.github├── actions│ └── <folder your action name>│ └── action.yaml└── workflows ├── deploy.yaml └── lint.yaml
Hands-on
มาลงมือทำกันเลย
ยังอยู่ที่โปรเจคเดิมนะ
starter workflow
จะเริ่มต้นด้วยการสร้าง workflow 2 อัน
- Lint
- Deploy
เราแค่ 2 ก่อนละกัน
.github/workflows/lint.yaml
name: ESLinton: 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
name: Deploy Projecton: 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
- install bun
- cache dependencies
- install dependencies
มาดู code กันก่อนเลย
.github/actions/install-and-caching/action.yaml
name: Install and Cache Dependenciesdescription: Install and cache dependenciesruns: 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 actionsteps:
ตรงนี้เป็นการเขียน steps เหมือนเดิมเลยrun:
ตรงที่ใช้run
คือเราจะใส่ shell command เองใช่มะ ตรงนี้เราจำเป็นต้องใส่ว่าจะใช้ shell ยี่ห้ออะไร ส่วนใหญ่ก็จะใช้ bash แหละ
How to use composite actions
ทีนี้มาดูกันว่าพอมี Composite actions แล้ว เราจะเอามาใช้ยังไงดี
ไม่ยากเลย เราแค่ใช้ uses:
แล้วใส่ path ไปที่ action ของเรา
ตอนนี้เรามี files แบบนี้
.github├── actions│ └── install-and-caching│ └── action.yaml└── workflows ├── deploy.yaml └── lint.yaml
เราแก้ workflow แบบนี้
.github/workflows/lint.yaml
name: ESLinton: 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 Projecton: 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 Dependenciesdescription: Install and cache dependenciesinputs: 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: ESLinton: 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

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

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
name: Install and Cache Dependenciesdescription: Install and cache dependenciesinputs: caching: description: Enable caching (default = true) required: false default: "true"outputs: cache-hit: # define output key description: Cache hit value: unknown # this will change laterruns: 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 เราจะใช้
echo <key>=<value> >> $GITHUB_OUTPUT
จากตัวอย่างของเราก็จะเป็น
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 Dependencies6 collapsed lines
description: Install and cache dependenciesinputs: 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: ESLint11 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 Project12 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
