#!/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..." mapfile -t ALL_DIRS < <( 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 " exit 1 fi mapfile -t CHANGED < <(git diff --name-only "$BASE_REF" HEAD) echo "Changed files (${#CHANGED[@]}):" for f in "${CHANGED[@]}"; do echo " $f"; done for d in "${ALL_DIRS[@]}"; do hit=0 for f in "${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[@]}"; 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[@]}"; do echo " - $t"; done echo " Tier 2 (orchestration, ${#TIER2[@]}):" for t in "${TIER2[@]}"; do echo " - $t"; done echo "" if [[ "$DRY_RUN" == true ]]; then echo "(dry-run,不實際 deploy)" exit 0 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[@]}"; do deploy_one "$d" || FAILED+=("$d") done for d in "${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[@]}"; do echo " ❌ $f"; done exit 1 fi