Files
arcrun-collector/cmd/arcrun-app/daemon-version.test.mjs
T
Leo 2e26528bc9 feat(version): 對外號真的不帶 v + release 掛得動成品 + 內外對應由機器產生(arcrun-rag#88)
leo 2026-08-18 當場抓到「剛剛不是說不要 v?」——實查確認不是措辭問題:
規約寫在 wiki,而**產生號碼的那條路照舊帶 v**。這輪修的是產地與載體。

① 對外號從**產生端**就是裸的
   `daemon-version.py` 是 daemon 那條線唯一的版本產生器,一路長到
   產物檔名(Arcrun-<版本>.dmg)、manifest.daemon.version、/api/latest。
   - 新戳的版本一律裸號;`RELEASED_RE` 加 `v?` 讀得懂歷史
   - 🔴 重打同一版**沿用 changelog 上原本的寫法**:selfupdate.go 的
     newerThanCurrent() 是字串不等於就提示更新 ⇒ 把 v0.18.28 改吐成 0.18.28
     會讓每台已安裝的機器收到一個內容相同卻宣稱是新版的**假更新**。
     規約會自己過期:頂上那段變裸號之後,重打路徑吐的也是裸號。
   - 實測:真實 changelog +一段未發佈 → 產生器吐 `0.18.30`(不是 v0.18.30)

② release 掛得動真的成品(本輪的硬前置)
   leo:「assets 都寫 source code,這兩個附檔實際是什麼?是 dmg 還是 go?」
   ——那是 Gitea/GitHub 自動產生的整包 repo 快照。實測既有 release 的
   attachment 數是 **0**,坐實那兩個附檔沒有一個是成品。
   - gitea/github-release.mjs 各補 uploadReleaseAsset/listReleaseAssets
     (兩邊 API 形狀不同是 API 本身的差別,不硬套成同一種)
   - release-lines.mjs 讓**每條版本線各自宣告**自己的成品(assetKeys);
     daemon 線明列 mac/win/msix,**排除釘在 0.15.7 的兩筆歷史遺留**
     ——照「有 file 欄就掛」會把舊檔掛上新版頁面,正是本輪要治的病
   - ship.mjs 建完 release 後掛檔,並**回頭查證**附件真的在頁面上;
     缺任何一個就擋(不留「點下去沒東西」的版本頁)

③ 內外對應由機器產生,不再是總管手寫
   leo:「對應關係記在該次發佈的 release note,不靠號碼本身編碼」
   - 新增 internal-version.mjs:`RAG-20260818-001-<commit7>`
   - 🔴 `formatInternalVersion` 收到空的 artifacts 直接丟例外
     ⇒ **算不出一個沒有成品的內部號**(leo:「內部每次就要出貨 bundle 的版本」)。
     ops-facts 原本記著「這件事目前靠自律,沒有機械閘」——這就是那道閘
   - 序號從**既有 release 的事實**算,不養會漂的帳;tag 與內文都掃
     (08-17 那兩筆內部號寫在 tag_name 上,只掃內文當天會從 1 重編、無聲撞號)

紅線遵守:既有 tag/檔名一個都沒回頭改;沒寫 GitHub(②③ 實測都在 Gitea);
沒部署、沒推 main、沒自己併分支。演練建的 release 與 tag 都已刪除複驗。

測試:installer 252 支全綠(原 233 +新 19);collector 產生器新增 6 支全綠。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:13:00 +08:00

90 lines
4.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* daemon-version.test.mjs — 證明**產生端**吐的是裸號(inkstone/arcrun-rag#88
*
* 🔴 為什麼這支測試存在,而不是改一個字串常數就算:
* leo 2026-08-17 定「對外號就是三個數字,不要 v」,規約寫進了
* InkStoneCo `system-dev/wiki/ops-facts.md`——然後 leo 隔天問「剛剛不是說不要 v?」,
* 因為 `install.arcrun.dev/api/latest` 照舊回 `daemon.version = "v0.18.28"`。
* ⇒ **規約只活在 wiki,產生號碼的那條路照舊帶 v。**
* 這支測試釘住的就是那條路:不是「常數長什麼樣」,是「**下一次產生版本號時它是裸的**」。
*
* 為什麼住在 `collector/` 而不是 `installer/scripts/`
* 被測的 `daemon-version.py` 是 collector 自己的版本產生器(D95 第一輪把它的三個輸入
* 全搬進 collector/,就是為了讓 collector/ 搬得成獨立 repo)。測試跟著被測物走,
* 拆 repo 時不會留下一支指向別人家的孤兒測試。
*
* 跑法:node --test collector/cmd/arcrun-app/daemon-version.test.mjs
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const SCRIPT = join(HERE, 'daemon-version.py');
/**
* 造一個**只有版本產生器需要的三樣東西**的假 collector 樹,然後真的跑那支腳本。
* 刻意不是 git repo`source_fingerprint()` 的 `git ls-files` 會失敗 → 回空字串 → 指紋閘不參與,
* 這裡要量的是「版本號長什麼樣」,不是指紋機制(它有自己的病史,不要攪在一起)。
*/
function runProducer({ changelog, line = '0.18', stamp = false }) {
const root = mkdtempSync(join(tmpdir(), 'daemon-version-'));
const appDir = join(root, 'cmd', 'arcrun-app');
mkdirSync(appDir, { recursive: true });
writeFileSync(join(root, 'CHANGELOG.md'), changelog);
writeFileSync(join(root, 'DAEMON_LINE'), `${line}\n`);
copyFileSync(SCRIPT, join(appDir, 'daemon-version.py'));
try {
const stdout = execFileSync('python3', [
join(appDir, 'daemon-version.py'), ...(stamp ? ['--stamp'] : []),
], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
return { version: stdout.trim(), changelogAfter: readFileSync(join(root, 'CHANGELOG.md'), 'utf8') };
} finally {
rmSync(root, { recursive: true, force: true });
}
}
const UNRELEASED = '## 下一版(未發佈)\n\n- 修了一件事\n\n';
const OLD_V = '## v0.18.282026-08-16\n\n- 舊的那一版,當初是帶 v 寫的\n';
const NEW_BARE = '## 0.18.292026-08-18\n\n- 新的那一版,裸號\n';
test('① 升版:下一個版本號是裸的(leo 2026-08-17「不要 v」)', () => {
const { version } = runProducer({ changelog: UNRELEASED + OLD_V });
assert.equal(version, '0.18.29');
assert.ok(!version.startsWith('v'), `產生端還在吐帶 v 的版本號:${version}`);
});
test('② 升版並戳章:寫進 changelog 的那一行也是裸的(下一輪讀到的就是裸號)', () => {
const { version, changelogAfter } = runProducer({ changelog: UNRELEASED + OLD_V, stamp: true });
assert.equal(version, '0.18.29');
assert.match(changelogAfter, /^## 0\.18\.29\d{4}-\d{2}-\d{2}$/m);
assert.ok(!/^## v0\.18\.29/m.test(changelogAfter), 'changelog 被戳成帶 v 的標題');
});
test('③ 換版本線:新線第一版也是裸的,不是只有 patch 遞增那條路被修', () => {
const { version } = runProducer({ changelog: UNRELEASED + OLD_V, line: '0.19' });
assert.equal(version, '0.19.0');
});
test('④ 重打同一版:沿用當初的寫法,**不回頭改已發出的版本**(紅線)', () => {
// 病:若這裡吐 `0.18.28`,每一台裝著 v0.18.28 的機器都會看到一個內容一樣卻宣稱是新版的更新
// ——selfupdate.go 的 newerThanCurrent() 是字串不等於就提示。
const { version } = runProducer({ changelog: OLD_V });
assert.equal(version, 'v0.18.28');
});
test('⑤ 過渡會自己過期:頂上那段已是裸號時,重打也吐裸號(不必記例外)', () => {
const { version } = runProducer({ changelog: NEW_BARE + OLD_V });
assert.equal(version, '0.18.29');
});
test('⑥ 讀得懂歷史:整份 changelog 都是舊的帶 v 寫法時,仍算得出下一版(且裸)', () => {
const onlyOld = UNRELEASED + '## v0.18.282026-08-16\n\n- a\n\n## v0.18.272026-08-15\n\n- b\n';
const { version } = runProducer({ changelog: onlyOld });
assert.equal(version, '0.18.29');
});