#!/bin/bash # deploy-web.sh — install/landing 的唯一部署入口(2026-07-28 立) # # 為什麼存在:leo 問「以後會出現改了沒推的問題嗎?」誠實答案=文字規範會被忘, # 只有機制可靠。本腳本把「驗語法→部署→驗版本有變→抓線上→記錄」焊成一步—— # 用它部署,就不可能出現 t79 那種「commit 了但部署失敗沒人發現」。 # # 用法:scripts/deploy-web.sh installer|landing # 部署狀態記錄在 system-dev/docs/4-guides/deploy-state.json(誰、何時、版本、檔案 sha) # ⇒ 「改了沒推」隨時可查:比對該檔現在的 sha 與記錄裡的 sha。 set -euo pipefail cd "$(dirname "$0")/.." TOP="$(pwd)" STATE="$TOP/system-dev/docs/4-guides/deploy-state.json" TARGET="${1:-}" # 部署源可用 DEPLOY_SRC 覆蓋(07-30:舊寫法寫死 scratchpad 且靜默 fallback, # scratchpad 是 session 專屬目錄,被清掉後部署源就斷——事故傳導路徑,故必印出本次部署源) SCRATCH="${DEPLOY_SRC:-/private/tmp/claude-501/-Users-youlinhsieh-Documents-tech-projects-InkStoneCo/92a75156-c295-4c79-bfeb-de1a20e4ed26/scratchpad}" case "$TARGET" in installer) DIR="$SCRATCH/rag-installer/installer/oauth-prototype" if [ ! -d "$DIR" ]; then echo "⚠️ 部署源 $DIR 不存在(scratchpad 被清?),改用 git 工作樹那份" DIR="$TOP/products/arcrun-rag/installer/oauth-prototype" [ -d "$DIR" ] || { echo "❌ 備援部署源也不存在(products 的 checkout 不在含 oauth-prototype 的分支);用 DEPLOY_SRC= 指定部署源"; exit 1; } fi FILE="$DIR/worker.js"; CFG="--config ./wrangler.toml"; URL="https://install.arcrun.dev/" ;; landing) DIR="$TOP/products/arcrun-rag/landing" FILE="$DIR/worker.js"; CFG=""; URL="https://rag.arcrun.dev/" ;; *) echo "用法:$0 installer|landing" >&2; exit 2 ;; esac echo "▶ 本次部署源=$DIR" echo "① esbuild 驗語法(node --check 抓不到 template literal 內的錯,t79 教訓)" # --external:cloudflare:* = Workers runtime 內建模組(如 cloudflare:email), # esbuild 不認識它們但 wrangler 認得;不標 external 會誤判成語法錯(07-29 landing 踩到)。 ( cd "$DIR" && npx esbuild worker.js --bundle --format=esm --outfile=/dev/null \ --external:cloudflare:* --external:./migrations.json --external:./workflows.json 2>/dev/null \ || npx esbuild worker.js --bundle --format=esm --outfile=/dev/null --external:cloudflare:* ) echo "①a 釘死 URL 必須真的存在(07-29 事故:釘碼用短碼拼湊出不存在的 commit,安裝全 404)" if [ "$TARGET" = "installer" ]; then BASE=$(grep -o "https://cdn.jsdelivr.net/gh/[^']*" "$FILE" | head -1) CODE=$(curl -s -m 30 -o /dev/null -w '%{http_code}' "$BASE/manifest.json") [ "$CODE" = "200" ] || { echo "❌ BUNDLE_BASE 指向的 manifest 回 $CODE(釘碼錯或 bundle 沒推):$BASE"; exit 1; } echo " $BASE/manifest.json → 200" fi echo "①b 文案契約測試(防「改好的又改錯」——禁句出現=拒絕部署)" if [ -f "$DIR/copy-contract.test.mjs" ]; then ( cd "$DIR" && node copy-contract.test.mjs ); fi echo "①c 防回退閘(07-30:版本號變動必須是人明示的決定,不能是部署源掉包的副作用)" LIVE_VER="$(curl -s -m 25 "${URL}?cb=$RANDOM" | grep -o '20[0-9][0-9]-[0-9][0-9]-[0-9][0-9]+[0-9a-f]\{7\}' | head -1 || true)" if [ "$TARGET" = "installer" ]; then NEXT_BUILT="$(grep -o "BUNDLE_BUILT = '[^']*'" "$FILE" | head -1 | sed "s/.*'\(.*\)'/\1/" || true)" NEXT_PIN="$(grep -o 'arcrun-rag-bundles@[0-9a-f]*' "$FILE" | head -1 | cut -d@ -f2 | cut -c1-7 || true)" { [ -n "$NEXT_BUILT" ] && [ -n "$NEXT_PIN" ]; } && NEXT_VER="${NEXT_BUILT}+${NEXT_PIN}" || NEXT_VER="" else NEXT_VER="$(grep -o 'SITE_BUNDLE_VERSION = "[^"]*"' "$DIR/wrangler.toml" 2>/dev/null | sed 's/.*"\(.*\)"/\1/' || true)" fi if [ -n "$LIVE_VER" ] && [ -n "$NEXT_VER" ] && [ "$LIVE_VER" != "$NEXT_VER" ] && [ "${ALLOW_VERSION_CHANGE:-}" != "1" ]; then echo "❌ 版本號會變:線上 $LIVE_VER → 要部署 $NEXT_VER" echo " 換版是人的決定:確定要換,重跑時帶 ALLOW_VERSION_CHANGE=1" exit 1 fi echo " 線上 ${LIVE_VER:-(抓不到)} / 待部 ${NEXT_VER:-(推導不出)}$([ "${ALLOW_VERSION_CHANGE:-}" = "1" ] && echo '(已明示換版)')" echo "①d 部署源必須落在 git(07-30:t143-t150 十筆只在本機沒推、雲端誤判程式碼失蹤的教訓)" PENDING_GIT=false if git -C "$DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then DIRTY="$(git -C "$DIR" status --porcelain -- "$(basename "$FILE")" | head -1 || true)" UNPUSHED="$(git -C "$DIR" log --oneline '@{u}..HEAD' 2>/dev/null | wc -l | tr -d ' ' || true)" [ -n "$DIRTY" ] && { echo " ⚠️⚠️ $(basename "$FILE") 有未 commit 修改——部署的東西 git 裡沒有!"; PENDING_GIT=true; } if [ "$UNPUSHED" = "" ] || ! git -C "$DIR" rev-parse '@{u}' >/dev/null 2>&1; then echo " ⚠️⚠️ 所在分支沒設 upstream——commit 了也沒人推得到 Gitea!"; PENDING_GIT=true elif [ "$UNPUSHED" != "0" ]; then echo " ⚠️⚠️ 有 $UNPUSHED 筆 commit 未推上 Gitea(git log @{u}..HEAD)"; PENDING_GIT=true fi $PENDING_GIT || echo " 部署源乾淨且已同步 Gitea ✓" else echo " ⚠️⚠️ 部署源不在任何 git 工作樹裡——這份程式碼沒有版控!"; PENDING_GIT=true fi # 封測期不擋出貨,但留痕:pending_git=true =「線上跑的和 Gitea 不一致」查得到 STATE_PATH="$STATE" python3 - "$TARGET" "$PENDING_GIT" <<'PY' import json,sys,os p=os.environ.get('STATE_PATH'); t,pg=sys.argv[1],sys.argv[2]=='true' try: d=json.load(open(p)) except Exception: d={} d.setdefault(t,{})['pending_git']=pg json.dump(d,open(p,'w'),ensure_ascii=False,indent=2) PY echo "② 部署(uncle6)" set -a; source "$TOP/.env" 2>/dev/null || true; set +a OUT="$(cd "$DIR" && CLOUDFLARE_ACCOUNT_ID=58309bb90fd93ad6d0fe0aae99170e9d npx wrangler deploy $CFG 2>&1)" VID="$(printf '%s' "$OUT" | grep -o 'Current Version ID: [a-f0-9-]*' | awk '{print $4}')" [ -n "$VID" ] || { echo "❌ 部署失敗(沒有 Version ID):"; printf '%s\n' "$OUT" | tail -8; exit 1; } echo " Version ID: $VID" echo "③ 版本必須有變(防『部署了但還是舊版』)" PREV="$(python3 -c " import json,sys try: print(json.load(open('$STATE')).get('$TARGET',{}).get('version','')) except Exception: print('')" 2>/dev/null)" if [ "$VID" = "$PREV" ] && [ -n "$PREV" ]; then echo "❌ Version ID 與上次相同($VID)=內容沒變或部署被跳過"; exit 1 fi echo "④ 線上實測(帶 cache-buster)" CODE="$(curl -s -m 25 -o /tmp/deploy_check.html -w '%{http_code}' "${URL}?cb=$RANDOM")" [ "$CODE" = "200" ] || { echo "❌ 線上回 $CODE"; exit 1; } echo " $URL → 200($(wc -c