มาทำ Versioning + GitOps + K8s บน Monorepo กันเถอะ! Part II

semantic version

For English version, please visit the link https://github.com/saenyakorn/monorepo-versioning-gitops/

ความเดิมตอนที่แล้ว

เราได้คุยกันเรื่อง

  1. Versioning สำคัญไฉน
  2. GitOps คืออะไร แล้วมีประโยชน์ยังไง?
  3. เปรียบเทียบ Monorepo vs others
  4. ความยากของการ Versioning และ Deploy บน Monorepo

ถ้าใครยังไม่ได้อ่าน ย้อนกลับไปอ่านก่อนได้นะ 🥺

📜 เนื้อหาวันนี้

โดยรวมเราจะพาเพื่อน ๆ มาทำความรู้จักกับการทำ versioning กันก่อน โดยเฉพาะบน Monorepo เนื้อหาจะมีดังนี้เลยย

  1. มาทำความรู้จักกับ semantic version กันเถอะ!
  2. สร้าง Monorepo แบบ minimal
  3. มาลอง setup Changesets บน Monorepo กัน
  4. มาลอง bump version แบบง่าย ๆ บน local
  5. สรุปบทความอันยาวเหยียดในไม่กี่บรรทัด
  6. บทส่งท้าย
  7. ตัวอย่างตอนต่อไป! (Development process ทั้งระบบ)

⭐️ มาทำความรู้จักกับ semantic version กันเถอะ!

ก่อนอื่นขอเกริ่นก่อนเลยว่า Pattern การตั้งชื่อ version มีหลายวิธีมาก แต่วิธีที่ชาวเรานิยมกันคือ Semantic Version ประกอบไปด้วยเลข 4 ตัว คือ

Major . Minor . Patch - Pre-release

  1. Major— เลขตำแหน่งนี้จะถูก bump เมื่อ function ไม่ compatible กับของเก่าเลย หรือเป็น breaking changes
  2. Minor— เลขนี้จะถูก bump เมื่อมี function ใหม่ ๆ เข้ามาที่ยัง compatible กับของเดิมอยู่ หรือเป็น feature
  3. Patch— เลขนี้จะถูก bump เมื่อ function เดิมยังทำงานได้เหมือนเดิมแต่มีการอุดช่องโหว่ หรือเป็น bug fix
  4. Pre-release (Optional) — เลขนี้จะใช้ก็ต่อเมื่อมีการทำ pre-release เท่านั้น ซึ่งไม่จำเป็นต้องใช้คำว่า “beta” ก็ได้ สามารถเป็นคำอื่นได้หมดเลย เช่น “next” หรือ “alpha” เป็นต้น

ด้วย pattern การตั้งเลข version แบบนี้ จะพอทำให้เราสามารถเดาได้ว่าการ release ของ library ต่าง ๆ ตาม GitHub หมายความว่าอะไร เช่น

มี library ตัวหนึ่งที่เดิมมี version ล่าสุดเป็น 1.5.2 ส่วน version ใหม่เป็น 1.6.0 ซึ่งหมายความว่า “library นี้มีการเพิ่ม feature ใหม่เข้าไป” นั่นเอง

ส่วน “เพิ่ม feature ใหม่” ที่ว่าคืออะไร ก็ต้องไปหาอ่านตาม CHANGELOG หรือไม่ก็ Release Notes ต่อไป

ตัวอย่าง GitHub release note

📐 สร้าง Monorepo แบบ minimal

ก่อนอื่น เรามาเริ่มสร้าง monorepo เป็นของตัวเองก่อนดีกว่า โดยในที่นี้เราจะใช้ Turborepo เป็น monorepo management tool กัน ซึ่งสามารถ initialize ได้ตามนี้เลย

npx create-turbo@latest

จากนั้นจะมี prompt มาให้เราใส่ชื่อ workspace สำหรับตรงนี้ใครอยากจะตั้งชื่ออะไรก็ตามสะดวกเลยย

