fix(cli): acr update/deploy 下載源由 GitHub codeload 改指 Gitea archive(Arcrun#4)

動機:D20 防 flag 鐵律下 self-hosted 用戶(如 Mira)不能碰 GitHub,
而「init 之後才新增的零件」(如 code 零件)唯一重裝管道 acr update 綁死
GitHub codeload → 沒有乾淨重裝路徑。

改動(只碰 CLI 下載邏輯,不動 README/cypher/mcp/tools):
- deploy.ts:ARCRUN_REPO 預設 uncle6me-web/Arcrun → Leo/Arcrun;
  新增 ARCRUN_GITEA_BASE(預設 https://git.uncle6.me)與 giteaToken()
  (ARCRUN_GITEA_TOKEN > GITEA_TOKEN,不寫死)。
- 抽出純函式 buildArchiveUrl / buildDownloadHeaders 走 Gitea archive API
  GET {base}/api/v1/repos/{owner}/{repo}/archive/{ref}.tar.gz,保留 #13 P2
  cache-buster + no-cache 防 stale;private repo 帶 Authorization: token。
- downloadRepoTarball 改用上述;401/403 給「設 GITEA_TOKEN」提示。
- update.ts docstring 對齊(GitHub release → Gitea archive;標註 install≈update)。

install≈update:init 與 update 共用 downloadAndDeploy,其內容指紋 manifest
天然「新零件補、內容未變者略過」,故用戶跑 acr update 即補裝新零件。

測試:cli/tests/deploy-url.test.ts(Node 內建 test runner,零新依賴)8 顆全綠,
涵蓋 URL 組裝、repo/base 覆蓋、token→header、public 無 token、env 優先序。
真正打 Gitea 下載/部署是 leo 的閘,不在本 PR。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015d5jDbuqT5Htwv3Q88XXKk
This commit is contained in:
Leo
2026-07-07 08:13:05 +00:00
parent b13b4dfe49
commit 794e3eba3a
4 changed files with 148 additions and 26 deletions
+72
View File
@@ -0,0 +1,72 @@
/**
* deploy-url.test.ts — 離線驗證下載源組裝(Arcrun#4codeload → Gitea archive)。
*
* 涵蓋純函式:buildArchiveUrlURL 組裝)、buildDownloadHeaderstoken → header)。
* 打真 Gitea 下載/部署是 leo 的閘,不在單元測試做。
*
* 用 Node 內建 test runnernode --test,零額外依賴;Node ≥22.18 自帶 TS type-stripping)。
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { buildArchiveUrl, buildDownloadHeaders } from '../src/lib/deploy.ts';
test('buildArchiveUrl 指向 Gitea archive API,不再是 GitHub codeload', () => {
const url = buildArchiveUrl('main', 'bust123');
assert.ok(url.startsWith('https://git.uncle6.me/api/v1/repos/Leo/Arcrun/archive/main.tar.gz'),
`預期 Gitea archive API,實得:${url}`);
assert.ok(!/codeload|github\.com/.test(url), `不應含 GitHub 下載源:${url}`);
});
test('buildArchiveUrl 帶 cache-buster query#13 P2 stale 防護沿用)', () => {
const url = buildArchiveUrl('main', 'abc def'); // 含空白 → 應被 encode
assert.match(url, /[?&]_cb=abc%20def$/);
});
test('buildArchiveUrl ref 進 archive 檔名(tag/branch 皆走同路徑)', () => {
const url = buildArchiveUrl('v1.2.3', 'x');
assert.ok(url.includes('/archive/v1.2.3.tar.gz?'), url);
});
test('buildArchiveUrl 可用 repo / base 覆蓋(fork/自架站台)', () => {
const url = buildArchiveUrl('main', 'x', 'Mira/Arcrun', 'https://gitea.example.org');
assert.ok(url.startsWith('https://gitea.example.org/api/v1/repos/Mira/Arcrun/archive/main.tar.gz'), url);
});
test('buildDownloadHeaders:有 token → 帶 Gitea 慣用 Authorization: token <T>', () => {
const h = buildDownloadHeaders('secrettok');
assert.equal(h.Authorization, 'token secrettok');
assert.equal(h['Cache-Control'], 'no-cache');
assert.equal(h.Pragma, 'no-cache');
});
test('buildDownloadHeaders:無 tokenpublic repo)→ 不帶 Authorization,仍 no-cache', () => {
// 顯式傳 undefined 會觸發預設參數 giteaToken()(讀 env);要驗 public 路徑需清掉 env token。
const saved = { a: process.env.ARCRUN_GITEA_TOKEN, g: process.env.GITEA_TOKEN };
delete process.env.ARCRUN_GITEA_TOKEN;
delete process.env.GITEA_TOKEN;
try {
const h = buildDownloadHeaders(); // 走預設 → giteaToken() → undefined
assert.equal(h.Authorization, undefined);
assert.equal(h['Cache-Control'], 'no-cache');
} finally {
if (saved.a !== undefined) process.env.ARCRUN_GITEA_TOKEN = saved.a;
if (saved.g !== undefined) process.env.GITEA_TOKEN = saved.g;
}
});
test('buildDownloadHeadersARCRUN_GITEA_TOKEN 優先於 GITEA_TOKENenv 機制,不寫死)', () => {
const saved = { a: process.env.ARCRUN_GITEA_TOKEN, g: process.env.GITEA_TOKEN };
process.env.ARCRUN_GITEA_TOKEN = 'preferred';
process.env.GITEA_TOKEN = 'fallback';
try {
assert.equal(buildDownloadHeaders().Authorization, 'token preferred');
} finally {
if (saved.a === undefined) delete process.env.ARCRUN_GITEA_TOKEN; else process.env.ARCRUN_GITEA_TOKEN = saved.a;
if (saved.g === undefined) delete process.env.GITEA_TOKEN; else process.env.GITEA_TOKEN = saved.g;
}
});
test('buildDownloadHeaders:空字串 token 視為無(避免送出 "token "', () => {
const h = buildDownloadHeaders('');
assert.equal(h.Authorization, undefined);
});