c2638668e3
leo 2026-08-20:「同一個 plugin 你用,薄殼也用,保證兩邊同步」
「我要你幫雲端做薄殼,永遠都有問題,你要做的就是這組設定
你自己可以 dogfooding」
搬進來:41 支 hook(51 條註冊)/7 支 command/2 支 skill/23 支腳本。
不搬 .env、wiki、docs——那些是知識不是環境。
51 條 hook 路徑全部從 $CLAUDE_PROJECT_DIR/.claude/hooks/ 改成 ${CLAUDE_PLUGIN_ROOT}/hooks/,
零漏網。那正是薄殼一直壞掉的根:雲端 cwd 不是真身,寫死路徑就斷。
尚未驗證:Claude Code 能不能從私有 Gitea repo 裝 marketplace(要憑證)。
下一步就是在本機實際裝一次,通了才動雲端 bootstrap.sh。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
129 lines
7.4 KiB
Bash
Executable File
129 lines
7.4 KiB
Bash
Executable File
#!/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 </tmp/deploy_check.html | tr -d ' ') B)"
|
||
|
||
echo "⑤ 記錄(供 drift 檢查:檔案 sha ≠ 記錄 sha = 改了沒推)"
|
||
SHA="$(shasum -a 256 "$FILE" | cut -d' ' -f1)"
|
||
STATE_PATH="$STATE" python3 - "$TARGET" "$VID" "$SHA" <<'PY'
|
||
import json,sys,datetime,os
|
||
p=os.environ.get('STATE_PATH')
|
||
t,v,sha=sys.argv[1],sys.argv[2],sys.argv[3]
|
||
try: d=json.load(open(p))
|
||
except Exception: d={}
|
||
d[t]={'version':v,'file_sha256':sha,'deployed_at':datetime.datetime.now().isoformat(timespec='seconds')}
|
||
json.dump(d,open(p,'w'),ensure_ascii=False,indent=2)
|
||
print(f" {t}: {v} / sha {sha[:16]}")
|
||
PY
|
||
echo "✅ 完成。之後查「改了沒推」:shasum 該檔 vs $STATE"
|