ต่อมามันจะให้เราเลือกว่าจะใช้ package manager อะไร ในที่นี้ผมขอเลือกเป็น pnpm ก็แล้วกัน (สำหรับใครที่จะใช้ npm หรือ yarn ก็ได้เหมือนกันนะ)

สำหรับผมขอเลือก package manager เป็น pnpm ก็แล้วกัน

สร้างเสร็จแล้วก็เปิด workspace มาดูด้วย VSCode ได้เลยย structure จะออกมาหน้าตาประมาณนี้เลย

└── demo-workspace/
├── apps/
│ ├── docs/
│ │ └── package.json
│ └── web/
│ └── package.json
├── packages/
│ └── ui/
│ └── package.json
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── turbo.json

Folder: apps

คือ folder ที่เก็บรวมรวม application ทั้งหมด เช่น React app, Next.js app, Express.js app อย่างในที่นี้คือจะมี 2 apps ด้วยกันคือ docs กับ web ซึ่งเป็น Next.js ทั้งคู่ โดย apps จะต้องไม่มีการ import ข้ามกันไปมาเด็ดขาด!

Folder: packages

คือ folder ที่เก็บรวบรวม package / library ทั้งหมด ซึ่งคือแบบเดียวกันที่เราชอบ npm install หรือ yarn add นั่นแหละ แต่สิ่งนี้เราสามารถ import ใช้ใน workspace เราได้เลยโดยไม่ต้อง publish packages ขึ้น NPM registry ก่อน

File: package.json

ทุกคนน่าจะรู้จัก file นี้เป็นอย่างดีอยู่แล้ว มันคือที่เก็บรวบรวม metadata ของ app/package นั้น ๆ เช่น dependencies, scripts, name, version, main (entrypoint) เป็นต้น

แต่สำหรับ root package.json นี้ จะเก็บเพียงแค่ global dependencies เช่น prettier กับ workspaces เป็นหลัก เช่นในที่นี้คือ

{
"name": "demo-workspace",
"version": "0.0.0",
"private": true,
// ------------------------FOCUS ON THIS ------------------------------
"workspaces": [
"apps/*",
"packages/*"
],
// --------------------------------------------------------------------
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"format": "prettier --write \"**/*.{ts,tsx,md}\""
},
"devDependencies": {
"eslint-config-custom": "workspace:*",
"prettier": "latest",
"turbo": "latest"
},
"engines": {
"node": ">=14.0.0"
},
"dependencies": {},
"packageManager": "pnpm@7.24.3"
}

สังเกตว่าจะมี field workspaces ติดมาด้วย นั่นคือเป็นการระบุว่าเราใช้งาน pnpm แบบ workspace mode นั่นเอง

สำหรับ package.json ของ app กับ package ก็ยังคงคล้าย ๆ กับที่เราเคยใช้ ๆ กันมา ซึ่งขอข้ามการอธิบายละกัน

File: pnpm-workspace.yaml

ทำหน้าที่เดียวกับ workspaces ใน root package.json แต่นั่นเป็น feature ของ npm และ yarn workspace แต่สำหรับ pnpm ต้องมี pnpm-workspace.yaml เพิ่มมาด้วย

สำหรับใครที่สงสัยว่าทำไมต้องมี pnpm-workspace.yaml สามารถตามอ่านได้ที่นี่เลย https://github.com/orgs/pnpm/discussions/3205

Dependencies

ถ้าลองไปดูใน package.json ของ web และ docs จะพบว่ามี UI เป็น dependencies อยู่ แบบนี้

"dependencies": {
// ...
"ui": "workspace:*", // For pnpm
"ui": "*" // For npm and yarn
},

ในที่นี้ หมายความว่า web กับ docs depends on (ขึ้นอยู่กับ) ui ซึ่งเป็น internal package ใน workspace เดียวกัน ดังนั้น เวลามีอะไรใน ui เปลี่ยน ก็จะแปลว่า web กับ docs ต้องเปลี่ยนตามไปด้วย

✏️ มาลอง setup Changesets บน Monorepo กัน

