feat(self-hosted): acr update 內容指紋跳過未變動 worker(22 個沒變的不再白跑)+ --force 強制全部重部
壓測 2026-06-12:22/23 成功後重跑仍全部 pnpm install + wrangler deploy。 manifest 存 ~/.arcrun/deploy-manifest.json,指紋含注入後內容+accountId(換帳號/KV 自動重部), 只在成功後記錄(失敗者下次必重試)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,7 @@ import {
|
||||
type DeployContext,
|
||||
} from '../lib/deploy.js';
|
||||
|
||||
export async function cmdUpdate(): Promise<void> {
|
||||
export async function cmdUpdate(opts: { force?: boolean } = {}): Promise<void> {
|
||||
const config = loadConfig();
|
||||
|
||||
if (config.mode !== 'self-hosted') {
|
||||
@@ -81,7 +81,7 @@ export async function cmdUpdate(): Promise<void> {
|
||||
d1DatabaseId: d1DatabaseId || undefined,
|
||||
};
|
||||
|
||||
const result = await downloadAndDeploy(ctx);
|
||||
const result = await downloadAndDeploy(ctx, 'main', { force: opts.force });
|
||||
|
||||
if (result.implemented) {
|
||||
// message 含部分失敗清單(「部署 X/Y 成功,N 失敗:✗ ...」)——必須印出來,
|
||||
|
||||
+2
-1
@@ -176,7 +176,8 @@ program
|
||||
program
|
||||
.command('update')
|
||||
.description('self-hosted:拉新 release 並重新部署到你的 Cloudflare')
|
||||
.action(() => cmdUpdate());
|
||||
.option('--force', '強制重部所有 worker(忽略未變動跳過快取)')
|
||||
.action((opts: { force?: boolean }) => cmdUpdate({ force: opts.force }));
|
||||
|
||||
// acr install-harness(把 arcrun 的 CC harness 裝進當前專案)
|
||||
program
|
||||
|
||||
+74
-3
@@ -10,10 +10,59 @@
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { mkdtempSync, readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { tmpdir, homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { createHash } from 'node:crypto';
|
||||
import chalk from 'chalk';
|
||||
|
||||
/** 部署狀態 manifest:記錄上次成功部署每個 worker 的內容指紋(content hash),
|
||||
* 讓 acr update 跳過未變動的 worker(壓測 2026-06-12:22/23 成功後重跑仍全部
|
||||
* pnpm install + wrangler deploy,22 個沒變的白跑)。存 ~/.arcrun/。
|
||||
* 指紋含 wrangler.toml 注入後的內容 → 換帳號/KV 會變更指紋 → 自動重部,不會誤跳。*/
|
||||
const MANIFEST_PATH = join(homedir(), '.arcrun', 'deploy-manifest.json');
|
||||
|
||||
function loadManifest(): Record<string, string> {
|
||||
try {
|
||||
return JSON.parse(readFileSync(MANIFEST_PATH, 'utf8')) as Record<string, string>;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function saveManifest(m: Record<string, string>): void {
|
||||
try {
|
||||
writeFileSync(MANIFEST_PATH, JSON.stringify(m, null, 2));
|
||||
} catch {
|
||||
/* manifest 寫失敗不致命:下次全部重部(退化成舊行為,不會錯,只是慢) */
|
||||
}
|
||||
}
|
||||
|
||||
/** 算一個 worker 目錄的內容指紋:遞迴 hash 所有檔案(排除 node_modules),
|
||||
* 加上 accountId(換帳號要重部)。檔案路徑相對化後排序 → 跨機器/temp 目錄穩定。*/
|
||||
function dirContentHash(dir: string, accountId: string): string {
|
||||
const h = createHash('sha256');
|
||||
h.update(accountId);
|
||||
const walk = (d: string, rel: string): void => {
|
||||
let entries: string[];
|
||||
try { entries = readdirSync(d).sort(); } catch { return; }
|
||||
for (const name of entries) {
|
||||
if (name === 'node_modules' || name === '.git') continue;
|
||||
const full = join(d, name);
|
||||
const relPath = rel ? `${rel}/${name}` : name;
|
||||
let st;
|
||||
try { st = statSync(full); } catch { continue; }
|
||||
if (st.isDirectory()) {
|
||||
walk(full, relPath);
|
||||
} else {
|
||||
h.update(relPath);
|
||||
try { h.update(readFileSync(full)); } catch { /* skip unreadable */ }
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(dir, '');
|
||||
return h.digest('hex');
|
||||
}
|
||||
|
||||
/** GitHub repo(codeload tarball 來源)。fork 者改這裡或用 ARCRUN_REPO env。
|
||||
* 注意:repo 名大小寫敏感(codeload 路徑需完全一致)。*/
|
||||
const ARCRUN_REPO = process.env.ARCRUN_REPO ?? 'uncle6me-web/Arcrun';
|
||||
@@ -82,7 +131,11 @@ export function wranglerAvailable(): boolean {
|
||||
* @param ctx 部署上下文
|
||||
* @param ref git ref(branch / tag),預設 main;acr update 可帶 tag
|
||||
*/
|
||||
export async function downloadAndDeploy(ctx: DeployContext, ref = 'main'): Promise<DeployResult> {
|
||||
export async function downloadAndDeploy(
|
||||
ctx: DeployContext,
|
||||
ref = 'main',
|
||||
opts: { force?: boolean } = {},
|
||||
): Promise<DeployResult> {
|
||||
// 1. 下載 + 解壓 codeload tarball
|
||||
let root: string;
|
||||
try {
|
||||
@@ -106,7 +159,11 @@ export async function downloadAndDeploy(ctx: DeployContext, ref = 'main'): Promi
|
||||
const allDirs = [...tier1, ...tier2];
|
||||
const failures: string[] = [];
|
||||
let deployed = 0;
|
||||
console.log(chalk.gray(` → 部署 ${allDirs.length} 個 worker(每個含 install + deploy,依序進行)...`));
|
||||
let skipped = 0;
|
||||
// 內容指紋 manifest:未變動且上次成功的 worker 跳過(key 用 worker 名,不用 temp 絕對路徑)。
|
||||
// --force 清空 manifest → 全部重部。
|
||||
const manifest = opts.force ? {} : loadManifest();
|
||||
console.log(chalk.gray(` → 部署 ${allDirs.length} 個 worker(未變動者跳過,依序進行)...`));
|
||||
for (let i = 0; i < allDirs.length; i++) {
|
||||
const dir = allDirs[i];
|
||||
const tomlPath = join(dir, 'wrangler.toml');
|
||||
@@ -114,14 +171,28 @@ export async function downloadAndDeploy(ctx: DeployContext, ref = 'main'): Promi
|
||||
process.stdout.write(chalk.gray(` [${i + 1}/${allDirs.length}] ${label} ...`));
|
||||
try {
|
||||
injectWranglerConfig(tomlPath, ctx);
|
||||
// 注入後算指紋:與 manifest 比,相同 = 上次成功部過且內容沒變 → 跳過。
|
||||
const hash = dirContentHash(dir, ctx.accountId);
|
||||
if (manifest[label] === hash) {
|
||||
skipped++;
|
||||
console.log(chalk.gray(' ⊘ 未變動,跳過'));
|
||||
continue;
|
||||
}
|
||||
runWranglerDeploy(dir, ctx);
|
||||
manifest[label] = hash; // 只在成功後記錄 → 失敗者下次必重試
|
||||
saveManifest(manifest);
|
||||
deployed++;
|
||||
console.log(chalk.green(' ✓'));
|
||||
} catch (e) {
|
||||
delete manifest[label]; // 失敗 → 清掉舊指紋,確保下次重部
|
||||
saveManifest(manifest);
|
||||
failures.push(`${dir}: ${e instanceof Error ? e.message : String(e)}`);
|
||||
console.log(chalk.yellow(' ⚠'));
|
||||
}
|
||||
}
|
||||
if (skipped > 0) {
|
||||
console.log(chalk.gray(` (${skipped} 個未變動已跳過;要強制全部重部跑 acr update --force)`));
|
||||
}
|
||||
|
||||
// 3.5 KBDB Base: D1 建好後套 migration(建三表 + recipe_stat seed)。
|
||||
// 建 D1(cf-api ensureD1Database)只產生空資料庫,schema 要靠這步套。
|
||||
|
||||
Reference in New Issue
Block a user