From 6ee901dc6441bfbc6afb76fe3b6eb442a84ef6cd Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Fri, 28 Aug 2026 08:16:58 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=8F=91=E5=B8=83=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=20=E2=80=94=20=E5=85=A8=E5=B9=B3=E5=8F=B0=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E6=89=93=E5=8C=85=20+=20gitcode=20release=20=E8=B5=84=E4=BA=A7?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/release.sh: 前端构建 → 6 平台交叉编译 (linux/windows/darwin × amd64/arm64) → tar.gz/zip 打包 → SHA256SUMS;子目录隔离同名二进制 - scripts/upload_assets.py: gitcode 两步上传流程 (upload_url 签名 → urllib PUT 到 OBS),curl 会 401 但 urllib 干净成功 - 首次发布 v0.1.0: 6 个平台安装包 + SHA256SUMS 已上传,下载校验一致 --- .gitignore | 3 + scripts/release.sh | 137 +++++++++++++++++++++++++++++++++++++++ scripts/upload_assets.py | 77 ++++++++++++++++++++++ 3 files changed, 217 insertions(+) create mode 100755 scripts/release.sh create mode 100755 scripts/upload_assets.py diff --git a/.gitignore b/.gitignore index 4a9d8dd..1817f88 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ gui-test-screenshots/ # vendored deps (regenerable; keep out of git) vendor/ .pi-glla/ + +# 发布产物(由 scripts/release.sh 生成) +dist/ diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..77db1d5 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# +# webui4frpc 多平台发布脚本 +# +# 用法: +# ./scripts/release.sh [version] # 默认 version=0.1.0 +# +# 行为: +# 1. 构建前端 (web/dist) 并同步到 internal/httpapi/dist (go:embed 用) +# 2. 交叉编译全平台单二进制 (linux/windows/darwin × amd64/arm64) +# 3. 打包各平台安装包: +# - linux/darwin: tar.gz (二进制 + README + systemd 模板) +# - windows: zip (exe + README) +# 4. 生成 SHA256SUMS +# 5. 输出到 dist/artifacts// +# +# 环境变量: +# GOPROXY_OVERRIDE 覆盖 go module 代理 (默认 goproxy.cn,direct, 走本地 7890 代理) +# NO_BUILD_FRONTEND 设为 1 跳过前端构建 (用现有 dist) +set -euo pipefail + +VERSION="${1:-0.1.0}" +VERSION="${VERSION#v}" # 去掉可能的前导 v +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +OUT="$ROOT/dist/artifacts/$VERSION" +GOFLAGS_ENV="" + +# ---- 0. Go module 代理 (内网需走本地代理访问 goproxy.cn) ---- +if [ -n "${GOPROXY_OVERRIDE:-}" ]; then + GOFLAGS_ENV="GOPROXY=${GOPROXY_OVERRIDE}" +else + GOFLAGS_ENV="GOPROXY=https://goproxy.cn,direct" +fi +# 若本地 7890 代理存在则导出 (模块下载走代理) +if curl -s -o /dev/null -m 2 -x http://127.0.0.1:7890 https://goproxy.cn >/dev/null 2>&1; then + export HTTPS_PROXY=http://127.0.0.1:7890 HTTP_PROXY=http://127.0.0.1:7890 +fi + +echo "==> 版本: $VERSION" +mkdir -p "$OUT" + +# ---- 1. 构建前端 ---- +if [ "${NO_BUILD_FRONTEND:-0}" != "1" ]; then + echo "==> 构建前端" + ( cd "$ROOT/web" && npm run build ) + rm -rf "$ROOT/internal/httpapi/dist" + cp -r "$ROOT/web/dist" "$ROOT/internal/httpapi/dist" +else + echo "==> 跳过前端构建 (用现有 dist)" +fi + +# ---- 2. 交叉编译全平台 ---- +# target 格式: os arch 扩展名 [tar|zip] +TARGETS=( + "linux amd64" "linux arm64" + "windows amd64" "windows arm64" + "darwin amd64" "darwin arm64" +) + +declare -A PLATFORM_EXT=( + [linux-amd64]=tgz [linux-arm64]=tgz + [darwin-amd64]=tgz [darwin-arm64]=tgz + [windows-amd64]=zip [windows-arm64]=zip +) +declare -A PLATFORM_BIN=( + [linux-amd64]=webui4frpc [linux-arm64]=webui4frpc + [darwin-amd64]=webui4frpc [darwin-arm64]=webui4frpc + [windows-amd64]=webui4frpc.exe [windows-arm64]=webui4frpc.exe +) + +for target in "${TARGETS[@]}"; do + os="${target%% *}"; arch="${target##* }" + key="$os-$arch" + bin="${PLATFORM_BIN[$key]}" + # 每个平台编译到独立子目录,避免 darwin/linux 同名二进制互相覆盖 + bindir="$OUT/bin/$key" + mkdir -p "$bindir" + echo "==> 编译 $os/$arch" + env $GOFLAGS_ENV CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" \ + go build -trimpath -ldflags="-s -w" \ + -o "$bindir/$bin" "$ROOT/cmd/webui4frpc" +done + +# ---- 3. 打包各平台 ---- +# 整理 systemd 模板 +cat > "$OUT/webui4frpc.service.tpl" <<'TPL' +[Unit] +Description=webui4frpc - visual frpc controller (cluster node) +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +ExecStart=/opt/webui4frpc/webui4frpc -addr 0.0.0.0:7500 -user admin -password admin -workdir /opt/webui4frpc/data +Restart=always +RestartSec=5 +LimitNOFILE=65535 +NoNewPrivileges=true + +[Install] +WantedBy=multi-user.target +TPL + +for target in "${TARGETS[@]}"; do + os="${target%% *}"; arch="${target##* }" + key="$os-$arch" + bin="${PLATFORM_BIN[$key]}" + ext="${PLATFORM_EXT[$key]}" + base="webui4frpc-${VERSION}-${os}-${arch}" + + echo "==> 打包 $key -> $base.$ext" + cp "$ROOT/README.md" "$OUT/README.md" + bindir="$OUT/bin/$key" + # staging 目录: 把二进制 + README (+systemd) 放一起再一次性打包 + stage="$OUT/.stage/$key" + rm -rf "$stage"; mkdir -p "$stage" + cp "$bindir/$bin" "$stage/$bin" + cp "$OUT/README.md" "$stage/README.md" + if [ "$ext" = "zip" ]; then + ( cd "$stage" && zip -q "$OUT/$base.zip" "$bin" README.md ) + else + cp "$OUT/webui4frpc.service.tpl" "$stage/" + ( cd "$stage" && tar czf "$OUT/$base.tar.gz" "$bin" README.md webui4frpc.service.tpl ) + fi +done +rm -rf "$OUT/.stage" + +# ---- 4. 校验和 ---- +echo "==> 生成 SHA256SUMS" +( cd "$OUT" && sha256sum *.tar.gz *.zip 2>/dev/null | tee SHA256SUMS ) + +# ---- 5. 汇总 ---- +echo "" +echo "==> 发布产物位于: $OUT" +ls -lh "$OUT" +echo "" +echo "==> 完成" diff --git a/scripts/upload_assets.py b/scripts/upload_assets.py new file mode 100755 index 0000000..734fb58 --- /dev/null +++ b/scripts/upload_assets.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""上传 release 资产到 gitcode(两步:取签名 URL → PUT 到 OBS)。 + +用法: upload_assets.py [file...] +不传 file 时上传该版本目录下所有 tar.gz/zip 与 SHA256SUMS。 +""" +import json +import os +import sys +import urllib.request +import urllib.parse + +REPO = "JianFeeeee/webui4frpc" +API = "https://gitcode.com/api/v5/repos" + + +def get_upload_url(version: str, token: str, filename: str) -> tuple[str, dict]: + q = urllib.parse.urlencode({"file_name": filename}) + url = f"{API}/{REPO}/releases/v{version}/upload_url?{q}" + req = urllib.request.Request(url, headers={"private-token": token}) + with urllib.request.urlopen(req, timeout=20) as r: + data = json.loads(r.read()) + return data["url"], data.get("headers", {}) + + +def put_file(url: str, headers: dict, path: str) -> tuple[int, str]: + size = os.path.getsize(path) + with open(path, "rb") as f: + body = f.read() + req = urllib.request.Request(url, data=body, method="PUT") + for k, v in headers.items(): + req.add_header(k, v) + req.add_header("Content-Length", str(size)) + try: + with urllib.request.urlopen(req, timeout=300) as r: + return r.status, r.read().decode("utf-8", "replace")[:300] + except urllib.error.HTTPError as e: + return e.code, e.read().decode("utf-8", "replace")[:300] + + +def main() -> int: + if len(sys.argv) < 3: + print(__doc__) + return 2 + version, token = sys.argv[1], sys.argv[2] + outdir = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "dist", "artifacts", version, + ) + files = sys.argv[3:] or sorted( + f for f in os.listdir(outdir) + if f.endswith((".tar.gz", ".zip")) or f == "SHA256SUMS" + ) + failed = 0 + for name in files: + path = os.path.join(outdir, name) + if not os.path.isfile(path): + print(f"skip (missing): {name}") + continue + print(f"==> {name} ({os.path.getsize(path)/1048576:.1f} MiB)") + try: + url, headers = get_upload_url(version, token, name) + except Exception as e: # noqa: BLE001 + print(f" upload_url FAILED: {e}") + failed += 1 + continue + status, body = put_file(url, headers, path) + ok = 200 <= status < 300 + print(f" PUT -> {status} {'OK' if ok else body}") + if not ok: + failed += 1 + print(f"\n{'ALL OK' if failed == 0 else f'{failed} FAILED'}") + return 1 if failed else 0 + + +if __name__ == "__main__": + sys.exit(main())