ก่อนอื่นเรามาทำความรู้จักกับ Changesets กันก่อน Changesets คือ versioning tool ที่จะช่วยให้เราสามารถ manage versioning package หลาย ๆ อันพร้อมกันได้บน Monorepo โดย concept หลักของ Changesets คือ สร้าง Changesets file ขึ้นมาเพื่ออธิบายว่า มี changes อะไรเกิดขึ้นบ้าง และใช้สิ่งนี้เป็นข้อมูลในการ bump version

วิธี setup ก็ง่ายแสนง่าย เพียงแค่รันคำสั่ง

pnpm add -D -w @changesets/cli @changesets/changelog-github
pnpm changeset init

จะพบว่ามี files ใหม่เกิดขึ้นมาที่ .changeset folder

└── workspace-name/
├── .changeset/
│ ├── config.json
│ └── README.md

├── apps/
├── packages/
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── turbo.json

File config.json

คือไฟล์ที่เอาไว้เก็บ configuration ของการทำ versioning

ที่เราต้องแก้จะมีเพียงแค่ field “changelog” ซึ่งเป็น filed ที่เอาไว้ระบุว่าเรา generate CHANGELOG อย่างไร ซึ่งถ้าเราอยากให้มันทำ GitHub release note ให้ด้วยจะต้องไปแก้เป็น @changesets/changelog-github แทน แบบนี้

{
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
- "changelog": "@changesets/cli/changelog",
+ "changelog": ["@changesets/changelog-github", { "repo": "ORGANIZATION_NAME/REPO_NAME" }],
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}

เสร็จแล้ว! (อย่าลืมแก้ “ORGANIZATION_NAME/REPO_NAME” ด้วยนะ)

📈 มาลอง bump version แบบง่าย ๆ บน local

สมมติว่าเราสร้าง component Button มาแล้วใน packages/ui แล้วเราอยากจะ bump version ของ package นี้ (ซึ่งจะกระทบ web และ docs ด้วย)

เริ่มแรกให้เรา add Changesets file ก่อน ด้วยคำสั่ง

pnpm changeset

จากนั้นจะมี prompt มาให้เรากรอกว่าอยากจะ bump package ไหน​ แล้ว bump เป็น major/minor/patch ซึ่งในที่นี้เรามี component ใหม่ เราเลยอยาก bump เป็น minor

เลือกว่าจะ bump package อะไรบ้าง ใช้ space bar ในการ select (multiple select)
ตอนแรกมันจะถามว่าจะเป็น major ไหม ให้เรากด enter ข้ามไปเลย แล้วกด space bar ตอนเป็น minor เพื่อเลือกแล้วกด enter
เขียนสรุปอธิบายไปว่าเกิดอะไรขึ้นบ้างใน changes นี้

เมื่อทำเสร็จปุ๊ป จะเห็นว่าจะมีไฟล์ชื่อแปลก ๆ งอกขึ้นมาใน folder .changeset ที่เนื้อหาข้างในหน้าตาประมาณนี้

changeset file ของ ui

ซึ่งหมายความว่าเราจะ bump package/ui แบบ minor แล้วมี summary เป็น “Create a new Button component”

แน่นอนว่าเราสามารถแก้ไขได้ตลอด (และสามารถให้คนอื่น review ได้ด้วยตอนเปิด PR ซึ่งสำคัญมาก!)

แล้วก็เราจะขอ bump version ของ docs เป็น minor ด้วยอีกอันหนึ่ง (ใช้วิธีเดิมนี่แหละ) ด้วยข้อความว่า “Initialize a new main page”

changeset file ของ docs

ถึงจุดนี้แล้วเราจะมี Changesets 2 files ละ ต่อไปจะเป็นขั้นตอน bump version กัน โดยใช้คำสั่ง

pnpm changeset version

ก็จะพบว่า Changesets file ทั้ง 2 อันหายไปแล้ว พร้อมกับมีการแก้เลข version ใน package.json ของ ui, docs และ web แถมมี CHANGELOG เพิ่มขึ้นมาด้วย ดังนี้

CHANGELOG ของ package/ui
CHANGELOG ของ apps/web
CHANGELOG ของ apps/docs

