跳至主要内容
💬 回報這一頁

版本策略

三層各自獨立版號,因為它們的變動頻率與影響半徑完全不同。

套件版號意義
@dooping/tokens契約。變動影響所有宿主
@dooping/react參考實作。使用者複製走之後就與版號脫鉤
文件站跟著 repo,不獨立發版

進版是什麼事件

dev ──開發、CI 確認──▶ main ──▶ 文件站/Storybook/registry 自動更新

└─ token 內容有變才 bump npm 版號

dev 併進 main 就是一次進版。 其他系統一律以 main 為參照,不看 dev

一次進版對應 CHANGELOG 的一則。 寫不出「取用者要做什麼」的變更,就不是一次進版,繼續留在 dev

兩條散佈通道的更新時機不同,這個區分很重要:

通道何時更新
文件站 / Storybook / registry每次進版都更新(它們反映 main 的當下狀態)
npm @dooping/tokens只在 token 內容真的變更時 bump

預覽站不是進版

dev 推上去後會部署到 /preview/,給維護者在真實裝置上確認 UI—— 尤其是響應式與觸控目標,那是開發機看不出來的。

預覽站不可被參照

/preview/ 的內容隨時會被下一次 dev push 覆蓋,而且沒有經過進版。 其他系統一律以 main 的正式網址為準,不得參照任何 /preview/ 底下的東西。

預覽站的 registry 檔案雖然存在,內容裡的相依網址仍指向正式站—— 就算誤複製了安裝指令,裝到的也是正式版本。

兩個對外訊號

訊號給誰看什麼時候動
規範版號 vX.Y.Z(GitHub tag + registry 戳記)所有取用端元件、token 或模式有取用端可感知的變更
@dooping/tokens 的 npm 版號有安裝 token 套件的系統token 內容變更

規範版號回答的問題是:「這次進版,我需不需要跟進?」 看大中小就知道分量——大=會壞、中=有新東西、小=修正微調(判準見上方兩張 SemVer 表, 對象是規範整體)。純文件進版不 bump、也就沒有新 tag:取用端看不到新版號, 就代表不需要任何動作。

版號正本是根目錄 package.jsonversionpackages/reactversion.ts 跟隨它、 registry 戳記由它產生(守衛測試強制四處一致),所以 tag、戳記與 /r/index.jsonversion 永遠是同一個號碼——「我抄的是哪一版」直接對得回 GitHub 的 tag。

三層版號的對應關係(配對模型)

兩條版號線不同步前進——那怎麼知道「哪一版規範配哪一版 token」? 答案是配對模型:對應關係不靠同號,靠宣告。

定義:每個規範版恰好宣告一個 tokens 版。樞紐是 packages/react/package.json"@dooping/tokens": "x.y.z" 那一行—— 它是配對的唯一正本,其他所有體現都從它產生:

體現處形式給誰用
/r/index.jsontokensVersion機器可讀配對(與 version 並列)取用端一個端點問到配對
registry item 的 dependencies@dooping/tokens@^x.y.znpx shadcn add 自動裝對
v* tag 的訊息「tokens 配對版:x.y.z」從 git 稽核,不翻 CHANGELOG
CHANGELOG 標題「規範 vA/tokens vB」要升級的人

多對一合法、一對多非法。token 沒變時,多個規範版指向同一 tokens 版 (v0.6.0 與 v0.6.1 都配 0.5.0);反過來,一個規範版宣告兩個 tokens 版不可能發生—— 守衛強制宣告是單一值。

規範 v0.1.0 v0.2.0 v0.2.1 v0.4.0 v0.6.0 v0.6.1
│ │ │ │ │ │
tokens 0.1.2 ──┴───────┘ 0.3.0 0.5.0 ──┘

^ 的語意就是配對的鬆緊:0.x 之下 ^0.6.0>=0.6.0 <0.7.0—— 同 minor 的 token 修補自動吃(不必重抄元件);宣告跨了 minor, 代表元件可能開始依賴新 token,該回 CHANGELOG 看要不要跟進。

