[Platform IO] ลอง Build firmware ผ่าน Gitlab-CI พร้อม Release assets
สวัสดีครับ หลังจากที่หายไปนานหลายเดือนเลย วันนี้ก็เลยกะว่าจะมาแชร์วิธีการ Build firmware ผ่าน Gitlab-CI พร้อมกับขั้นตอนการทำ Release version ของ firmware สุดท้ายเราก็ได้จะไฟล์ Firmware.bin ที่พร้อมจะทำการอัพโหลดเข้า ESP32 เราได้เลย
ซึ่งการระบุเวอร์ชันในโค้ดมีความสำคัญเพื่อช่วยในการติดตามการเปลี่ยนแปลง จัดการความเข้ากันได้ ตรวจสอบและแก้ไขข้อผิดพลาด และสนับสนุนการบำรุงรักษาโค้ดในระยะยาว ทำให้การพัฒนาและบำรุงรักษาโค้ดเป็นไปอย่างมีประสิทธิภาพและราบรื่น
โดยขั้นตอนทั้งหมดนี้ทางผมจะใช้ Semantic-release ที่เป็น Package ใน NodeJS เป็นตัวช่วยจัดการอีกทีนะครับ สำหรับท่านผู้อ่านท่านไหนมีวิธี Release ที่สะดวกกว่านี้ รบกวนแนะนำกันด้วยนะครับ 🙏🙏🙏
เริ่มกันเลยดีกว่า บทความนี้สิ่งที่เราต้องเตรียมก็คือ
- Conventional Commits: เป็นรูปแบบการเขียนข้อความสำหรับการ commit ในระบบ version control เช่น Git โดยมีโครงสร้างที่ชัดเจน ประกอบด้วยประเภทการเปลี่ยนแปลง (เช่น
feat
สำหรับการเพิ่มฟีเจอร์,fix
สำหรับการแก้ไขบั๊ก), ขอบเขตที่ได้รับผลกระทบ และคำอธิบายสั้น ๆ เกี่ยวกับการเปลี่ยนแปลง รูปแบบนี้ช่วยให้การตรวจสอบประวัติการเปลี่ยนแปลงและการจัดการโปรเจกต์ทำได้ง่ายขึ้น - Semantic-release: เป็นเครื่องมืออัตโนมัติสำหรับการจัดการการปล่อยเวอร์ชันในโปรเจกต์ Node.js โดยใช้รูปแบบ Conventional Commits ในการกำหนดเวอร์ชันใหม่อย่างเป็นระบบ หลังจากทำการ merge pull request หรือ push commits เข้าไปใน repository เครื่องมือจะตรวจสอบข้อความ commit เพื่อตัดสินใจว่าจะเพิ่มเลขเวอร์ชันหลัก (
major
), รอง (minor
), หรือแพตช์ (patch
) นอกจากนี้semantic-release
ยังสามารถสร้างและอัปเดต Changelog, ปล่อยเวอร์ชันใหม่ไปยัง npm registry และทำการแจ้งเตือนเกี่ยวกับการปล่อยเวอร์ชันไปยังแพลตฟอร์มต่าง ๆ เช่น GitHub, GitLab หรือ Slack โดยอัตโนมัติ [Ref จาก ChatGPT นะครับขี้เกียจเขียนสุดๆ 5555]
ติดตั้ง Semantic-release อื่นๆใน package.json
// Install Semantic-release.
npm i -s -D @semantic-release/changelog @semantic-release/commit-analyzer \
@semantic-release/git @semantic-release/gitlab @semantic-release/gitlab-config \
@semantic-release/npm @semantic-release/release-notes-generator @semantic-release/exec
ติดตั้ง commitizen และทำการ Init package เข้ามาใช้ใน package.json ของเรา
// Install commitizen.
npm install -D commitizen
// Setup adapter for changelog.
commitizen init cz-conventional-changelog --save-dev --save-exact
ติดตั้ง Conventionalcommits ด้วยเพื่อเอาไว้เช็ค commit message ของเราในขั้นตอน Release
npm install -D conventional-changelog-conventionalcommits
ในส่วนของ Package.json อย่าลืมปรับให้เป็น private project ด้วยนะครับ
## package.json ##
{
"name": "aws-iot-timer-relay-4ch",
"version": "2.2.4",
"private": true
}
ส่วนต่อมาสร้างไฟล์ .releaserc.ymal เพื่อให้ตัว Semantic-release มาเรียกใช้ในขั้นตอน CI/CD
## .releaserc.ymal ##
branches: ["main"]
ci: true
debug: true
dryRun: false
tagFormat: "v${version}"
preset: "conventionalcommits"
gitlabUrl: "https://gitlab.xxx.com" # <--- your gitlab url.
verifyConditions:
- "@semantic-release/changelog"
- "@semantic-release/git"
- "@semantic-release/gitlab"
analyzeCommits:
- path: "@semantic-release/commit-analyzer"
releaseRules:
- type: "breaking"
release: "major"
- type: "feat"
release: "minor"
- type: "*"
release: "patch"
generateNotes:
- path: "@semantic-release/release-notes-generator"
writerOpts:
groupBy: "type"
commitGroupsSort: "title"
commitsSort: "header"
linkCompare: true
linkReferences: true
presetConfig:
types:
- type: chore
section: Others
- type: revert
section: Reverts
- type: feat
section: Features
- type: fix
section: Bug Fixes
- type: improvement
section: Feature Improvements
- type: docs
section: Docs
- type: style
section: Styling
- type: refactor
section: Code Refactoring
- type: perf
section: Performance Improvements
- type: test
section: Tests
- type: build
section: Build System
- type: ci
section: CI
prepare:
- path: "@semantic-release/changelog"
- path: "@semantic-release/npm"
- path: "@semantic-release/exec"
cmd: "./update-version.sh ${nextRelease.version} && echo v${nextRelease.version} > .release-version" # <-- Remember this script.
- path: "@semantic-release/git"
message: "[skip ci] chore(release): v${nextRelease.version}"
assets: ["CHANGELOG.md", "package.json", "include/CONFIG.h"]
publish:
- path: "@semantic-release/npm"
- path: "@semantic-release/gitlab"
success:
false
fail:
false
จาก .releaserc.yml ข้างต้นจะมี 2 ส่วนที่จะต้อง Config ด้วยกัน ส่วนแรกเราจะต้องกำหนด Gitlab Url ของเรา และส่วนที่สองจะเป็น Custom script ที่ใช้ในการอัพเดทไฟล์ CONFIG.h
./update-version.sh ${nextRelease.version} && echo v${nextRelease.version} > .release-version
โดยที่ภายใน script จะเป็นการ Stamp version ของ Semantic-release เข้าไปใน CONFIG.h ซึ่งจะเป็นไฟล์ที่ผมเอาไว้เก็บ Config ของอุปกรณ์ทุกอย่างไว้ในนี้เลย
## update-version.sh ##
#!/bin/bash
# Receive nextRelease.version from the first command line argument
nextRelease_version=$1
# Escape periods in the version number, since sed treats them as special characters
escapedVersion=$(echo "${nextRelease_version}" | sed 's/\./\\./g')
# Use sed to replace the version number in CONFIG.h
sed -i -e "s/#define FIRMWARE_VERSION \".*\"/#define FIRMWARE_VERSION \"${escapedVersion}\"/g" include/CONFIG.h
echo "updated version to ${nextRelease_version} in CONFIG.h"
## CONFIG.h ##
#ifndef CONFIG_H
#define CONFIG_H
#define FIRMWARE_VERSION "2.2.4" <-- Stamp a latest version.
#define TWDT_TIMEOUT 180
#define MQTT_KEEP_ALIVE_TIMEOUT 15000
// WiFi
#define WIFI_SSID "XXXX"
#define WIFI_PASSWORD "PASSWORD"
#endif
ต่อมาก็คอนฟิค .gitlab-ci.yaml ด้วย
variables:
GIT_SUBMODULE_STRATEGY: recursive
RELEASE_REGISTRY_URL: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases"
PACKAGE_REGISTRY_URL: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/firmware"
stages:
- release
- build
- update-asset
semantic-release:
stage: release
image:
name: node:20-bullseye-slim
only:
refs:
- main
except:
refs:
- tags
variables:
- $CI_COMMIT_TITLE =~ /^RELEASE:.+$/
before_script:
- apt-get update && apt-get install -y git
- npm ci
script:
- HUSKY=0 npm run release
artifacts:
paths:
- .release-version
expire_in: 1 hour
build:
stage: build
image: python:3.11
only:
refs:
- main
before_script:
- pip install -U platformio
script:
- git pull origin main
- pio run
artifacts:
paths:
- .pio/
expire_in: 1 hour
update-asset:
stage: update-asset
image: curlimages/curl:latest
only:
refs:
- main
before_script:
- FW_VERSION=$(cat .release-version)
script:
- |
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file .pio/build/nodemcu-32s/firmware.bin "${PACKAGE_REGISTRY_URL}/${FW_VERSION}/firmware.bin"
- |
curl --request POST --header "JOB-TOKEN: ${CI_JOB_TOKEN}" "${RELEASE_REGISTRY_URL}/${FW_VERSION}/assets/links" \
--data name="firmware.bin" \
--data url="${PACKAGE_REGISTRY_URL}/${FW_VERSION}/firmware.bin"
- |
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file .pio/build/nodemcu-32s/bootloader.bin "${PACKAGE_REGISTRY_URL}/${FW_VERSION}/bootloader.bin"
- |
curl --request POST --header "JOB-TOKEN: ${CI_JOB_TOKEN}" "${RELEASE_REGISTRY_URL}/${FW_VERSION}/assets/links" \
--data name="bootloader.bin" \
--data url="${PACKAGE_REGISTRY_URL}/${FW_VERSION}/bootloader.bin"
- |
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file .pio/build/nodemcu-32s/partitions.bin "${PACKAGE_REGISTRY_URL}/${FW_VERSION}/partitions.bin"
- |
curl --request POST --header "JOB-TOKEN: ${CI_JOB_TOKEN}" "${RELEASE_REGISTRY_URL}/${FW_VERSION}/assets/links" \
--data name="partitions.bin" \
--data url="${PACKAGE_REGISTRY_URL}/${FW_VERSION}/partitions.bin"
ในขั้นตอนของ CI/CD เราจะมีการกำหนดทั้งหมด 4 Stage ด้วยกัน
- Release stage: จะทำการรัน Semantic-release package ที่เราติดตั้งผ่าน Nodejs แล้วหลังจากที่ Release เสร็จแล้วเราจะได้รับไฟล์มาทั้งหมด 3 ไฟล์
- CHANGELOG.md: เป็นไฟล์ที่ระบุ Commit message ในแต่ละ Version
- package.json: ทำการระบุ Version ตัวใหม่เข้าไป
- include/CONFIG.h: เป็นไฟล์ CONFIG.h ที่เราจะเอาไว้ระบุ version ใน firmware
- เมื่อขั้นตอนการ Release เสร็จสิ้นเราจะทำการ สร้างไฟล์ .release-version ไว้เพื่อเรียกใช้เป็น artifact ใน stage อื่นๆ
- Build stage: จะทำการ Build code ของเราให้เป็น Binary file โดยจะใช้จะทำการรันผ่าน Python ซึ่งจะใช้ Package “Platformio” เป็นตัว build อีกที หลังจากที่ Build เสร็จแล้วเราก็จะได้ไฟล์ Binary กับพวก dependencies ต่างๆ ซึ่งจะถูกเก็บไว้ในโฟลเดอร์ “.pio/” เพื่อเอาไว้ใช้ใน Stage ต่อไป
- Update-asset stage: จะเป็นการอัพโหลดไฟล์ที่เราจำเป็นต้องใช้ในการ Upload firmware เข้าไปใน ESP32 ซึ่งใน Stage นี้เราจะใช้ curl ในการอัพโหลดได้เลยโดยที่ไฟล์ที่ผมจะอัพโหลดจะประกอบไปด้วย
- firmware.bin ไฟล์ Firmware ตัวจริง
- bootloader.bin ไฟล์
bootloader.bin
ใน PlatformIO เป็นโปรแกรมขนาดเล็กที่ทำงานเมื่อเปิดเครื่องหรือรีเซ็ตอุปกรณ์ เพื่อโหลดและเริ่มการทำงานของเฟิร์มแวร์หลักในหน่วยความจำ - partitions.bin ไฟล์
partitions.bin
กำหนดการแบ่งพาร์ติชันในหน่วยความจำของอุปกรณ์เพื่อจัดสรรพื้นที่สำหรับ bootloader, เฟิร์มแวร์, การตั้งค่า, และข้อมูลอื่นๆ
ในขั้นตอน Update-asset เราจะทำการ สร้างตัวแปร FW_VERSION ซึ่งจะเข้าไปอ่านไฟล์จาก .release-version มาอีกที เพื่อตอนที่เราทำการอัพโหลดไฟล์ Binary ต่างๆขึ้นไปเก็บใน Gitlab package registry มันจะแยกโฟลเดอร์ให้ตาม version ที่ Release ออกมาเลย
สร้าง CI/CD variables บน Gitlab ก่อน
ในขั้นตอนนี้เราจะต้องการเพิ่ม Key ใน CI/CD ด้วย โดยที่เราจะต้องสร้าง Key “GITLAB_TOKEN” พร้อมกับทั้งต้อง Generate Access Token เข้ามาด้วย อย่าลืม!!!!
เสร็จแล้วถึงเวลาที่เราจะมารัน CI/CD ของจริงกันสักที
Push code เราขึ้นไปบน Gitlab โลดดดดดด….. อย่าลืมต้องเป็น Branch “main” เท่านั้นนะอันนี้
เมื่อตัว Semantic-release เสร็จ สังเกตดูว่า Semantic-release-bot มันจะทำการ Push code ที่เรา Config ใน .releaserc.yaml เข้าไปด้วย ยกตัวอย่างรูปข้างบนจะทำการ Push Commit “[skip ci] chore(release): v2.2.4” พร้อมทั้ง Push tag ขึ้นมาด้วย
ทำไมของผมถึงมี 5 Stage (วงกลมเขียว/ส้ม 5 อัน) ต้องบอกว่าอันนี้ผมไม่ได้รันจริงๆครับแค่เอารูปจาก Project ที่รันใน Office มาโชว์เฉยๆ แต่ของเพื่อนๆจะขึ้นแค่ 3 Stage นะครับ 55555++
หลังจากที่ทุกขั้นตอนเสร็จแล้วเราก็ไปเช็ค Release ได้ที่ Project => Deploy => Releases ก็จะได้ข้อมูลตามรูปด้านล่างเลยครับ
หน้าตาไฟล์ CHANGELOG.md ซึ่งจะเป็น markdown format
ส่วนใน package.json จะมีการเปลี่ยน version ตาม Semantic-release
และไฟล์ include/CONFIG.h จะทำการ stamp version จาก 2.2.3 -> 2.2.4 ตามรูปด้านล่างเลยครับ
ก็เป็นอันเสร็จสิ้นสำหรับ Process CI/CD เท่านี้นะครับ เราก็จะได้ Flow ของการ Stamp version พร้อมกับ Release และ Package ที่จำเป็นสำหรับการ Upgrade firmware ของ ESP32 แล้วว….