feat: replace git-based OTA with curl + Python tarfile (no git dependency)
This commit is contained in:
@@ -4,59 +4,107 @@ set -euo pipefail
|
|||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
# ── OTA update ────────────────────────────────────────────────────────────────
|
# ── OTA update ────────────────────────────────────────────────────────────────
|
||||||
# Ensure git is available. If not, attempt a quiet install via brew (macOS) or
|
# Uses curl + Python stdlib tarfile — no git or unzip required.
|
||||||
# apt-get (Linux). If the install fails or the OS is unsupported, skip OTA
|
#
|
||||||
# silently — never abort the script over a missing update tool.
|
# Flow:
|
||||||
if ! command -v git &>/dev/null; then
|
# 1. Fetch latest commit SHA from GitHub API (tiny JSON, ~1 KB, 5 s timeout).
|
||||||
echo " ℹ git not found — attempting to install..."
|
# 2. Compare against .poke_version (last installed SHA). Skip if already
|
||||||
_git_installed=0
|
# up to date or if the remote is unreachable.
|
||||||
if command -v brew &>/dev/null; then
|
# 3. Download the repo tarball only when an update exists (30 s timeout).
|
||||||
brew install git --quiet &>/dev/null && _git_installed=1 || true
|
# 4. Extract with Python tarfile, stripping the GitHub top-level prefix and
|
||||||
elif command -v apt-get &>/dev/null; then
|
# skipping protected local files (.env, config.yml, .venv).
|
||||||
sudo apt-get install -y -qq git &>/dev/null && _git_installed=1 || true
|
# 5. Persist new SHA to .poke_version and reinstall deps if requirements.txt
|
||||||
fi
|
# changed.
|
||||||
if [ "$_git_installed" -eq 1 ] && command -v git &>/dev/null; then
|
|
||||||
echo " ✓ git installed"
|
if command -v curl &>/dev/null && command -v python3 &>/dev/null; then
|
||||||
else
|
_OTA_REPO="kacperkwapisz/poke-mail"
|
||||||
echo " ⚠ Could not install git — skipping update check."
|
_OTA_BRANCH="main"
|
||||||
fi
|
_VERSION_FILE=".poke_version"
|
||||||
fi
|
|
||||||
|
|
||||||
# Pull latest changes from remote with a short timeout so we don't hang offline.
|
|
||||||
# If requirements.txt changed, reinstall dependencies afterwards.
|
|
||||||
if command -v git &>/dev/null && git rev-parse --is-inside-work-tree &>/dev/null 2>&1; then
|
|
||||||
echo "Checking for updates..."
|
echo "Checking for updates..."
|
||||||
REQS_BEFORE=$(git rev-parse HEAD:requirements.txt 2>/dev/null || echo "")
|
|
||||||
|
|
||||||
# git fetch with a 5-second timeout; silently skip if offline or unreachable
|
# Step 1: lightweight SHA check (fail silently if offline)
|
||||||
if git fetch --depth=1 origin --quiet --no-tags \
|
_REMOTE_SHA=$(curl -sf --max-time 5 \
|
||||||
-c core.sshCommand="ssh -o ConnectTimeout=5" \
|
"https://api.github.com/repos/${_OTA_REPO}/commits/${_OTA_BRANCH}" \
|
||||||
-c http.lowSpeedLimit=1 -c http.lowSpeedTime=5 \
|
| python3 -c \
|
||||||
2>/dev/null; then
|
"import json,sys; print(json.load(sys.stdin)['sha'])" \
|
||||||
LOCAL=$(git rev-parse HEAD)
|
2>/dev/null || echo "")
|
||||||
REMOTE=$(git rev-parse FETCH_HEAD 2>/dev/null || echo "")
|
|
||||||
|
|
||||||
if [ -n "$REMOTE" ] && [ "$LOCAL" != "$REMOTE" ]; then
|
_LOCAL_SHA=$(cat "$_VERSION_FILE" 2>/dev/null || echo "")
|
||||||
echo " ↳ Update found (${LOCAL:0:7} → ${REMOTE:0:7}), applying..."
|
|
||||||
git merge --ff-only FETCH_HEAD --quiet
|
|
||||||
echo " ✓ Updated to $(git rev-parse --short HEAD)"
|
|
||||||
|
|
||||||
# Re-check requirements.txt after update
|
if [ -z "$_REMOTE_SHA" ]; then
|
||||||
REQS_AFTER=$(git rev-parse HEAD:requirements.txt 2>/dev/null || echo "")
|
echo " ℹ Could not reach remote — continuing with local version."
|
||||||
if [ "$REQS_BEFORE" != "$REQS_AFTER" ]; then
|
echo ""
|
||||||
|
elif [ "$_REMOTE_SHA" = "$_LOCAL_SHA" ]; then
|
||||||
|
echo " ✓ Already up to date (${_REMOTE_SHA:0:7})"
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
echo " ↳ Update found (${_LOCAL_SHA:0:7:-} → ${_REMOTE_SHA:0:7}), downloading..."
|
||||||
|
|
||||||
|
# Hash requirements.txt before extraction so we can detect changes
|
||||||
|
_REQS_BEFORE=$(python3 -c \
|
||||||
|
"import hashlib; print(hashlib.md5(open('requirements.txt','rb').read()).hexdigest())" \
|
||||||
|
2>/dev/null || echo "")
|
||||||
|
|
||||||
|
_TMP_TAR=$(mktemp /tmp/poke-mail-update.XXXXXX.tar.gz)
|
||||||
|
|
||||||
|
if curl -sfL --max-time 30 \
|
||||||
|
"https://api.github.com/repos/${_OTA_REPO}/tarball/${_OTA_BRANCH}" \
|
||||||
|
-o "$_TMP_TAR" 2>/dev/null; then
|
||||||
|
|
||||||
|
# Extract with Python: strip GitHub's top-level dir, skip protected paths
|
||||||
|
python3 - "$_TMP_TAR" <<'PYEOF'
|
||||||
|
import sys, tarfile, os
|
||||||
|
|
||||||
|
archive = sys.argv[1]
|
||||||
|
# Files/dirs that must never be overwritten by an OTA update
|
||||||
|
PROTECTED = {'.env', 'config.yml', '.venv', '.poke_version'}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with tarfile.open(archive, 'r:gz') as tf:
|
||||||
|
members = tf.getmembers()
|
||||||
|
if not members:
|
||||||
|
sys.exit(0)
|
||||||
|
# GitHub tarball root dir is e.g. "owner-repo-<sha>/"
|
||||||
|
prefix = members[0].name.split('/')[0] + '/'
|
||||||
|
for m in members:
|
||||||
|
if not m.name.startswith(prefix):
|
||||||
|
continue
|
||||||
|
rel = m.name[len(prefix):] # path relative to repo root
|
||||||
|
if not rel: # skip the root dir entry itself
|
||||||
|
continue
|
||||||
|
top = rel.split('/')[0]
|
||||||
|
if top in PROTECTED:
|
||||||
|
continue
|
||||||
|
m.name = rel
|
||||||
|
try:
|
||||||
|
tf.extract(m, path='.', set_attrs=False)
|
||||||
|
except Exception:
|
||||||
|
pass # best-effort; don't abort on permission issues etc.
|
||||||
|
except Exception as e:
|
||||||
|
print(f' ⚠ Extraction error: {e}')
|
||||||
|
sys.exit(1)
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
# Persist new SHA so we don't re-download next run
|
||||||
|
echo "$_REMOTE_SHA" > "$_VERSION_FILE"
|
||||||
|
echo " ✓ Updated to ${_REMOTE_SHA:0:7}"
|
||||||
|
|
||||||
|
# Reinstall deps if requirements.txt changed
|
||||||
|
_REQS_AFTER=$(python3 -c \
|
||||||
|
"import hashlib; print(hashlib.md5(open('requirements.txt','rb').read()).hexdigest())" \
|
||||||
|
2>/dev/null || echo "")
|
||||||
|
if [ -n "$_REQS_BEFORE" ] && [ "$_REQS_BEFORE" != "$_REQS_AFTER" ]; then
|
||||||
echo " ↳ requirements.txt changed — reinstalling dependencies..."
|
echo " ↳ requirements.txt changed — reinstalling dependencies..."
|
||||||
# Activate venv if it already exists so pip targets the right env
|
|
||||||
[ -d .venv ] && source .venv/bin/activate
|
[ -d .venv ] && source .venv/bin/activate
|
||||||
pip install -q -r requirements.txt
|
pip install -q -r requirements.txt
|
||||||
echo " ✓ Dependencies updated"
|
echo " ✓ Dependencies updated"
|
||||||
fi
|
fi
|
||||||
echo ""
|
|
||||||
else
|
else
|
||||||
echo " ✓ Already up to date"
|
echo " ℹ Download failed — continuing with local version."
|
||||||
echo ""
|
|
||||||
fi
|
fi
|
||||||
else
|
|
||||||
echo " ℹ Could not reach remote — continuing with local version."
|
rm -f "$_TMP_TAR"
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -107,8 +155,6 @@ except Exception:
|
|||||||
|
|
||||||
if [ -n "$POKE_TOKEN" ]; then
|
if [ -n "$POKE_TOKEN" ]; then
|
||||||
echo " ✓ Poke API key detected from 'poke login'"
|
echo " ✓ Poke API key detected from 'poke login'"
|
||||||
# Pass token via env var to avoid shell-interpolation injection in Python source.
|
|
||||||
# json.dumps handles quoting/escaping so the result is valid YAML.
|
|
||||||
POKE_TOKEN="$POKE_TOKEN" python3 - <<'PYEOF'
|
POKE_TOKEN="$POKE_TOKEN" python3 - <<'PYEOF'
|
||||||
import os, re, json
|
import os, re, json
|
||||||
token = os.environ['POKE_TOKEN']
|
token = os.environ['POKE_TOKEN']
|
||||||
@@ -150,9 +196,6 @@ PYEOF
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# 4. MCP_API_KEY — generate once and persist to .env
|
# 4. MCP_API_KEY — generate once and persist to .env
|
||||||
# Regenerate when: .env is missing, contains the placeholder, or has an
|
|
||||||
# empty assignment (MCP_API_KEY=) which would still fail at the :? check.
|
|
||||||
# Anchored to non-commented, line-start assignments only.
|
|
||||||
if [ ! -f .env ] \
|
if [ ! -f .env ] \
|
||||||
|| grep -Eq '^[[:space:]]*MCP_API_KEY=your-secret-key-here' .env 2>/dev/null \
|
|| grep -Eq '^[[:space:]]*MCP_API_KEY=your-secret-key-here' .env 2>/dev/null \
|
||||||
|| ! grep -Eq '^[[:space:]]*MCP_API_KEY=.+' .env 2>/dev/null; then
|
|| ! grep -Eq '^[[:space:]]*MCP_API_KEY=.+' .env 2>/dev/null; then
|
||||||
@@ -162,7 +205,6 @@ alphabet = string.ascii_letters + string.digits
|
|||||||
print(''.join(secrets.choice(alphabet) for _ in range(48)))
|
print(''.join(secrets.choice(alphabet) for _ in range(48)))
|
||||||
")
|
")
|
||||||
if [ -f .env ]; then
|
if [ -f .env ]; then
|
||||||
# Anchor to line-start with MULTILINE so only the actual assignment is updated.
|
|
||||||
RANDOM_KEY="$RANDOM_KEY" python3 - <<'PYEOF'
|
RANDOM_KEY="$RANDOM_KEY" python3 - <<'PYEOF'
|
||||||
import os, re
|
import os, re
|
||||||
new_key = os.environ['RANDOM_KEY']
|
new_key = os.environ['RANDOM_KEY']
|
||||||
@@ -189,14 +231,11 @@ if [ -f .env ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Tunnel-mode detection ─────────────────────────────────────────────────────
|
# ── Tunnel-mode detection ─────────────────────────────────────────────────────
|
||||||
# When POKE_TUNNEL=1 the poke tunnel handles auth — MCP_API_KEY is optional.
|
POKE_TUNNEL="${POKE_TUNNEL:-1}"
|
||||||
# In all other modes (direct HTTP, Docker, etc.) it is required.
|
|
||||||
POKE_TUNNEL="${POKE_TUNNEL:-1}" # default to tunnel mode since start.sh always tunnels
|
|
||||||
|
|
||||||
if [ "${POKE_TUNNEL}" != "1" ]; then
|
if [ "${POKE_TUNNEL}" != "1" ]; then
|
||||||
: "${MCP_API_KEY:?MCP_API_KEY is not set — add it to .env or export it}"
|
: "${MCP_API_KEY:?MCP_API_KEY is not set — add it to .env or export it}"
|
||||||
else
|
else
|
||||||
# In tunnel mode warn when the key is absent but don't abort.
|
|
||||||
if [ -z "${MCP_API_KEY:-}" ]; then
|
if [ -z "${MCP_API_KEY:-}" ]; then
|
||||||
echo " ℹ MCP_API_KEY not set — server runs unauthenticated (safe: poke tunnel handles auth)."
|
echo " ℹ MCP_API_KEY not set — server runs unauthenticated (safe: poke tunnel handles auth)."
|
||||||
fi
|
fi
|
||||||
@@ -209,10 +248,8 @@ python3 src/server.py &
|
|||||||
SERVER_PID=$!
|
SERVER_PID=$!
|
||||||
trap "kill $SERVER_PID 2>/dev/null" EXIT
|
trap "kill $SERVER_PID 2>/dev/null" EXIT
|
||||||
|
|
||||||
# Wait for server to be ready
|
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|
||||||
# Tunnel to Poke — prefer the globally-installed poke binary; fall back to npx.
|
|
||||||
echo "Starting tunnel to Poke..."
|
echo "Starting tunnel to Poke..."
|
||||||
if command -v poke &>/dev/null; then
|
if command -v poke &>/dev/null; then
|
||||||
poke tunnel http://localhost:3000/mcp --name "poke-mail"
|
poke tunnel http://localhost:3000/mcp --name "poke-mail"
|
||||||
|
|||||||
Reference in New Issue
Block a user