Thanapon Tapala

Backend Developer

Embedded Developer

Smart Farmer

Maker

Thanapon Tapala

Backend Developer

Embedded Developer

Smart Farmer

Maker

Blog Post

[Platform IO] ลอง Build firmware ผ่าน Gitlab-CI พร้อม Release assets

[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 แล้วว….

สำหรับวันนี้ขอบคุณและสวัสดีครับ 🙏🙏🙏

Taggs: