Files
Arcrun/console-ui/scripts/verify-live.mjs
T
uncle6me-web e730b3f831 console-ui 部署:宣告值只解讀一次,並補上線上實況驗證(WIP)
同源於 2026-08-08 的 apiBase 事故:部署腳本把 apiBase/profile 印在
終端機上,卻沒有寫進推上去的產物 ⇒ 印對的、推錯的。

根因比想像深:deploy.targets.json 原本靠 build.mjs 在 build 期把
profile/apiBase 烤進產物,但 t160(e744ad1)清世代債時把 build.mjs
整支刪掉改成直接託管 public/,沒有人接手「把宣告值寫進產物」這件事
⇒ 前兩次事故的解等於被還原,只剩下「印出來給人看」。

本次落地:
- targets.mjs:宣告值的唯一讀取點,把「一個目標展開成期望的產物長相」
  定死成函式,供部署/驗證共用同一個答案,杜絕三邊各自解讀而漂移。
  缺 apiBase/accountId/verifyUrls/未定義的 profile 一律拒絕部署。
- verify-live.mjs:獨立可跑,驗「線上網址真的在用的組態」=「宣告值」。
- deploy.targets.json:補 _profiles(profile → views/home)與 verifyUrls。

WIP:deploy.mjs 尚未改接 targets.mjs,public/config.js 尚未退役。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 20:14:20 +08:00

112 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.
/**
* verify-live.mjs — 驗「線上網址現在真的在用的組態」=「deploy.targets.json 宣告的目標」。
*
* 用法:
* node scripts/verify-live.mjs 驗全部目標的全部對外網址
* node scripts/verify-live.mjs personal 只驗某個目標
* node scripts/verify-live.mjs --wait 容忍 CF Pages 生效延遲(重試)
* npm run verify
*
* 為什麼要這支(2026-08-08):部署腳本印出來的值,過去只是「它打算做什麼」,
* 沒有任何一步回頭確認「站上真的變成那樣」。deploy.mjs 推完會自動叫它;
* 它也可以獨立跑,用來回答「現在哪一台躺在錯的狀態」。
*
* 🔴 一律帶 no-cache(快取害人誤判過)。curl|grep 不算驗前端,但 config.js 是
* 純文字資產、VIEWS/HOME 是 HTML 內嵌常數,抓原始碼比對是「這一項」的正確驗法;
* 「頁面真的能用」另外走瀏覽器實載。
*/
import { loadTargets, resolveTarget, parseApiBase } from './targets.mjs';
const NOCACHE = { 'Cache-Control': 'no-cache', Pragma: 'no-cache' };
async function get(url) {
const res = await fetch(`${url}${url.includes('?') ? '&' : '?'}_nc=${Date.now()}`, {
headers: NOCACHE,
cache: 'no-store',
redirect: 'follow',
});
return { status: res.status, text: await res.text() };
}
/** 驗一個網址。回傳 { url, ok, checks:[{name, ok, want, got}] } */
export async function verifyUrl(t, url) {
const checks = [];
try {
const cfg = await get(`${url}/config.js`);
const got = cfg.status === 200 ? parseApiBase(cfg.text) : `HTTP ${cfg.status}`;
checks.push({ name: 'apiBase', ok: got === t.apiBase, want: t.apiBase, got: got ?? '(config.js 裡找不到 apiBase)' });
} catch (e) {
checks.push({ name: 'apiBase', ok: false, want: t.apiBase, got: `連線失敗:${e.message}` });
}
try {
const con = await get(`${url}/console/`);
const views = con.text.match(/var VIEWS = (\[[^\]]*\]);/);
const home = con.text.match(/var HOME = "([^"]*)";/);
const gotViews = con.status === 200 ? (views ? views[1] : '(找不到 VIEWS)') : `HTTP ${con.status}`;
const gotHome = con.status === 200 ? (home ? home[1] : '(找不到 HOME)') : `HTTP ${con.status}`;
checks.push({
name: `profile(${t.profile}).views`,
ok: gotViews === JSON.stringify(t.views),
want: JSON.stringify(t.views),
got: gotViews,
});
checks.push({ name: `profile(${t.profile}).home`, ok: gotHome === t.home, want: t.home, got: gotHome });
} catch (e) {
checks.push({ name: `profile(${t.profile})`, ok: false, want: t.profile, got: `連線失敗:${e.message}` });
}
return { url, ok: checks.every((c) => c.ok), checks };
}
export async function verifyTarget(name, { wait = false } = {}) {
const t = resolveTarget(name);
const attempts = wait ? 8 : 1;
let results = [];
for (let i = 1; i <= attempts; i++) {
results = [];
for (const url of t.verifyUrls) results.push(await verifyUrl(t, url));
if (results.every((r) => r.ok) || i === attempts) break;
process.stdout.write(` … 尚未生效,${5}s 後重試(${i}/${attempts - 1}\n`);
await new Promise((r) => setTimeout(r, 5000));
}
return { name, target: t, ok: results.every((r) => r.ok), results };
}
export function printReport(reports) {
for (const r of reports) {
console.log(`\n【${r.name}${r.target.description}`);
console.log(` 宣告:profile=${r.target.profile} apiBase=${r.target.apiBase}`);
for (const u of r.results) {
console.log(` ${u.ok ? '✅' : '❌'} ${u.url}`);
for (const c of u.checks) {
if (c.ok) console.log(` ✓ ${c.name} = ${c.got}`);
else console.log(` ✗ ${c.name}\n 宣告:${c.want}\n 線上:${c.got}`);
}
}
}
}
export async function verifyAll(names, opts) {
const reports = [];
for (const n of names) reports.push(await verifyTarget(n, opts));
return reports;
}
const isCli = process.argv[1] && import.meta.url === `file://${process.argv[1]}`;
if (isCli) {
const args = process.argv.slice(2);
const wait = args.includes('--wait');
const picked = args.filter((a) => !a.startsWith('--'));
const names = picked.length ? picked : loadTargets().names;
const reports = await verifyAll(names, { wait });
printReport(reports);
const bad = reports.filter((r) => !r.ok);
if (bad.length) {
console.error(`\n❌ ${bad.length} 個目標的線上組態與宣告不符:${bad.map((b) => b.name).join('、')}`);
console.error(' (站上實際在用的值 ≠ deploy.targets.json 宣告的值——這正是要被擋掉的那個病)');
process.exit(1);
}
console.log('\n✅ 所有目標:線上組態=宣告值。');
}