b17d0080ee
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
318 lines
12 KiB
Bash
318 lines
12 KiB
Bash
#!/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 diff,deploy 改到的 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 login(pnpm 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.2(macOS 內建)沒有 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 跑 whoami(pnpm 需要在 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 不只推 worker,npm CLI 也要同步)──
|
||
# 「推送 = 全部 publish target 到位」(tests/release.feature)。worker 走 wrangler,
|
||
# CLI 走 npm。只有 cli/ 在本次 diff 內 + 版本比 npm 新時才 publish(同版跳過,不假失敗)。
|
||
# 不在 --dry-run 時跑。需 npm login(npm 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")
|
||
|
||
# 自動昇版(richblack:deploy 時自動 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"
|
||
# CHANGELOG:prepend 新版本 + 本次 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
|
||
|
||
# 優先用 .env 的 NPM_API_TOKEN(authToken)——互動 npm login 常因 publish 政策 403。
|
||
# 無 token 才退回 npm whoami(互動登入)。token 不入 log。
|
||
NPM_TOK=""
|
||
if [[ -f .env ]]; then
|
||
NPM_TOK=$(grep '^NPM_API_TOKEN=' .env 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"'"'"' \r\n')
|
||
fi
|
||
PUB_RC=""
|
||
if [[ -n "$NPM_TOK" ]]; then
|
||
PUB_RC=$(mktemp)
|
||
printf '//registry.npmjs.org/:_authToken=%s\n' "$NPM_TOK" > "$PUB_RC"
|
||
fi
|
||
if [[ -z "$NPM_TOK" ]] && ! npm whoami >/dev/null 2>&1; then
|
||
echo " ⚠ 無 .env NPM_API_TOKEN 且未 npm login,跳過 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_CONFIG_USERCONFIG="${PUB_RC:-$HOME/.npmrc}" 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
|
||
[[ -n "$PUB_RC" ]] && rm -f "$PUB_RC"
|
||
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
|