From 944da9fee4d08760e3ff47e8dc14c723f5065959 Mon Sep 17 00:00:00 2001 From: Jz0ojiang Date: Wed, 12 Aug 2026 15:38:55 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=91=E5=B8=83=E9=93=BE=E8=B7=AF=EF=BC=9Ata?= =?UTF-8?q?g=20=E8=A7=A6=E5=8F=91=20CI=20=E5=8F=91=20Release=EF=BC=8Cinsta?= =?UTF-8?q?ll.sh=20=E8=B4=9F=E8=B4=A3=E5=AE=89=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 产物是架构无关的 JS tarball(约 400 KB)而不是 bun 单文件: 单文件 94 MB 里几乎全是运行时,静态文件还得外置。架构差异 由安装侧的 bun 运行时吸收,版本钉死,和 CI 构建版本一起改。 --- .gitea/workflows/release.yml | 48 +++++ install.sh | 390 +++++++++++++++++++++++++++++++++++ 2 files changed, 438 insertions(+) create mode 100644 .gitea/workflows/release.yml create mode 100644 install.sh diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..ac6ac1e --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,48 @@ +# 发布流程:推送 v* 标签 → 测试 → 构建 → 把产物挂到 Gitea Release 上。 +# +# 产物 patchbay.tar.gz 与 CPU 架构无关(纯 JS + 静态文件), +# 架构差异由目标机器上的 bun 运行时吸收,安装侧见仓库根目录的 install.sh。 +name: release + +on: + push: + tags: + - 'v*' + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # 构建用的 bun 版本和 install.sh 里的 BUN_VERSION 保持一致,升级时两处一起改 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: '1.3.14' + + - run: bun install --frozen-lockfile + - run: bun test + - run: bun run typecheck + + # 布局必须保持 server/ 与 web-dist/ 平级: + # server/routes.ts 里 WEB_DIST 用 import.meta.url 相对定位 ../web-dist/ + - name: 构建并打包 + run: | + bun run build + bun build --target=bun --minify server/index.ts --outfile dist/patchbay/server/index.js + cp -r web-dist dist/patchbay/web-dist + echo "${GITHUB_REF_NAME}" > dist/patchbay/version.txt + cp dist/patchbay/version.txt version.txt + tar -czf patchbay.tar.gz -C dist patchbay + + # version.txt 单独作为 asset 再传一份,install.sh check 用它比对版本, + # 不用把整个 tarball 拉下来 + - name: 发布 Release + uses: https://gitea.com/actions/release-action@main + with: + # 默认令牌建不了 Release 时(老版本 Gitea 的权限限制), + # 在仓库 Settings → Actions → Secrets 里加一个 RELEASE_TOKEN + api_key: '${{ secrets.RELEASE_TOKEN || github.token }}' + files: |- + patchbay.tar.gz + version.txt diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..0b6ca6b --- /dev/null +++ b/install.sh @@ -0,0 +1,390 @@ +#!/usr/bin/env bash +# +# install.sh — Patchbay 的安装 / 更新 / 卸载脚本 +# +# 它管四样东西: +# 1. bun 运行时(私有副本,版本钉死) 默认 /opt/patchbay/bun/bun +# 2. 应用本体(来自 Gitea Release) 默认 /opt/patchbay/app/ +# 3. 环境变量文件 /etc/patchbay/env +# 4. systemd 服务 /etc/systemd/system/patchbay.service +# +# bash <(curl -fsSL https://git.im0o.top/im0o/mihomo-patchbay/raw/branch/main/install.sh) +# └─ 安装;已装过就更新到最新 Release(环境变量文件保持不动) +# install.sh check # 只查询有没有新版,不改动任何东西,退出码见 --help +# install.sh uninstall # 卸载;--purge 连环境变量文件和访问令牌一起删除 +# +# 前提:机器上已经用 mihomo-up.sh 装好了 mihomo(Patchbay 要读它的配置文件、 +# 调它的二进制做校验、连它的管理接口)。还没装先去装那个。 +# +set -Eeuo pipefail + +SELF_VERSION="1.0.0" + +REPO_URL="https://git.im0o.top/im0o/mihomo-patchbay" +# bun 版本钉死,避免运行时行为漂移;和 .gitea/workflows/release.yml 的构建版本一起改 +BUN_VERSION="${PATCHBAY_BUN_VERSION:-1.3.14}" + +PREFIX="${PATCHBAY_PREFIX:-/opt/patchbay}" +ENV_FILE="/etc/patchbay/env" +SERVICE="${PATCHBAY_SERVICE:-patchbay}" +UNIT_FILE="/etc/systemd/system/${SERVICE}.service" + +# mihomo-up.sh 装出来的默认布局;mihomo 装在别处时用这两个环境变量覆盖 +MIHOMO_BIN="${MIHOMO_BIN:-/usr/local/bin/mihomo}" +MIHOMO_CONF_DIR="${MIHOMO_CONF_DIR:-/etc/mihomo}" + +# GitHub 下载前缀(bun 从 GitHub 下载)。直连不通时填个反代,例如 https://ghfast.top/ +MIRROR="${MIHOMO_MIRROR:-}" + +WANT_VERSION=""; FORCE=0; ASSUME_YES=0; PURGE=0 +TMPDIR_="" + +# ───────────────────────── 输出 ───────────────────────── +if [[ -t 1 ]]; then + R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m'; W=$'\033[1m'; D=$'\033[2m'; N=$'\033[0m' +else + R=""; G=""; Y=""; W=""; D=""; N="" +fi + +log() { printf '%s\n' "$*" >&2; } +info() { log "${D}··${N} $*"; } +ok() { log "${G}✓${N} $*"; } +warn() { log "${Y}!${N} $*"; } +die() { printf '%s✗%s %s\n' "$R" "$N" "$*" >&2; exit 1; } + +cleanup() { + local rc=$? + trap - ERR + [[ -n "${TMPDIR_:-}" && -d "$TMPDIR_" ]] && rm -rf "$TMPDIR_" + return $rc +} +trap cleanup EXIT +trap 'die "第 $LINENO 行出错,已中止"' ERR + +usage() { + cat <<'EOF' +install.sh — 安装 / 更新 / 卸载 Patchbay + + install.sh 安装;已装过就更新到最新 Release + install.sh check 只查询有没有新版,不改动任何东西 + install.sh uninstall [--purge] + +选项: + -y, --yes 非交互确认,所有询问都当作同意(用默认值) + --force 版本相同也重新安装 + --version 装指定版本的 Release,如 v0.1.0;也用于回滚 + --mirror GitHub 下载前缀(下载 bun 用),如 https://ghfast.top/ + --purge uninstall 时连环境变量文件和访问令牌一起删除 + -v 打印本脚本自身的版本号 + +check 退出码: 0 = 已是最新 10 = 有新版或未安装 1 = 出错 +EOF + exit "${1:-0}" +} + +# 从 /dev/tty 读,这样 bash <(curl ...) 这种 stdin 被占用的场景也能正常按键 +TTY_IN="/dev/stdin" +[[ -r /dev/tty ]] && TTY_IN="/dev/tty" + +ask_default() { # ask_default "提示" "默认值" varname + local __adf_def="$2" __adf_var="$3" __adf_ans="" + if (( ASSUME_YES )); then printf -v "$__adf_var" '%s' "$__adf_def"; return 0; fi + printf '%s %s[%s]%s ' "$1" "$D" "$__adf_def" "$N" >&2 + read -r __adf_ans < "$TTY_IN" || __adf_ans="" + [[ -z "$__adf_ans" ]] && __adf_ans="$__adf_def" + printf -v "$__adf_var" '%s' "$__adf_ans" +} + +# ───────────────────────── 基础能力 ───────────────────────── +need() { command -v "$1" >/dev/null 2>&1 || die "缺少命令: $1"; } + +require_root() { + [[ ${EUID:-$(id -u)} -eq 0 ]] || die "$1 需要 root 权限,请用 sudo 重新运行" +} + +gh_url() { # gh_url "owner/repo/releases/..." -> 完整下载地址 + local u="https://github.com/$1" + [[ -n "$MIRROR" ]] && u="${MIRROR%/}/$u" + printf '%s' "$u" +} + +release_url() { # release_url -> Gitea Release 下载地址 + if [[ -n "$WANT_VERSION" ]]; then + printf '%s/releases/download/%s/%s' "$REPO_URL" "$WANT_VERSION" "$1" + else + printf '%s/releases/latest/download/%s' "$REPO_URL" "$1" + fi +} + +# 读 config.yaml 顶层的简单键(形如 `secret: "xxx"`),套路取自 mihomo-up.sh 的 cfg_get +conf_get() { + local key="$1" f="$MIHOMO_CONF_DIR/config.yaml" + [[ -f "$f" ]] || return 0 + sed -n -E "s/^${key}:[[:space:]]*//p" "$f" | sed -n '1p' \ + | sed -E 's/[[:space:]]*#.*$//; s/^"(.*)"$/\1/; s/^'"'"'(.*)'"'"'$/\1/' +} + +installed_version() { + [[ -f "$PREFIX/app/version.txt" ]] && tr -d '[:space:]' < "$PREFIX/app/version.txt" || printf '' +} + +latest_version() { + local v + v="$(curl -fsSL --retry 2 --connect-timeout 5 --max-time 15 "$(release_url version.txt)" \ + | tr -d '[:space:]')" \ + || die "拉取版本号失败: $(release_url version.txt)(仓库还没发布过 Release?网络不通?)" + [[ "$v" =~ ^v[0-9]+\.[0-9]+ ]] || die "拿到的版本号格式不对: $v" + printf '%s' "$v" +} + +svc_running() { systemctl is-active --quiet "$SERVICE" 2>/dev/null; } + +# ───────────────────────── bun 运行时 ───────────────────────── +bun_asset() { + case "$(uname -m)" in + x86_64|amd64) + # bun 的常规 x64 构建要求 CPU 支持 AVX2,老机器要用 baseline 构建 + if grep -qw avx2 /proc/cpuinfo 2>/dev/null; then echo "bun-linux-x64" + else echo "bun-linux-x64-baseline"; fi ;; + aarch64|arm64) echo "bun-linux-aarch64" ;; + *) die "未适配的架构: $(uname -m)(bun 只发布 linux x64 / aarch64)" ;; + esac +} + +install_bun() { + local bun="$PREFIX/bun/bun" + if [[ -x "$bun" && "$("$bun" --version 2>/dev/null || true)" == "$BUN_VERSION" ]]; then + info "bun $BUN_VERSION 已就位,跳过" + return 0 + fi + need unzip + local asset url + asset="$(bun_asset)" + url="$(gh_url "oven-sh/bun/releases/download/bun-v${BUN_VERSION}/${asset}.zip")" + + info "下载 ${asset}.zip(约 30 MB)" + curl -fL -# --retry 3 --connect-timeout 15 --max-time 600 -o "$TMPDIR_/bun.zip" "$url" \ + || die "下载失败: $url(GitHub 不通就加 --mirror 换个下载源)" + unzip -q -o "$TMPDIR_/bun.zip" -d "$TMPDIR_" || die "解压失败,下载到的不是有效的 zip" + + # 闸门: 新 bun 能运行起来,且自报版本号和目标一致(也能暴露架构选错) + local got + got="$("$TMPDIR_/$asset/bun" --version 2>/dev/null || true)" + [[ "$got" == "$BUN_VERSION" ]] \ + || die "bun 校验失败: 期望 $BUN_VERSION,实际 ${got:-执行不了}(多半是架构不对)" + + mkdir -p "$PREFIX/bun" + install -m 0755 "$TMPDIR_/$asset/bun" "$bun" + ok "bun $BUN_VERSION → $bun" +} + +# ───────────────────────── 应用本体 ───────────────────────── +install_app() { # $1 = 目标版本 + local target="$1" url + url="$(release_url patchbay.tar.gz)" + + info "下载 patchbay.tar.gz" + curl -fL -# --retry 3 --connect-timeout 15 --max-time 300 -o "$TMPDIR_/app.tar.gz" "$url" \ + || die "下载失败: $url" + mkdir -p "$TMPDIR_/unpack" + tar -xzf "$TMPDIR_/app.tar.gz" -C "$TMPDIR_/unpack" || die "解压失败,下载到的不是有效的 tar.gz" + + # 闸门: 包里的东西齐不齐。server/ 和 web-dist/ 必须平级,服务端靠相对路径找前端 + [[ -f "$TMPDIR_/unpack/patchbay/server/index.js" && -d "$TMPDIR_/unpack/patchbay/web-dist" ]] \ + || die "包内布局和预期不符(应有 patchbay/server/index.js 和 patchbay/web-dist/),已放弃" + + # 应用目录无状态(配置和令牌都在别处),留一份旧版用于出问题时手动换回 + rm -rf "$PREFIX/app.prev" + [[ -d "$PREFIX/app" ]] && mv "$PREFIX/app" "$PREFIX/app.prev" + mv "$TMPDIR_/unpack/patchbay" "$PREFIX/app" + ok "已安装 $target → $PREFIX/app" +} + +# ───────────────────────── 环境变量文件 ───────────────────────── +# 各项的含义见仓库里的 server/env.ts;这里从 mihomo 的现有配置直接推导, +# 装完即可用,不用人工填一遍已经存在的信息。 +write_env() { + if [[ -f "$ENV_FILE" ]]; then + info "环境变量文件已存在,保持不动: $ENV_FILE" + return 0 + fi + + local conf="$MIHOMO_CONF_DIR/config.yaml" + [[ -f "$conf" ]] || die "找不到 $conf。先用 mihomo-up.sh 装好 mihomo 再来装 Patchbay" + [[ -x "$MIHOMO_BIN" ]] || die "找不到 mihomo 二进制 $MIHOMO_BIN(装在别处就用 MIHOMO_BIN=路径 覆盖)" + + local ctrl secret + ctrl="$(conf_get 'external-controller')" + secret="$(conf_get 'secret')" + [[ -n "$ctrl" && "$ctrl" != '""' ]] \ + || die "配置里没开管理接口(external-controller),Patchbay 靠它读线路和重载。先用 mihomo-up.sh 的配置向导开起来" + + # Patchbay 和内核在同一台机器上,管理接口绑了 0.0.0.0 也从环回地址连 + local api_host="${ctrl%:*}" api_port="${ctrl##*:}" + [[ "$api_host" == "0.0.0.0" || -z "$api_host" ]] && api_host="127.0.0.1" + + local port + ask_default " Patchbay 监听端口" "7580" port + [[ "$port" =~ ^[0-9]+$ ]] || die "端口要是数字,收到的是「$port」" + + mkdir -p "$(dirname "$ENV_FILE")" + cat > "$ENV_FILE" < "$1" </dev/null 2>&1 \ + || die "这台机器没有 systemd。手动运行: $PREFIX/bun/bun $PREFIX/app/server/index.js(环境变量见 $ENV_FILE)" + local tmp; tmp="$(mktemp)" + write_unit "$tmp" + if [[ -f "$UNIT_FILE" ]] && cmp -s "$tmp" "$UNIT_FILE"; then + rm -f "$tmp"; info "服务文件已是最新,没改动" + else + install -m 0644 "$tmp" "$UNIT_FILE"; rm -f "$tmp" + ok "已写入 $UNIT_FILE" + fi + systemctl daemon-reload +} + +start_service() { + systemctl enable "$SERVICE" >/dev/null 2>&1 && info "已设为开机自启" + systemctl restart "$SERVICE" || true + sleep 2 + if svc_running; then + ok "$SERVICE 已启动" + else + warn "$SERVICE 没有启动成功。看具体原因: journalctl -u $SERVICE -n 50 --no-pager" + return 1 + fi +} + +show_access() { + local port token="" + port="$(sed -n -E 's/^PATCHBAY_PORT=//p' "$ENV_FILE" 2>/dev/null | sed -n '1p')" + # 访问令牌由服务端首次启动时生成,存在配置文件旁边(见 server/auth.ts) + [[ -f "$MIHOMO_CONF_DIR/.patchbay-token" ]] && token="$(tr -d '[:space:]' < "$MIHOMO_CONF_DIR/.patchbay-token")" + + log "" + log " ${D}只监听本机。从你自己的电脑访问,先开一条 SSH 隧道:${N}" + log " ${W}ssh -N -L ${port:-7580}:127.0.0.1:${port:-7580} 用户名@这台机器${N}" + log " ${D}然后浏览器打开:${N}" + log " ${W}http://127.0.0.1:${port:-7580}${N}" + if [[ -n "$token" ]]; then + log " ${D}访问令牌${N} ${W}${token}${N}" + log " ${D}令牌存在 $MIHOMO_CONF_DIR/.patchbay-token,忘了随时回来看。${N}" + else + log " ${D}访问令牌在服务首次启动时生成: journalctl -u $SERVICE | grep 令牌${N}" + fi +} + +# ───────────────────────── 命令 ───────────────────────── +do_install() { + require_root "安装到 $PREFIX 并写入 systemd 服务" + need curl; need tar + TMPDIR_="$(mktemp -d)" + + local target cur + target="${WANT_VERSION:-$(latest_version)}" + cur="$(installed_version)" + if [[ "$cur" == "$target" && $FORCE -eq 0 ]]; then + ok "已是 $cur,无需操作(--force 可重装)" + return 0 + fi + + log "${W}Patchbay ${target}${N} ${D}${cur:+(当前 $cur)}→ $PREFIX${N}" + install_bun + install_app "$target" + write_env + install_unit + start_service || return 1 + show_access +} + +do_check() { + local cur latest + cur="$(installed_version)" + latest="$(latest_version)" + if [[ -z "$cur" ]]; then + log "未安装(最新 Release 是 $latest)"; exit 10 + elif [[ "$cur" == "$latest" ]]; then + log "已是最新 ($cur)"; exit 0 + else + log "有新版: $cur → $latest"; exit 10 + fi +} + +do_uninstall() { + require_root "卸载 Patchbay" + if command -v systemctl >/dev/null 2>&1; then + systemctl disable --now "$SERVICE" >/dev/null 2>&1 || true + rm -f "$UNIT_FILE" + systemctl daemon-reload + fi + rm -rf "$PREFIX" + ok "已删除 $PREFIX 和 systemd 服务" + if (( PURGE )); then + rm -f "$ENV_FILE" "$MIHOMO_CONF_DIR/.patchbay-token" + rmdir "$(dirname "$ENV_FILE")" 2>/dev/null || true + ok "已删除环境变量文件和访问令牌" + else + info "留下了 $ENV_FILE 和访问令牌,重装可以继续用;不要了加 --purge" + fi +} + +# ───────────────────────── 入口 ───────────────────────── +CMD="install" +while (( $# )); do + case "$1" in + install|check|uninstall) CMD="$1" ;; + -y|--yes) ASSUME_YES=1 ;; + --force) FORCE=1 ;; + --purge) PURGE=1 ;; + --version) shift; WANT_VERSION="${1:-}"; [[ -n "$WANT_VERSION" ]] || die "--version 后面要跟版本号" ;; + --mirror) shift; MIRROR="${1:-}"; [[ -n "$MIRROR" ]] || die "--mirror 后面要跟地址" ;; + -h|--help) usage ;; + -v) printf '%s\n' "$SELF_VERSION"; exit 0 ;; + *) die "不认识的参数: $1(--help 看用法)" ;; + esac + shift +done + +case "$CMD" in + install) do_install ;; + check) do_check ;; + uninstall) do_uninstall ;; +esac