fix(cli): 更新完還看得到版本號——CLI 重部署不再把版本標籤(和你的設定)洗掉

leo 08-12 實撞:更新完 leo21c,Portal 設定頁的「版本」變成
「無法讀取目前版本(知識庫服務可能正在啟動)」。版本號是 leo 唯一的驗收介面,
看不到就等於他無法自己確認任何一次更新有沒有生效。

根因(Arcrun#106):`bundle_version` 來自部署時注入的 plain_text var
`ARCRUN_BUNDLE_VERSION`,而**只有安裝器會注入**。wrangler deploy 是整份覆蓋,
toml 沒寫的 var 直接消失 ⇒ CLI 更新那條路每跑一次就把標籤洗掉一次。
#97 修好了「櫃子」(KV/D1/Vectorize 沿用既有),沒修「櫃子上的標籤」。

修法(兩種 var 走相反的規則,這是本次的判斷):
· 設定類 var = 使用者實例的事實 → **沿用**(讀綁定時同一份回應就帶回來,不多打 API)
  ——把 #97「已部署的 worker 上綁著什麼就是事實」原封不動套用到 plain_text var。
· 版本標籤 = 這份成品的屬性 → **每趟重烙,絕不沿用舊值**。
  沿用舊值會得到一個永遠停在安裝當天的假標籤——比沒有標籤更糟,
  因為它會讓人以為驗收過了。
  版號取部署當下發行頻道公告的 release(Portal/daemon 就是拿它當「最新版」比),
  另外把**真正部署的 commit** 一起烙上去(/health 多吐 `bundle_commit`)→ 漂掉查得出來。
  查不到 release 就誠實退成 `YYYY-MM-DD+<commit7>`,不掰一個 semver 假裝已是最新。

順帶(都是同一條路上的東西):
· ref 先解析成 commit sha 再用 sha 下載 archive——不可變,順手解掉 branch tarball 被快取的老病
· Portal 版本行接受帶 build metadata 的 semver(`1.4.41+d61` 這種先前一律被當成「較舊版本」)
· cli 測試在 node 22 上本來一支都跑不起來(.js→.ts 解析 + parameter property),補上 resolve hook
  ——#97 那份「使用者的東西還在不在」的迴歸守衛也在其中,跑不起來的守衛等於沒有守衛
· types.ts 的 ARCRUN_BUNDLE_VERSION 重複宣告(TS2300)併回一處

驗證見 PR:cli 49/49 綠、cypher health 4/4 綠、Portal 版本行原始碼實跑五種情境、
對真實已部署 worker 的唯讀 dry-run。**未做**:真實實例上的 acr update 端到端
(本機唯一有憑證的帳號是 leo21c=紅線禁碰,youlin 無憑證)。

Refs: Leo/Arcrun#106, #97, #95

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
uncle6me-web
2026-08-12 21:58:43 +08:00
parent f87d0e92f4
commit 53b05c6d3d
12 changed files with 645 additions and 19 deletions
+8
View File
@@ -15,11 +15,19 @@ export const healthRouter = new Hono<{ Bindings: Bindings }>();
// 要在實例自己這一側就看得出來,不是等用戶登不進去才發現(#10「寧可明顯失敗」)。
// 只回統計不回內容(帳號數/有沒有 console 帳密/分片數),不洩漏任何 email 或雜湊。
// bundle_version 的既有行為不動(未注入就省略該欄——daemon 對空字串判 stale 是正確的)。
// Arcrun#106leo 08-12 實撞:更新完設定頁變成「無法讀取目前版本」):
// `bundle_version` 只在部署時被注入,而**只有安裝器會注入**——CLI 更新那條路重部署
// 等於把這個標籤洗掉(wrangler deploy 整份覆蓋,toml 沒寫的 var 直接消失)。
// 修在 CLI 那側(cli/src/lib/deploy.ts:既有 var 沿用 + 版本標籤每趟重烙)。
// 這裡只多吐一個 `bundle_commit`:版號是「發行頻道的編號」,commit 才是「真的部了哪份碼」——
// 兩個一起看才有辦法查「標籤有沒有跟成品漂掉」。沒注入就省略該欄(同 bundle_version 的既有行為)。
healthRouter.get('/health', (c) => {
const bundleVersion = c.env.ARCRUN_BUNDLE_VERSION;
const bundleCommit = c.env.ARCRUN_BUNDLE_COMMIT;
return c.json({
ok: true,
...(bundleVersion ? { bundle_version: bundleVersion } : {}),
...(bundleCommit ? { bundle_commit: bundleCommit } : {}),
auth_store: authStoreStatus(c.env),
// arcrun-rag#38/#69/#252026-08-11):安裝器判斷「要不要重推」只比 bundle_version——
// 但這次要修的洞是「installer 從沒注入過 PORTAL_MAIL_RELAY_BASE」,跟 bundle 內容
+9 -3
View File
@@ -75,6 +75,13 @@ export type Bindings = {
* 未注入(本地 dev/舊實例)= undefined/health 省略該欄。
*/
ARCRUN_BUNDLE_VERSION?: string;
/**
* Arcrun#106:這份成品實際來自哪個 commit(40 碼 sha)。
* `ARCRUN_BUNDLE_VERSION` 是**發行頻道的編號**semverPortal/daemon 拿它比新舊),
* 這個是**真的部了哪份碼**——兩個一起吐,標籤跟成品漂掉時查得出來。
* 由 `acr init/update`cli/src/lib/deploy.ts)注入;安裝器那條路沒有此 var → /health 省略該欄。
*/
ARCRUN_BUNDLE_COMMIT?: string;
// Platform telemetry api_key(可選,wrangler secret
// 對應 SDD .agents/specs/llm-interface/ M1.2
// 設了會把 agent-telemetry block 都聚集在 platform_telemetry user_id 下
@@ -103,9 +110,8 @@ export type Bindings = {
GITEA_TOKEN?: string; // wrangler secret(建議唯讀 scope token
GITEA_SPRINT_REPO?: string; // 預設 Leo/InkStoneCo
GITEA_SPRINT_DIR?: string; // 預設 system-dev/docs/3-specs/autonomy-dispatch
// 安裝器部署時注入的 bundle 版本(格式 "YYYY-MM-DD/commit",老實例無此 var)。
// daemon 比對此值決定是否提示用戶更新(/health 曝露,缺 var 時回空字串)。
ARCRUN_BUNDLE_VERSION?: string;
// ARCRUN_BUNDLE_VERSION 原本在這裡重複宣告了一次——TS2300 重複識別字,
// #106 順手併回上面那一處,說明同源,行為零變化。)
// MCP access_token 存活秒數的「顯示鏡像」(console 設定頁 MCP TTL 佔位區塊用)。
// 真相住在 mcp worker 的同名 envmcp/src/types.ts,預設 259200030 天);cypher 這份
// 只供顯示,兩處部署時要一致(#32 形態 config 同步教訓)。未設 → 頁面如實標「預設值」。
+29
View File
@@ -26,4 +26,33 @@ describe('GET /health — bundle_version 欄位', () => {
expect(data.ok).toBe(true);
expect(data.bundle_version).toBe('2026-07-28/6d06162');
});
// Arcrun#106CLI 更新那條路會多烙一個 commit(版號=發行頻道編號,commit=真的部了哪份碼)。
it('有 ARCRUN_BUNDLE_COMMIT 時一起回(acr update 注入情境)', async () => {
const fakeEnv = {
ARCRUN_BUNDLE_VERSION: '1.4.41',
ARCRUN_BUNDLE_COMMIT: 'f87d0e92f49690253e7c89c5badc82a08eb5d21b',
} as unknown as Bindings;
const res = await healthRouter.fetch(
new Request('http://localhost/health'),
fakeEnv,
{} as ExecutionContext,
);
const data = await res.json() as { bundle_version: string; bundle_commit: string };
expect(data.bundle_version).toBe('1.4.41');
expect(data.bundle_commit).toBe('f87d0e92f49690253e7c89c5badc82a08eb5d21b');
});
// 安裝器那條路沒有這個 var(回歸:不能因為多了新欄位就讓舊路徑多吐一個空字串出來)。
it('沒 ARCRUN_BUNDLE_COMMIT 就省略該欄(安裝器路徑不受影響)', async () => {
const fakeEnv = { ARCRUN_BUNDLE_VERSION: '1.4.41' } as unknown as Bindings;
const res = await healthRouter.fetch(
new Request('http://localhost/health'),
fakeEnv,
{} as ExecutionContext,
);
const data = await res.json() as { bundle_version: string; bundle_commit?: string };
expect(data.bundle_version).toBe('1.4.41');
expect(data.bundle_commit).toBeUndefined();
});
});