42cb1d7aa9
leo 2026-08-08:「已經發生過一次這個錯誤,把舊版界面上到 prod, 你要確定不可再犯。」 實測(repo 對線上,純文字資產比對): repo portal/index.html 343,969 bytes 線上 mira.uncle6.me 82,911 bytes(Songti 12 處,舊金色 serif 品牌) 線上 pages.dev 82,911 bytes(同上) 而e730b3f那版 verify-live 對這兩站**三項檢查全過**——因為它驗的是 組態(apiBase、profile 的 views/home),不是世代。 ⇒ 一個網址可以組態完全正確、卻對外展示一套早就被淘汰的介面, 而所有機械檢查都說它是綠的。這就是要消滅的狀態。 本次落地: 一、世代指紋(targets.mjs) 逐一取線上/產物的資產(index / portal / console / favicon.svg), 遮掉本來就該隨部署目標不同的那兩行(VIEWS/HOME),其餘按位元組比對。 刻意不用關鍵字清單——清單要人維護,而舊世代能無聲上線正是因為沒人記得維護它。 誠實 trade-off 寫在檔內:repo 改了沒部署就會判紅,那是正確的(那時線上確實不當代)。 二、宣告值真的寫進產物(收掉e730b3f標的 WIP) deploy.mjs 改為由 targets.mjs 產出 .staging/<目標> 再推: config.js 由宣告值即時產生、console 的 VIEWS/HOME 依 profile 覆寫, **覆寫沒命中就中止部署**;推之前回頭讀磁碟上那份驗一次(不看腳本印了什麼)。 public/config.js 刪除——它是產物不是原始碼。 三、修好一道從 08-03 起就在誤判的閘 t160 的世代閘比對 portal 全文含「登記新庫」即拒部,而 66f1b59(08-03) 加了一則**說明「已經把它拿掉了」的 HTML 註解** ⇒ 該閘自那天起每次誤判, npm run deploy:personal 連續五天推不出去。改成剝掉註解後只看可見內容, 並降級為輔助(主判準是指紋)。這正是「手工關鍵字閘會腐爛」的實例。 四、讓它在該跑的時候真的被跑到(不再生出沒人記得執行的腳本) · deploy.mjs 推完自動回頭驗線上,不過就算本次部署失敗 · .deploy-state.json 只在線上實測通過後才寫,且不進版控 (新 checkout 沒紀錄=狀態未知=該被提醒,而不是繼承別人的綠燈) · Stop hook 每回合離線比對「手上這一代 vs 最後一次驗過的部署」, 在要說「做完了」的那一刻出聲(實測 0.096s,不連網) 五、uncle6 邊界寫進工具本身(leo 08-08:「要看範例只在 youlin 網站,不要去碰 uncle6」) deploy.targets.json 的 enterprise 標 frozen:deploy 拒絕部署、verify 連抓都不抓。 目標本身保留不刪——刪掉就變成下一個 AI 眼中「從來沒有過這個站」的失憶。 同源清掉兩處還活著的舊記錄:README 的線上 demo 連結、public/index.html 的註解。 驗收證據見 commit 後的實測輸出(舊世代樣本取自 git 歷史 ad367e4,本機起站餵判準, 未碰任何線上資源)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
109 lines
4.9 KiB
JavaScript
109 lines
4.9 KiB
JavaScript
/**
|
||
* deploy.mjs — 依具名目標部署 console-ui 到 Cloudflare Pages
|
||
*
|
||
* 用法:npm run deploy:personal
|
||
* npm run deploy:personal -- --dry-run (只產出並驗產物,不推)
|
||
*
|
||
* 為什麼不直接用 `wrangler pages deploy`(2026-07-22 leo 立,實際踩到才補):
|
||
* **兩個帳號都有名為 arcrun-console-ui 的 Pages 專案**
|
||
* wrangler 若 OAuth 登入在別的帳號,`--project-name arcrun-console-ui` 會部到別人的站上。
|
||
* 本腳本強制帶目標的 accountId,並在部署前印出目標,避免部錯帳號。
|
||
*
|
||
* 同時把 profile/apiBase 綁進目標(deploy.targets.json),不再靠部署者記得帶環境變數——
|
||
* 帶漏過三次:漏 profile 顯示成錯的版本、漏 apiBase 導致登入 405。
|
||
*
|
||
* 🔴 三道閘,全部**讀磁碟上真的要被推的那份**,不看本腳本自己印了什麼
|
||
* (2026-08-08 事故的形狀正是「印的是 A、推的是 B」):
|
||
* ① 產物閘 :宣告值有沒有真的寫進產物(apiBase / VIEWS / HOME)
|
||
* ② 世代閘 :產物是不是當代(指紋+t160 的文字指紋)
|
||
* ③ 線上閘 :推完回頭抓線上,組態+世代都要對上,否則本次部署算失敗
|
||
* 三閘都過才寫 .deploy-state.json(那份紀錄是「經過線上實測」的意思,不是「我跑過指令」)。
|
||
*/
|
||
import { spawnSync } from 'node:child_process';
|
||
import { join } from 'node:path';
|
||
import { ROOT, assertArtifact, buildArtifact, loadTargets, resolveTarget, writeState } from './targets.mjs';
|
||
import { printReport, verifyTarget } from './verify-live.mjs';
|
||
|
||
const args = process.argv.slice(2);
|
||
const dryRun = args.includes('--dry-run');
|
||
const name = args.find((a) => !a.startsWith('--'));
|
||
|
||
let t;
|
||
try {
|
||
if (!name) throw Object.assign(new Error('沒有指定部署目標'), { usage: true });
|
||
t = resolveTarget(name);
|
||
} catch (e) {
|
||
console.error(`✘ ${e.message}`);
|
||
if (e.usage) console.error(`用法:npm run deploy:<target>\n可用目標:${loadTargets().active.join(' / ')}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
if (t.frozen) {
|
||
console.error(`✘ 目標 ${name} 已凍結,拒絕部署。\n ${t.frozen}`);
|
||
console.error(' (要解凍是人的決定:改 deploy.targets.json 拿掉 frozen 欄位,並說明理由。)');
|
||
process.exit(1);
|
||
}
|
||
|
||
console.log(`\n部署目標:${name}`);
|
||
console.log(` 說明 :${t.description}`);
|
||
console.log(` 帳號 :${t.accountId}`);
|
||
console.log(` 專案 :${t.projectName}`);
|
||
console.log(` profile :${t.profile}`);
|
||
console.log(` apiBase :${t.apiBase}`);
|
||
|
||
// ── ①② 產出 + 驗產物 ────────────────────────────────────────────────
|
||
const outDir = join(ROOT, '.staging', name);
|
||
try {
|
||
buildArtifact(t, outDir);
|
||
} catch (e) {
|
||
console.error(`\n✘ 產出失敗:${e.message}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
const gate = assertArtifact(t, outDir);
|
||
console.log(`\n產物:${outDir}`);
|
||
console.log(` 世代指紋:${gate.generation.slice(0, 12)}`);
|
||
if (!gate.ok) {
|
||
console.error('\n✘ 產物閘不通過——推上去的會跟宣告的不一樣,拒絕部署:');
|
||
for (const p of gate.problems) console.error(` · ${p}`);
|
||
process.exit(1);
|
||
}
|
||
console.log(' ✅ 產物閘:宣告值確實寫進產物,且是當代。');
|
||
|
||
if (dryRun) {
|
||
console.log('\n(--dry-run:到此為止,沒有推任何東西。)');
|
||
process.exit(0);
|
||
}
|
||
|
||
// ── 推 ───────────────────────────────────────────────────────────────
|
||
const env = { ...process.env, DEPLOY_TARGET: name, CLOUDFLARE_ACCOUNT_ID: t.accountId };
|
||
// --commit-dirty:本地部署常有未提交變更,不因此中斷
|
||
const deploy = spawnSync(
|
||
'npx',
|
||
['wrangler', 'pages', 'deploy', outDir, '--project-name', t.projectName, '--commit-dirty=true'],
|
||
{ stdio: 'inherit', cwd: ROOT, env },
|
||
);
|
||
if (deploy.status !== 0) {
|
||
console.error('\n✘ wrangler 部署失敗。');
|
||
process.exit(deploy.status ?? 1);
|
||
}
|
||
|
||
// ── ③ 線上閘 ─────────────────────────────────────────────────────────
|
||
console.log('\n── 回頭驗線上(組態+世代)──');
|
||
const report = await verifyTarget(name, { wait: true });
|
||
printReport([report]);
|
||
if (!report.ok) {
|
||
console.error('\n✘ 推上去了,但線上跑的 ≠ 我們手上這一份。**本次部署視為失敗**。');
|
||
console.error(' (wrangler 說成功不代表對外網址就對——這正是要被擋掉的那個病。)');
|
||
process.exit(1);
|
||
}
|
||
|
||
writeState(name, {
|
||
generation: gate.generation,
|
||
apiBase: t.apiBase,
|
||
profile: t.profile,
|
||
urls: t.verifyUrls,
|
||
verifiedAt: new Date().toISOString(),
|
||
});
|
||
console.log('\n✅ 部署完成,且線上實測=宣告值+當代世代。已記入 .deploy-state.json。');
|