取用端自查(一行對照就知道自己在不在配對上):

npm ls @dooping/tokens; curl -s https://kielchang.github.io/dooping-design-book/r/index.json | jq -r .tokensVersion

兩個數字的關係只有三種:相等=配對正確;本地落後=該升級 token; 線上 tokensVersion 比 npm 能裝到的還新=上游合併了但還沒發佈(等,或提醒維護者)。

配對由四道保證釘住,每一道擋一種真實出過的錯:

保證擋什麼
測試守衛(處內一致+index 配對)七處版號任何一處漂移、tokensVersion 與宣告不符
CI 兩硬閘(內容變了版號要跟)改了 token/元件卻忘記 bump——擋 PR
發佈硬閘(tag 名=package.json 版本、commit 在 main 上)推錯 tag 發出錯的版本;對未進版的 commit 發佈
兩處軟閘(PR 與進版當下的漂移提示)宣告了卻忘記發佈——不擋,但一直出聲到發為止

進版流程:工程師提議,維護者在 GitHub 上確認

① dev 上:依判準表 bump 規範版號 + 寫 CHANGELOG(這是「提議」)
② 開 dev → main 的 Pull Request
└ CI 在 PR 上把關:守衛全綠?內容變了版號有沒有跟上?
③ 在 GitHub 看 PR(版號、CHANGELOG、檢查結果)→ 合併 = 確認
④ 部署成功後自動蓋上 tag vX.Y.Z(版號沒動就不打,重跑不重複打)
⑤ 自動發 GitHub Release:notes=CHANGELOG 該則,標題帶 tokens 配對版

tag 是自動蓋章,不是閘門——閘門是第 ③ 步的合併。日期不佔 tag 名稱, 在 tag 描述與 CHANGELOG 裡。

第 ⑤ 步是給取用端的推播訊號:Watch 這個 repo 的 Releases(或訂 releases.atom)就會在每次進版時被主動告知,notes 直接是 CHANGELOG 該則全文—— 通知本身回答三問。純文件進版不發 Release,「沒有訊號=不需要動作」。 訂閱方式、收到訊號後的判斷與每層更新程序,見跟上新版

一次合併只會蓋一個 tag

第 ④ 步讀的是合併當下根目錄 package.jsonversion。所以如果 dev 上累積了 好幾次 bump 才併,只有最後那一個版號會有 tag。

這不是缺陷——依上面第一條,dev 併進 main 才算一次進版,中間那些版號從來沒有 任何取用端拿得到,它們是工作狀態。CHANGELOG 要照這個事實寫:一次合併一則, 底下用 ### 分工作項,不要把中間版號寫成好幾則發佈。

實際發生過:v0.3.0 沒有 tag,因為它是隨 v0.4.0 那次合併一起釋出的。

CHANGELOG 標題裡的 SHA 是選填的

標題格式是 ## vX.Y.Z · YYYY-MM-DD,後面可以再加 merge commit 的 short SHA。

但它不能是「合併前改名」的前置條件——那個 SHA 是 dev → main 的 merge commit, 合併前根本不存在。原本的規則把它寫成必填,結果連續三次進版都乾脆跳過改名, 於是 v0.2.1v0.4.0 兩個已發佈版本一直躺在 CHANGELOG 的「未發佈」一節底下。

commit 本身記在 tag 描述裡,不會遺失。合併後想補 SHA 再補。

tokens-v* 則刻意維持人工,它是 npm 發佈的第二道閘門。兩條對等的路:

# 路一:本機打 tag(推上去即觸發 publish-tokens.yml)
git tag -a tokens-v0.1.3 -m "tokens 0.1.3" && git push origin tokens-v0.1.3

路二:GitHub 網頁 → Actions → Publish tokens → Run workflow → 取消勾選 dry_run。不需要開終端機,效果相同(0.1.2 就是這樣發的)。

