diff --git a/README.md b/README.md index 73c7ea5b..b5cb505c 100644 --- a/README.md +++ b/README.md @@ -174,10 +174,20 @@ go build -o cyberstrike-ai cmd/server/main.go ### Version Update (No Breaking Changes) -**CyberStrikeAI version update (when there are no compatibility changes):** -1. Download the latest source code. -2. Copy the old project's `/data` folder and `config.yaml` file into the new source directory. -3. Restart with: `chmod +x run.sh && ./run.sh` +**CyberStrikeAI one-click upgrade (recommended):** +1. (First time) enable the script: `chmod +x upgrade.sh` +2. Upgrade with: `./upgrade.sh` (optional flags: `--tag vX.Y.Z`, `--no-venv`, `--preserve-custom`, `--yes`) +3. The script will back up your `config.yaml` and `data/`, upgrade the code from GitHub Release, update `config.yaml`'s `version`, then restart the server. + +Recommended one-liner: +`chmod +x upgrade.sh && ./upgrade.sh --yes` + +If something goes wrong, you can restore from `.upgrade-backup/` (or manually copy `/data` and `config.yaml` back) and run `./run.sh` again. + +Requirements / tips: +* You need `curl` or `wget` for downloading Release packages. +* `rsync` is recommended/required for the safe code sync. +* If GitHub API rate-limits you, set `export GITHUB_TOKEN="..."` before running `./upgrade.sh`. ⚠️ **Note:** This procedure only applies to version updates without compatibility or breaking changes. If a release includes compatibility changes, this method may not apply. diff --git a/README_CN.md b/README_CN.md index 2d45b58c..5f75fff7 100644 --- a/README_CN.md +++ b/README_CN.md @@ -173,9 +173,19 @@ go build -o cyberstrike-ai cmd/server/main.go ### CyberStrikeAI 版本更新(无兼容性问题) -1. 下载最新源代码; -2. 将旧项目的 `/data` 文件夹、`config.yaml` 文件复制至新版源代码目录; -3. 执行命令重启:`chmod +x run.sh && ./run.sh` +1. (首次使用)启用脚本:`chmod +x upgrade.sh` +2. 一键升级:`./upgrade.sh`(可选参数:`--tag vX.Y.Z`、`--no-venv`、`--preserve-custom`、`--yes`) +3. 脚本会备份你的 `config.yaml` 和 `data/`,从 GitHub Release 升级代码,更新 `config.yaml` 的 `version` 字段后重启服务。 + +推荐的一键指令: +`chmod +x upgrade.sh && ./upgrade.sh --yes` + +如果升级失败,可以从 `.upgrade-backup/` 恢复,或按旧方式手动拷贝 `/data` 和 `config.yaml` 后再运行 `./run.sh`。 + +依赖/提示: +* 需要 `curl` 或 `wget` 用于下载 GitHub Release 包。 +* 建议/需要 `rsync` 用于安全同步代码。 +* 如果遇到 GitHub API 限流,运行前设置 `export GITHUB_TOKEN="..."` 再执行 `./upgrade.sh`。 ⚠️ **注意:** 仅适用于无兼容性变更的版本更新。若版本存在兼容性调整,此方法不适用。 diff --git a/upgrade.sh b/upgrade.sh new file mode 100644 index 00000000..d6a6bce7 --- /dev/null +++ b/upgrade.sh @@ -0,0 +1,397 @@ +#!/usr/bin/env bash +set -euo pipefail + +# CyberStrikeAI GitHub one-click upgrade script (Release/Tag) +# +# Default preserves: +# - config.yaml +# - data/ +# - venv/ (disabled with --no-venv) +# +# Optional preserves (may overwrite upstream updates): +# - roles/ +# - skills/ +# - tools/ +# Enable with --preserve-custom + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$ROOT_DIR" + +BINARY_NAME="cyberstrike-ai" +CONFIG_FILE="$ROOT_DIR/config.yaml" +DATA_DIR="$ROOT_DIR/data" +VENV_DIR="$ROOT_DIR/venv" +KNOWLEDGE_BASE_DIR="$ROOT_DIR/knowledge_base" + +BACKUP_BASE_DIR="$ROOT_DIR/.upgrade-backup" + +GITHUB_REPO="Ed1s0nZ/CyberStrikeAI" + +TAG="" +PRESERVE_CUSTOM=0 +PRESERVE_VENV=1 +STOP_SERVICE=1 +FORCE_STOP=0 +YES=0 + +usage() { + cat < Specify GitHub Release tag (e.g. v1.3.28). + If omitted, the script uses the latest release. + --preserve-custom Preserve roles/skills/tools (may overwrite upstream files). + Use with caution. + --no-venv Do not preserve venv/ (Python deps will be re-installed). + --no-stop Do not try to stop the running service. + --force-stop If no process matching current directory is found, also stop + any cyberstrike-ai processes (use with caution). + --yes Do not ask for confirmation. + +Description: + The script backs up config.yaml/data/ (and optionally venv/roles/skills/tools) to + .upgrade-backup/ +EOF +} + +log() { printf "%s\n" "$*"; } +info() { log "[INFO] $*"; } +warn() { log "[WARN] $*"; } +err() { log "[ERROR] $*"; } + +have_cmd() { command -v "$1" >/dev/null 2>&1; } + +http_get() { + # $1: url + if have_cmd curl; then + # If GITHUB_TOKEN is provided, use it for api.github.com to avoid low rate limits. + if [[ -n "${GITHUB_TOKEN:-}" && "$1" == https://api.github.com/* ]]; then + curl -fsSL -H "Authorization: Bearer ${GITHUB_TOKEN}" "$1" + else + curl -fsSL "$1" + fi + elif have_cmd wget; then + wget -qO- "$1" + else + err "curl or wget is required to download GitHub releases. Please install one of them." + exit 1 + fi +} + +stop_service() { + # Try to stop the service that is running from the current project directory. + # If nothing is found and --force-stop is enabled, stop all cyberstrike-ai processes. + if [[ "$STOP_SERVICE" -ne 1 ]]; then + return 0 + fi + + local pids="" + if have_cmd pgrep; then + # Prefer matches where the command line contains the current project path. + pids="$(pgrep -f "${ROOT_DIR}.*${BINARY_NAME}" || true)" + if [[ -z "$pids" && "$FORCE_STOP" -eq 1 ]]; then + warn "No ${BINARY_NAME} process found under the current directory. Will try to force-stop all matching ${BINARY_NAME} processes." + pids="$(pgrep -f "${BINARY_NAME}" || true)" + fi + fi + + if [[ -z "$pids" ]]; then + info "No ${BINARY_NAME} process detected (or no matching process). Skipping stop step." + return 0 + fi + + warn "Detected running PID(s): ${pids}" + for pid in $pids; do + if kill -0 "$pid" 2>/dev/null; then + info "Sending SIGTERM to PID=${pid}..." + kill -TERM "$pid" 2>/dev/null || true + fi + done + + # Wait for exit + local deadline=$((SECONDS + 20)) + while [[ $SECONDS -lt $deadline ]]; do + local alive=0 + for pid in $pids; do + if kill -0 "$pid" 2>/dev/null; then + alive=1 + break + fi + done + if [[ "$alive" -eq 0 ]]; then + info "Service stopped." + return 0 + fi + sleep 1 + done + + warn "Timed out waiting for processes to exit. Still running PID(s): ${pids} (may still hold file handles)." + return 0 +} + +backup_dir_tgz() { + # $1: label, $2: path + local label="$1" + local path="$2" + if [[ -e "$path" ]]; then + info "Backing up ${label} -> ${BACKUP_BASE_DIR}/$(basename "$path").tgz" + tar -czf "${BACKUP_BASE_DIR}/$(basename "$path").tgz" -C "$ROOT_DIR" "$(basename "$path")" + fi +} + +backup_config() { + if [[ -f "$CONFIG_FILE" ]]; then + cp -a "$CONFIG_FILE" "${BACKUP_BASE_DIR}/config.yaml" + fi +} + +ensure_git_style_env() { + # No hard requirement; just a sanity check. + if [[ ! -f "$CONFIG_FILE" ]]; then + err "Could not find ${CONFIG_FILE}. Please verify you are in the correct project directory." + exit 1 + fi +} + +confirm_or_exit() { + if [[ "$YES" -eq 1 ]]; then + return 0 + fi + + if [[ ! -t 0 ]]; then + err "Non-interactive terminal detected. Please add --yes to continue." + exit 1 + fi + + warn "About to perform upgrade:" + info " - Preserve config.yaml: yes" + info " - Preserve data/: yes" + if [[ "$PRESERVE_VENV" -eq 1 ]]; then + info " - Preserve venv/: yes" + else + info " - Preserve venv/: no (will remove old venv and re-install deps)" + fi + if [[ "$PRESERVE_CUSTOM" -eq 1 ]]; then + info " - Preserve roles/skills/tools: yes (may overwrite upstream updates)" + else + info " - Preserve roles/skills/tools: no (will use upstream versions)" + fi + info " - Stop service: ${STOP_SERVICE}" + echo "" + read -r -p "Continue? (y/N) " ans + if [[ "${ans:-N}" != "y" && "${ans:-N}" != "Y" ]]; then + err "Cancelled." + exit 1 + fi +} + +resolve_tag() { + if [[ -n "$TAG" ]]; then + info "Using specified tag: $TAG" + return 0 + fi + + local api_url="https://api.github.com/repos/${GITHUB_REPO}/releases/latest" + info "Fetching latest Release..." + local json + json="$(http_get "$api_url")" + TAG="$(printf '%s' "$json" | python3 - <<'PY' +import json, sys +data=json.loads(sys.stdin.read() or "{}") +print(data.get("tag_name","")) +PY +)" + + if [[ -z "$TAG" ]]; then + local msg + msg="$(printf '%s' "$json" | python3 -c "import sys,json; d=json.loads(sys.stdin.read() or '{}'); print(d.get('message',''))" 2>/dev/null || true)" + if [[ -n "$msg" ]]; then + err "Failed to fetch latest tag: ${msg}" + else + err "Failed to fetch latest tag. Please try using --tag to specify the version." + fi + exit 1 + fi + info "Latest Release tag: $TAG" +} + +update_config_version() { + # Replace config.yaml's version: ... with the specified tag. + local new_tag="$1" + python3 - "$CONFIG_FILE" "$new_tag" </dev/null 2>&1 || true' EXIT + + local tarball="${tmp_dir}/source.tar.gz" + local url="https://github.com/${GITHUB_REPO}/archive/refs/tags/${TAG}.tar.gz" + info "Downloading source package: ${url}" + http_get "$url" >"$tarball" + + info "Extracting source package..." + tar -xzf "$tarball" -C "$tmp_dir" + + # GitHub tarball usually creates a top-level directory. + local extracted_dir + extracted_dir="$(ls -d "${tmp_dir}"/*/ 2>/dev/null | head -n 1 || true)" + if [[ -z "$extracted_dir" || ! -f "${extracted_dir}/run.sh" ]]; then + err "run.sh not found in the extracted directory. Please check network/download contents." + exit 1 + fi + + sync_code "$tmp_dir" "$extracted_dir" + + # Update config.yaml version display + if [[ -f "$CONFIG_FILE" ]]; then + info "Updating config.yaml version field to: $TAG" + update_config_version "$TAG" + fi + + info "Upgrade complete. Starting service..." + chmod +x ./run.sh + ./run.sh +} + +main "$@" +