e730b3f831
同源於 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>
112 lines
4.7 KiB
JavaScript
112 lines
4.7 KiB
JavaScript
/**
|
||
* 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✅ 所有目標:線上組態=宣告值。');
|
||
}
|