自動蓋的 v* 不會誤觸發發佈:它不符合 tokens-v* 的比對模式, 而且 GITHUB_TOKEN 建立的 ref 依 GitHub 的防遞迴機制本來就不會觸發其他 workflow。

Token 的 SemVer 定義

變更級別
移除或改名一個 tokenmajor
改變一個 token 的語意--warning 從黃變紅)major
改變 --radius 之類會影響全站外觀的基準值major
新增 tokenminor
微調色值但語意不變(對比修正)patch
修正 CSS 產物的 bugpatch

什麼不該發版

上面那張表定義了「什麼變更算哪一級」,但漏了另一半——不是每次進版都該動 npm 版號:

變更npm 版號main
封裝設定(exportsfiles不 bump
CI、建置腳本、測試不 bump
文件、註解、重構、改名不 bump

這些照樣進 main、文件站與 registry 照樣更新,但躺著等下次真的有 token 變更時順路帶出去

✅ 這樣做

移除 token 前先標記棄用,保留至少一個 minor 版本, 並在 CHANGELOG 寫清楚替代方案。

🚫 不要這樣

不要「順手改名」。token 名稱是契約,改名對所有宿主都是編譯失敗 或更糟——樣式默默失效。

🚫 不要這樣

不要為了驗證發佈管線而發版。沒有內容變更的版本會稀釋 「token 幾乎不會被改,所以它才是契約」這個主張——取用者看到版號一直跳, 就不會相信它穩定。要驗證管線請用 --dry-run

元件的 SemVer 定義

變更級別
移除或改名 propmajor
改變預設行為(例如分頁預設值)major
新增 prop(有預設值)minor
新增元件minor
視覺微調、修 bugpatch

視覺變更也算變更。 「只是把 padding 從 8px 改成 12px」對已經對齊過版面的使用者 就是一次 breaking change。至少要 patch,而且要寫進 CHANGELOG。

元件走 registry 複製散佈,沒有 npm 版號可以鎖,所以版號改用戳記的形式交付: 每個 registry item 與 /r/index.json 都帶 version 欄位。取用端因此答得出兩個問題—— 「我抄的是哪一版」(看複製進來那份)與「現在最新是哪一版」(看線上 index)。

curl -s https://kielchang.github.io/dooping-design-book/r/index.json | jq -r .version

CHANGELOG 寫給誰看

寫給要升級的人,不是寫給自己。每一則要回答:

  1. 改了什麼
  2. 我需要做什麼(不需要就明說「無須調整」)
  3. 為什麼改(連到 ADR)

文件版本對映產品版號

上面講的是三層各自怎麼編號。但有兩個問題完全在讀者那一側,規則本身回答不了: 我現在讀的這頁文件對應哪一版產品?我手上這份產出物是哪一版產的?

文件站與產品同 repo 時,很容易讓文件永遠只有一個版本=主線最新狀態。 問題在於產品有版本,而使用者手上的不一定是最新版

「文件說設定頁有這個開關,我這裡沒有。」 「喔那是下一版才有的。」

這句對話會反覆發生,而每一次都在消耗使用者對文件的信任。 發生幾次之後,他不會再查文件,他會直接問人——那就是文件站失效的那一刻。

做法:文件也做版本快照,版號直接沿用產品版號

版本內容標示
current(掛在 /next開發中、尚未發佈未發佈橫幅,每頁常駐
最新快照=正式環境現在跑的版本預設顯示
舊快照還有人在用的舊版版本下拉可切

三個實作細節,每一個都對應一種會出錯的方式:

  1. 預設顯示「最新快照」,不是 current 預設值決定 99% 的讀者看到什麼; current 是還沒發佈的內容,把它當預設等於讓多數讀者讀到一份描述未來的文件。
  2. 未發佈的內容要有橫幅,而且要在每一頁。 讀者多半從搜尋引擎或別人貼的連結 直接落到某一頁,他不會經過首頁,也不會去看下拉選單。
  3. 設定要動態讀取已有的快照清單。 手寫版本清單會撞上先有雞還先有蛋: 設定裡宣告了 v1.5.0 但快照指令還沒跑,文件站當場建置失敗—— 而這件事發生的時間點,永遠是你正在發佈的時候。

發佈 SOP 要把快照綁進去:bump 版號 → 寫 CHANGELOG → 快照文件 → 合併發佈快照要跟在 bump 後面,成為同一個步驟。 拆成兩件事的話,被跳過的永遠是文件那一件—— 而跳過一次之後,那一版就永遠沒有對應的文件了。

✅ 這樣做

文件版號直接用產品版號,不要另起一套。 讀者要對照的是「我用的系統是哪一版」,不是「文件寫到第幾版」。

🚫 不要這樣

不要每次改錯字都快照一次。快照的意義是對映一次發佈; 快照數量應該等於發佈次數,多於它就只是在製造要維護的舊分支。

產出物的身分(provenance)

版號不只是給開發者看的。每一個離開系統的產出物,都要帶著自己的身分。

單一來源、建置時注入、三個地方顯示:

建置時注入:
__APP_VERSION__ = package.json 的 version
__GIT_SHA__ = CI 環境變數的 commit(取前 7 碼),本地建置= "dev"
__BUILD_TIME__ = 建置當下時間

單一來源:
version.ts 讀這三個常數,對外只匯出組好的字串與連結
顯示位置形式為什麼是這裡
側邊欄最底v1.4.2 · a3f9c21(可點開對應 commit)常駐但不佔位。要回報問題時看一眼就有
設定 →「關於」版號、commit 連結、建置時間、環境需要完整資訊時的去處
列印頁尾v1.4.2 · a3f9c21 2026-07-29 14:20 產生最重要的一個,理由見下

為什麼列印頁尾最重要

螢幕上的東西可以隨時再看一次;印出來的那張紙不會更新

使用者拿著一份三個月前印出來的文件說「這個數字不對」時, 你要回答的第一個問題是「這是哪一版算出來的」。 如果那張紙上沒有版號,這個問題無解——沒有人記得三個月前的那一天系統是哪一版, 而在那之後可能已經修過三次計算邏輯。

有了頁尾那一行,同一個回報就從「無法重現」變成「用 a3f9c21 重跑一次」。 做法見列印與匯出第 6 條。

本地建置要顯示 dev

v1.4.2 · dev ← 有人在自己機器上建的
v1.4.2 · a3f9c21 ← CI 建的,可追溯

這一個字省下的是另一種難查的問題:有人把本機建置的版本部署上去, 之後所有回報都對不到任何一個 commit。顯示 dev 讓這件事在第一眼就被看見。

✅ 這樣做

版號字串只有一個來源,側邊欄、關於頁、列印頁尾共用同一個匯出。

🚫 不要這樣

不要在列印頁尾另外寫一次版號。兩處手寫的版本字串一定會分歧, 而分歧的那一份通常就是印在紙上的那一份——最不容易被發現、也最需要正確的那一份。

✅ 這樣做

注入機制要防呆:常數沒被注入時(例如某個測試環境直接跑原始碼) 也不能讓畫面崩掉,退成 dev 即可。

🚫 不要這樣

不要讓「顯示版號」這件小事有能力讓整個應用程式白屏。 它的重要性在於出事時能追溯,不在於它自己出事。

為什麼元件版號與 token 分開

元件是要被複製走的。使用者複製之後,它就是使用者的程式碼—— 再幫他管版號沒有意義。元件版號存在的目的只有一個:讓人知道自己抄的是哪一版, 方便日後對照上游的修正。

也因為如此,版號檔放在元件庫裡面packages/react/src/version.ts), 不只是 package.json。元件是要被複製走的——複製走之後 package.json 留在原地, 而使用者最需要的資訊正是「我手上這份是哪一版」。