คำอธิบาย: สำหรับการ bump version ที่เกิดขึ้นนี้ package/ui ได้ bump แบบ minor changes ไปซึ่ง web และ docs มี ui เป็น dependencies ทำให้ web กับ docs มี patch changes เพิ่มขึ้นมาสำหรับการ upgrade dependency versions

นอกจากนี้ docs เองก็ยังมี minor changes เป็นของตัวเอง ทำให้ CHANGELOG ของ docs มีทั้ง minor changes และ patch changes ในการ bump version ครั้งเดียว

เสร็จแล้ว! เย้ 🎉

✨ สรุปบทความอันยาวเหยียดในไม่กี่บรรทัด

อธิบายซะยืดยาว แต่ถ้าให้สรุปสั้น ๆ ก็คือ

  1. สร้าง Monorepo ขึ้นมาก่อน (หรือถ้ามีอยู่แล้วก็ข้ามไปได้เลย)
  2. ตรวจสอบว่ามี package.json อยู่ในทุก ๆ apps และ packages และมีการ define npm/yarn/pnpm workspace ไว้แล้ว
  3. ติดตั้ง Changesets ด้วย — pnpm changeset init
  4. เวลาจะ describe changes ให้เพิ่ม Changesets file ด้วยคำสั่ง — pnpm changeset
  5. เวลาจะ bump version ให้สั่ง — pnpm changeset version

แต่จะสังเกตว่าการทำ versioning นี้ ยังเป็นการทำด้วยมืออยู่เลย แม้จะแก้ปัญหาการ bump version กับการ resolve dependencies หลาย ๆ apps/packages พร้อมกันไปแล้ว แต่ยังไม่ได้แก้ปัญหาที่เกิดจาก human error เช่น

  • ลืม bump version บ้าง
  • ลืม describe changes บ้าง

🙏 บทส่งท้าย

วันนี้เราได้คุยกันไปในเรื่อง

  1. มาทำความรู้จักกับ semantic version กันเถอะ! — มี 3 เลข major / minor / patch แล้วก็มีแถมเลขหนึ่งคือ pre-release
  2. สร้าง Monorepo แบบ minimal— ในที่นี้ใช้ Turborepo สร้างได้โดยการรันคำสั่ง npx create-turbo@latest
  3. มาลอง setup Changesets บน Monorepo กัน — setup ง่ายนิดเดียว แค่ 2 คำสั่งกับแก้ json file 1 บรรทัด

หวังว่า blog นี้จะเป็นประโยชน์ไม่มากก็น้อย สำหรับคนที่ต้องการทำ full pipeline สำหรับการ versioning, deployment และ GitOps บน monorepo ถ้าคิดว่าบทความนี้เป็นประโยชน์อย่าลืมแชร์ให้เพื่อน ๆ อ่านกันด้วยล่ะ! ❤️

ขอบคุณทุกท่านที่ตามอ่านมาจนถึงตรงนี้นะครับ อย่าลืมติดตามตอนต่อไปกันด้วยล่ะ!

▶️ ตัวอย่างตอนต่อไป

ตอนต่อไปเราจะพาเพื่อน ๆ ไปรู้จักกับ automated full pipeline ว่าตั้งแต่ต้นจนจบ pipeline ต้องทำอะไรบ้าง และในฐานะที่เราเป็น developer เราจะต้องทำอะไรบ้าง

สปอยเล็กน้อย คือถ้า setup เสร็จเรียบร้อยสมบูรณ์แล้ว developer แทบจะไม่ต้องทำอะไรเลย แค่กด merge อย่างเดียว ฮึ ๆ 😏

ติดตามตอนต่อไปได้ที่นี่เลยย

Appendix

ทั้งนี้ automated full pipelines demo ของการ versioning, deployment และ GitOps บน monorepo อยู่ที่นี่แล้ว https://github.com/saenyakorn/monorepo-versioning-gitops

ปล. ถ้าชอบก็อย่าลืมกด Star เป็นกำลังใจให้ด้วยนะครับ 🫠

--

--