#!/bin/bash # github-arm.sh — 解除 GitHub 接觸保險(D20 發射鈕,leo 親手跑,AI 不得代跑) # 用法:scripts/github-arm.sh "任務描述" [有效分鐘,預設30,上限60] set -euo pipefail if [ ! -t 0 ]; then echo "❌ 本腳本必須由人類在終端機互動執行(防 AI 代按發射鈕)。" >&2 exit 1 fi MISSION="${1:-}" MINUTES="${2:-30}" if [ -z "$MISSION" ]; then echo "用法:scripts/github-arm.sh \"任務描述\" [分鐘]" >&2 exit 1 fi # 第二參數必須是純數字分鐘。2026-08-01 leo 實撞:AI 給的指令把中文註解寫在同一行, # 被當成分鐘數 → 下方 $(( )) 算術炸掉 → EXPIRY unbound、保險沒解成卻看似執行過。 case "$MINUTES" in ''|*[!0-9]*) echo "❌ 第二個參數要純數字分鐘(收到:$MINUTES)" >&2 echo " 正確:scripts/github-arm.sh \"任務描述\" 30" >&2 exit 1;; esac if [ "$MINUTES" -gt 60 ]; then MINUTES=60; fi if [ "$MINUTES" -lt 1 ]; then MINUTES=1; fi cd "$(dirname "$0")/.." # ── 搬遷預覽(arcrun-rag#27,leo:「解保險前要看得到這次會搬什麼」)────────── # 純讀本機已存在的檔案(ship.targets.json + stage/prod 兩份 manifest.json), # 完全不碰網路——此刻保險還沒解,不能在這裡先觸網。 # 找不到這些檔就安靜略過(本腳本不是只給出貨用,其他 GitHub 任務沒有這些檔是正常的)。 ARCRUN_RAG_TARGETS="products/arcrun-rag/installer/ship.targets.json" STAGE_BUNDLE_DIR="/private/tmp/arcrun-rag-bundles-staging" PROD_BUNDLE_DIR="/private/tmp/arcrun-rag-bundles" if [ -f "$ARCRUN_RAG_TARGETS" ] && [ -f "$STAGE_BUNDLE_DIR/manifest.json" ] && [ -f "$PROD_BUNDLE_DIR/manifest.json" ]; then echo "═══════════════════════════════════════════" echo "📦 搬遷預覽(只讀本機檔案,尚未碰網路)" echo "═══════════════════════════════════════════" python3 - "$ARCRUN_RAG_TARGETS" "$STAGE_BUNDLE_DIR" "$PROD_BUNDLE_DIR" <<'PYEOF' || echo "(搬遷預覽讀檔失敗,略過——不影響下面的保險流程)" import json, os, sys targets_json, stage_dir, prod_dir = sys.argv[1], sys.argv[2], sys.argv[3] cfg = json.load(open(targets_json)) prod = cfg.get("targets", {}).get("prod", {}) remote = prod.get("bundles", {}).get("remote", "(未知)") branch = prod.get("bundles", {}).get("branch", "main") stage = json.load(open(os.path.join(stage_dir, "manifest.json"))) prodm = json.load(open(os.path.join(prod_dir, "manifest.json"))) print(f"搬到哪:{remote}(branch {branch})") print(f"版本:prod 目前 {prodm.get('release')} → stage 現在是 {stage.get('release')}") stage_core = {c.get("name"): c.get("sha256", "") for c in stage.get("core", [])} prod_core = {c.get("name"): c.get("sha256", "") for c in prodm.get("core", [])} changed = [n for n in prod_core if stage_core.get(n) != prod_core.get(n)] same = [n for n in prod_core if n in stage_core and stage_core.get(n) == prod_core.get(n)] print(f"prod 管的 {len(prod_core)} 顆裡,內容會變的:{len(changed)} 顆") for n in changed: print(f" - {n}") if same: print(f"內容沒變(sha 相同):{len(same)} 顆 — {', '.join(same)}") has_local_clone = os.path.isdir(os.path.join(prod_dir, ".git")) est = "1 次(push;沿用已存在的本地 clone,不需再 clone)" if has_local_clone \ else "2 次(clone 1 + push 1;本地還沒有這個 clone)" print(f"預估碰 GitHub 幾次:{est}") print("(不含 jsDelivr purge——那是 CDN 快取清除,不算 GitHub 接觸,ROE 不計)") PYEOF echo "" else echo "(找不到本地 stage/prod bundle 的 manifest.json,略過搬遷預覽——非出貨類任務屬正常)" echo "" fi echo "═══════════════════════════════════════════" echo "🚀 GitHub 接觸儀式 — 發射前檢查(ROE)" echo "═══════════════════════════════════════════" echo "任務:$MISSION" echo "時效:$MINUTES 分鐘(到期自動回保險)" echo "" echo "交戰規則(每條都要守):" echo " □ 單一 repo,不跨 repo fan-out" echo " □ 網路請求 ≤5 次(clone/push/PR 各算一次)" echo " □ 禁批量操作、禁迴圈打 API、禁開/改 Actions" echo " □ 動作間隔像人手(秒級間隔,不連發)" echo " □ 一收到 403/429/驗證挑戰 → 立即全停回報" echo "" read -r -p "以上確認,解除保險?(yes/N) " CONFIRM if [ "$CONFIRM" != "yes" ]; then echo "已取消,保險維持。" exit 0 fi EXPIRY=$(( $(date +%s) + MINUTES * 60 )) printf '%s\n%s\n%s\n' "$EXPIRY" "$MISSION" "armed_at=$(date '+%Y-%m-%d %H:%M:%S')" > .github-armed echo "" echo "✅ 保險已解除至 $(date -r "$EXPIRY" '+%H:%M:%S')。期間每次 GitHub 接觸自動記入 github-contact-log.md。" echo " 提前上保險:rm .github-armed"