2 Commits
Author SHA1 Message Date
im0o fbb2a824dc 发布步骤改成直接 curl Gitea API
release / release (push) Successful in 31s
release-action 是 Go 程序,runner 镜像里没有 Go 工具链,
装工具链不如十几行 curl,还少一个第三方依赖。
2026-08-12 15:45:29 +08:00
im0o 944da9fee4 发布链路:tag 触发 CI 发 Release,install.sh 负责安装
release / release (push) Failing after 1m11s
产物是架构无关的 JS tarball(约 400 KB)而不是 bun 单文件:
单文件 94 MB 里几乎全是运行时,静态文件还得外置。架构差异
由安装侧的 bun 运行时吸收,版本钉死,和 CI 构建版本一起改。
2026-08-12 15:38:55 +08:00
2 changed files with 452 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
# 发布流程:推送 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 拉下来。
# 直接 curl Gitea 的 API 而不用现成 action:gitea.com/actions/release-action
# 是 Go 程序,runner 镜像里没有 Go 工具链,跑不起来
- name: 发布 Release
env:
# 默认令牌建不了 Release 时(老版本 Gitea 的权限限制),
# 在仓库 Settings → Actions → Secrets 里加一个 RELEASE_TOKEN
TOKEN: ${{ secrets.RELEASE_TOKEN || github.token }}
run: |
api="${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}"
auth="Authorization: token ${TOKEN}"
# 建 Release;已存在(同一个 tag 重跑)就查回它的 id
resp="$(curl -fsS -X POST -H "$auth" -H 'Content-Type: application/json' \
-d "{\"tag_name\":\"${GITHUB_REF_NAME}\",\"name\":\"${GITHUB_REF_NAME}\"}" \
"$api/releases" \
|| curl -fsS -H "$auth" "$api/releases/tags/${GITHUB_REF_NAME}")"
# 响应里第一个 "id" 就是 release 自己的(嵌套对象如 author 排在它后面)
id="$(printf '%s' "$resp" | grep -o '"id":[0-9]*' | head -n1 | cut -d: -f2)"
[ -n "$id" ] || { echo "没拿到 release id,响应: $resp"; exit 1; }
for f in patchbay.tar.gz version.txt; do
curl -fsS -X POST -H "$auth" -F "attachment=@${f}" \
"$api/releases/${id}/assets?name=${f}" >/dev/null
echo "已上传 $f"
done
+390
View File
@@ -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 <v> 装指定版本的 Release,如 v0.1.0;也用于回滚
--mirror <url> 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 <asset> -> 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" <<EOF
# Patchbay 环境变量 — 由 install.sh v${SELF_VERSION} 生成于 $(date '+%F %T')
# 各项含义见 ${REPO_URL}(server/env.ts)。改完 systemctl restart ${SERVICE} 生效。
MIHOMO_API=http://${api_host}:${api_port}
MIHOMO_SECRET=${secret}
PATCHBAY_CONFIG=${conf}
PATCHBAY_MIHOMO_BIN=${MIHOMO_BIN}
PATCHBAY_MIHOMO_HOME=${MIHOMO_CONF_DIR}
PATCHBAY_HOST=127.0.0.1
PATCHBAY_PORT=${port}
EOF
# 里面有管理接口的密钥,不让其它用户读
chmod 0640 "$ENV_FILE"
ok "已生成 $ENV_FILE ${D}(权限 0640,密钥在里面)${N}"
}
# ───────────────────────── systemd 服务 ─────────────────────────
write_unit() {
cat > "$1" <<EOF
[Unit]
Description=Patchbay — mihomo 配置的网页界面
Documentation=${REPO_URL}
# 只是排序不是依赖: 内核连不上时 Patchbay 照样能起来编辑配置
After=network-online.target mihomo.service
Wants=network-online.target
[Service]
Type=simple
EnvironmentFile=${ENV_FILE}
ExecStart=${PREFIX}/bun/bun ${PREFIX}/app/server/index.js
WorkingDirectory=${PREFIX}/app
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
EOF
}
install_unit() {
command -v systemctl >/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