Files
Arcrun/scripts/local-deploy.sh
T
uncle6me-web 3d3de8b917 chore(release): deploy-from-committed 防呆流程(git 閘 + 清單)
壓測階段 6 抓到漂移新形態:cypher 改了但沒推 main → acr init 從 origin/main
codeload 抓到舊 worker → 薄殼打不存在的 /init/seed → 404。根因是「deploy 前沒先 push」。

防呆(沒 GH Actions,deploy 分開推,靠清單+閘不漏):
- scripts/check-release.sh 加「0. Git 同步」段:工作目錄髒 / 領先 origin/main 未 push → exit 1
- scripts/local-deploy.sh 實際 deploy 前強制跑 git 閘(未過拒絕 deploy;SKIP_GIT_CHECK=true 可強推)
- RELEASE-CHECKLIST.md:正確順序(先 commit+push main → 再 deploy → 線上打新端點驗證)

核心鐵則:self-hosted acr init 從 origin/main 抓 worker 源,故 git push 必須在 deploy 之前。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 15:51:51 +08:00

305 lines
12 KiB
Bash
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.
#!/usr/bin/env bash
# scripts/local-deploy.sh — 本機 deploy 取代 GH Actions
#
# 對應 LI SDD M5.6 / 2026-05-16 leo GH Actions 被停用後的 fallback
#
# 用法:
# bash scripts/local-deploy.sh # 偵測 git diffdeploy 改到的 worker
# bash scripts/local-deploy.sh --all # 全部 worker deploy
# bash scripts/local-deploy.sh cypher-executor registry # 指定 worker
# bash scripts/local-deploy.sh --base HEAD~3 # 改 diff base(預設 HEAD~1
# bash scripts/local-deploy.sh --dry-run # 只 list 不 deploy
#
# 邏輯模擬 .github/workflows/deploy.yml discover job
# - 掃所有 wrangler.toml 目錄
# - git diff base..HEAD 找改動
# - 對改到的 worker 跑 pnpm exec wrangler deploy
# - 若改到 registry/components/{name}/,連動 deploy .component-builds/{name}/
#
# 要求:
# - 已 wrangler loginpnpm exec wrangler whoami 確認)
# - 在 arcrun/ 根目錄執行
set -euo pipefail
ARCRUN_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ARCRUN_ROOT"
# ── Parse args ────────────────────────────────────────────────────────────────
BASE_REF="HEAD~1"
DRY_RUN=false
DEPLOY_ALL=false
EXPLICIT_TARGETS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--all) DEPLOY_ALL=true; shift ;;
--base) BASE_REF="$2"; shift 2 ;;
--dry-run) DRY_RUN=true; shift ;;
-h|--help)
sed -n '2,20p' "$0"
exit 0
;;
*) EXPLICIT_TARGETS+=("$1"); shift ;;
esac
done
# ── 1. 列所有可 deploy 的 worker 目錄 ─────────────────────────────────────────
# 排除 node_modules、wrangler.test.toml、Pages 專案
echo "🔍 Scanning worker directories..."
# bash 3.2macOS 內建)沒有 mapfile → 用 while read 相容寫法
ALL_DIRS=()
while IFS= read -r d; do
[ -n "$d" ] && ALL_DIRS+=("$d")
done < <(
find . -type f -name 'wrangler.toml' \
-not -path '*/node_modules/*' \
-not -name 'wrangler.test.toml' \
| while read f; do
if grep -q 'pages_build_output_dir' "$f"; then continue; fi
dirname "$f"
done \
| sort -u \
| sed 's|^\./||'
)
echo "Found ${#ALL_DIRS[@]} worker dirs"
# ── 2. 決定 targets ──────────────────────────────────────────────────────────
declare -a TARGETS=()
if [[ ${#EXPLICIT_TARGETS[@]} -gt 0 ]]; then
echo "🎯 Explicit targets: ${EXPLICIT_TARGETS[*]}"
for req in "${EXPLICIT_TARGETS[@]}"; do
matched=false
for d in "${ALL_DIRS[@]}"; do
if [[ "$d" == "$req" || "$(basename "$d")" == "$req" ]]; then
TARGETS+=("$d")
matched=true
fi
done
if [[ "$matched" == false ]]; then
echo "⚠️ '$req' not found in worker list (跳過)"
fi
done
elif [[ "$DEPLOY_ALL" == true ]]; then
echo "🎯 Mode: deploy all"
TARGETS=("${ALL_DIRS[@]}")
else
# git diff 過濾
echo "🎯 Mode: git diff $BASE_REF..HEAD"
if ! git rev-parse "$BASE_REF" >/dev/null 2>&1; then
echo "❌ base ref '$BASE_REF' not found. 改用 --all 或 --base <valid-ref>"
exit 1
fi
CHANGED=()
while IFS= read -r f; do
[ -n "$f" ] && CHANGED+=("$f")
done < <(git diff --name-only "$BASE_REF" HEAD)
echo "Changed files (${#CHANGED[@]}):"
for f in ${CHANGED[@]+"${CHANGED[@]}"}; do echo " $f"; done
for d in "${ALL_DIRS[@]}"; do
hit=0
for f in ${CHANGED[@]+"${CHANGED[@]}"}; do
if [[ "$f" == "$d"/* ]]; then hit=1; break; fi
done
# 連動:改 registry/components/{name}/ 也要 deploy .component-builds/{name}/
if [[ $hit -eq 0 && "$d" == .component-builds/* ]]; then
name="${d#.component-builds/}"
for f in ${CHANGED[@]+"${CHANGED[@]}"}; do
if [[ "$f" == "registry/components/$name"/* ]]; then hit=1; break; fi
done
fi
if [[ $hit -eq 1 ]]; then TARGETS+=("$d"); fi
done
fi
if [[ ${#TARGETS[@]} -eq 0 ]]; then
echo "✨ Nothing to deploy. All up-to-date."
exit 0
fi
# ── 3. 分 tier (component builds 先,orchestration 後) ────────────────────────
declare -a TIER1=() TIER2=()
for t in "${TARGETS[@]}"; do
if [[ "$t" == .component-builds/* ]]; then
TIER1+=("$t")
else
TIER2+=("$t")
fi
done
echo ""
echo "📦 Deploy plan:"
echo " Tier 1 (components, ${#TIER1[@]}):"
for t in ${TIER1[@]+"${TIER1[@]}"}; do echo " - $t"; done
echo " Tier 2 (orchestration, ${#TIER2[@]}):"
for t in ${TIER2[@]+"${TIER2[@]}"}; do echo " - $t"; done
echo ""
if [[ "$DRY_RUN" == true ]]; then
echo "(dry-run,不實際 deploy)"
exit 0
fi
# ── 3.5 Git 同步前置閘(壓測階段 6 教訓:deploy-from-committed)────────────────
# self-hosted `acr init` 從 origin/main codeload 抓 worker 源。若本機領先 main 未 push,
# 部署 prod 用的是新碼但 self-hosted 用戶抓到舊碼 → 薄殼打不存在的 API(seed 404)。
# ∴ 實際 deploy 前強制 git 同步檢查;未過用 --skip-git-check 可強制略過(自負風險)。
if [[ "${SKIP_GIT_CHECK:-false}" != "true" ]]; then
echo ""
echo "🔎 Git 同步前置檢查(deploy 前必過;要略過設 SKIP_GIT_CHECK=true..."
if ! bash scripts/check-release.sh >/tmp/arcrun-release-check.txt 2>&1; then
grep -E "✗|⚠|領先|未 commit|Git 未同步" /tmp/arcrun-release-check.txt | sed 's/^/ /'
echo "❌ Git 未同步 → 先 commit + git push 再 deploy(見 RELEASE-CHECKLIST.md)。"
echo " (確定要在未 push 狀態 deploy SKIP_GIT_CHECK=true bash scripts/local-deploy.sh ..."
exit 1
fi
echo " ✓ Git 已同步,繼續 deploy"
fi
# ── 4. wrangler whoami 健康檢查 ──────────────────────────────────────────────
# 隨便挑一個 worker dir 跑 whoamipnpm 需要在 npm package 內)
SAMPLE_DIR=""
for d in "${TARGETS[@]}"; do
if [[ -f "$d/package.json" ]]; then SAMPLE_DIR="$d"; break; fi
done
if [[ -z "$SAMPLE_DIR" ]]; then
echo "❌ 找不到任何 target 有 package.json (wrangler whoami 需要)"
exit 1
fi
echo "🔑 wrangler whoami:"
(cd "$SAMPLE_DIR" && pnpm exec wrangler whoami 2>&1 | grep -E "logged in|email" | head -2) || {
echo "❌ wrangler 沒登入。先跑:cd $SAMPLE_DIR && pnpm exec wrangler login"
exit 1
}
echo ""
# ── 5. 依序 deploy ──────────────────────────────────────────────────────────
deploy_one() {
local dir="$1"
echo ""
echo "▶ Deploying $dir ..."
pushd "$dir" >/dev/null
# 若是 component build,先確認有 component.wasm (從 registry build 出來)
if [[ "$dir" == .component-builds/* ]]; then
name="${dir#.component-builds/}"
if [[ ! -f "component.wasm" ]] && [[ -d "../../registry/components/$name" ]]; then
echo " ⚙️ rebuild WASM from registry/components/$name ..."
(cd "../../registry/components/$name" && tinygo build -target=wasi -o "$name.wasm" main.go && cp "$name.wasm" "../../../.component-builds/$name/component.wasm") || {
echo " ❌ TinyGo build failed for $name"
popd >/dev/null
return 1
}
fi
fi
# pnpm install 若沒 node_modules(首次)
if [[ ! -d "node_modules" ]] && [[ -f "package.json" ]]; then
echo " 📥 pnpm install (first time)..."
pnpm install --frozen-lockfile 2>&1 | tail -3
fi
if pnpm exec wrangler deploy 2>&1 | tail -4; then
echo " ✅ Done"
else
echo " ❌ Failed"
popd >/dev/null
return 1
fi
popd >/dev/null
}
FAILED=()
for d in ${TIER1[@]+"${TIER1[@]}"}; do
deploy_one "$d" || FAILED+=("$d")
done
for d in ${TIER2[@]+"${TIER2[@]}"}; do
deploy_one "$d" || FAILED+=("$d")
done
echo ""
echo "════════════════════════════════════════════════════════════"
if [[ ${#FAILED[@]} -eq 0 ]]; then
echo "✅ All ${#TARGETS[@]} workers deployed successfully"
else
echo "⚠️ ${#FAILED[@]}/${#TARGETS[@]} workers failed:"
for f in ${FAILED[@]+"${FAILED[@]}"}; do echo " ❌ $f"; done
fi
# ── 6. CLI npm publish(壓測報告第 3 點:deploy 不只推 workernpm CLI 也要同步)──
# 「推送 = 全部 publish target 到位」(tests/release.feature)。worker 走 wrangler
# CLI 走 npm。只有 cli/ 在本次 diff 內 + 版本比 npm 新時才 publish(同版跳過,不假失敗)。
# 不在 --dry-run 時跑。需 npm loginnpm whoami 確認)。
if [[ "${DRY_RUN:-false}" != "true" ]]; then
CLI_CHANGED=false
for t in "${TARGETS[@]:-}"; do [[ "$t" == "cli" || "$t" == "cli/" ]] && CLI_CHANGED=true; done
# --all 或 diff 含 cli/ 都算
if git diff --name-only "${BASE_REF}"..HEAD 2>/dev/null | grep -q '^cli/'; then CLI_CHANGED=true; fi
if [[ "${DEPLOY_ALL:-false}" == "true" ]]; then CLI_CHANGED=true; fi
if [[ "$CLI_CHANGED" == "true" ]]; then
echo ""
echo "▶ CLI npm publish ..."
LOCAL_V=$(node -p "require('./cli/package.json').version" 2>/dev/null || echo "?")
REMOTE_V=$(npm view arcrun version 2>/dev/null || echo "none")
# 自動昇版(richblackdeploy 時自動 bump,避免忘了改):
# 若本機版本 == npm 上版本(= 改了 cli 但沒 bump)→ 自動 patch +1。
# 留版本記錄:把本次 commit subject 寫進 cli/CHANGELOG.md。
if [[ "$LOCAL_V" == "$REMOTE_V" ]]; then
echo " · 版本未 bump$LOCAL_V 已在 npm),自動 patch +1 ..."
NEW_V=$(cd cli && npm version patch --no-git-tag-version 2>/dev/null | tr -d 'v')
LOCAL_V="$NEW_V"
# CHANGELOGprepend 新版本 + 本次 commit subject(無 commit 則標 manual deploy
COMMIT_SUBJ=$(git log -1 --format='%s' 2>/dev/null || echo 'manual deploy')
COMMIT_DATE=$(git log -1 --format='%ad' --date=short 2>/dev/null || echo '')
CHANGELOG="cli/CHANGELOG.md"
TMP_CL=$(mktemp)
{
echo "# arcrun CLI Changelog"
echo ""
echo "## $NEW_V$COMMIT_DATE"
echo "- $COMMIT_SUBJ"
echo ""
if [[ -f "$CHANGELOG" ]]; then tail -n +2 "$CHANGELOG"; fi
} > "$TMP_CL"
mv "$TMP_CL" "$CHANGELOG"
echo " · 已 bump → $NEW_V,並記錄進 $CHANGELOG(記得 commit 這兩個檔)"
fi
if ! npm whoami >/dev/null 2>&1; then
echo " ⚠ 未 npm loginnpm whoami 失敗),跳過 publish。手動:cd cli && npm publish"
FAILED+=("cli:npm-publish(未登入)")
else
echo " 📦 publish arcrun $REMOTE_V$LOCAL_V ..."
if (cd cli && npm run build >/dev/null 2>&1 && npm publish --access public 2>&1 | tail -3); then
echo " ✅ npm publish 完成(arcrun@$LOCAL_V"
else
echo " ❌ npm publish 失敗"
FAILED+=("cli:npm-publish")
fi
fi
fi
fi
echo ""
echo "════════════════════════════════════════════════════════════"
if [[ ${#FAILED[@]} -eq 0 ]]; then
echo "✅ 全部 publish target 到位(worker + 必要時 CLI npm"
else
echo "⚠️ ${#FAILED[@]} 項失敗(誠實回報,未假綠):"
for f in ${FAILED[@]+"${FAILED[@]}"}; do echo " ❌ $f"; done
exit 1
fi