From 1d40b34f1c6869cf304a78eddabf7dafa627ab28 Mon Sep 17 00:00:00 2001 From: mofeng-git Date: Tue, 13 Oct 2026 22:05:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E4=BB=8E=20GitHub=20?= =?UTF-8?q?Release=20=E7=94=9F=E6=88=90=E5=9C=A8=E7=BA=BF=E5=8D=87?= =?UTF-8?q?=E7=BA=A7=E7=AB=99=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/build-update-site.sh | 176 +++++++++++++++++++++++++++++++++-- 1 file changed, 167 insertions(+), 9 deletions(-) diff --git a/scripts/build-update-site.sh b/scripts/build-update-site.sh index bb52a201..868dc035 100755 --- a/scripts/build-update-site.sh +++ b/scripts/build-update-site.sh @@ -5,6 +5,7 @@ # /v1/channels.json # /v1/releases.json # /v1/bin//one-kvm- +# 默认从 GitHub Release 下载 deb,提取二进制;也支持本地二进制输入。 # set -euo pipefail @@ -17,6 +18,11 @@ RELEASE_CHANNEL="stable" STABLE_VERSION="" BETA_VERSION="" PUBLISHED_AT="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" +PUBLISHED_AT_SET=0 +GITHUB_REPO="mofeng-git/One-KVM" +GITHUB_RELEASE="" +GITHUB_PROXY="${GITHUB_PROXY:-}" +GITHUB_API_PROXY="${GITHUB_API_PROXY:-}" ARTIFACTS_DIR="" X86_64_BIN="" AARCH64_BIN="" @@ -32,10 +38,17 @@ declare -a NOTES=() usage() { cat <<'EOF' Usage: - ./scripts/build-update-site.sh --version [options] + ./scripts/build-update-site.sh [options] -Required: - --version Release 版本号(如 0.1.10) +GitHub input (无本地输入时默认使用 latest Release): + --release GitHub Release 标签或完整页面 URL + --github-repo 默认 mofeng-git/One-KVM + --github-proxy deb 下载代理前缀,如 https://ghproxy.com + 代理失败后自动尝试 GitHub 直连 + --github-api-proxy 可选 GitHub API 代理前缀 + 也可设置 GITHUB_PROXY / GITHUB_API_PROXY + --version GitHub 输入默认从 deb 文件名读取;本地输入必填 + GitHub 输入指定时校验版本一致 Artifact input (二选一,可混用): --artifacts-dir 自动扫描目录中的标准文件名: @@ -50,8 +63,8 @@ Manifest options: --release-channel releases.json 里该版本所属渠道,默认 stable --stable channels.json 的 stable 指针,默认等于 --version --beta channels.json 的 beta 指针,默认等于 --version - --published-at 发布时间,默认当前 UTC 时间 - --note 发布说明,可重复传入多次 + --published-at 发布时间,默认 GitHub 发布时间(本地输入为当前时间) + --note 发布说明,可重复;指定后覆盖 GitHub 说明 Output options: --site-name 打包根目录名,默认 one-kvm-update @@ -62,6 +75,12 @@ Other: -h, --help 显示帮助 Example: + ./scripts/build-update-site.sh --release v261001 + + ./scripts/build-update-site.sh \ + --release https://github.com/mofeng-git/One-KVM/releases/tag/v261001 \ + --github-proxy https://ghproxy.com + ./scripts/build-update-site.sh \ --version 0.1.10 \ --artifacts-dir ./target/release \ @@ -104,7 +123,26 @@ is_valid_channel() { } while [[ $# -gt 0 ]]; do + if [[ "$1" != "-h" && "$1" != "--help" ]]; then + [[ $# -ge 2 && -n "$2" && "$2" != --* ]] || fail "Missing value for $1" + fi case "$1" in + --release | --github-tag) + GITHUB_RELEASE="$2" + shift 2 + ;; + --github-repo) + GITHUB_REPO="$2" + shift 2 + ;; + --github-proxy) + GITHUB_PROXY="$2" + shift 2 + ;; + --github-api-proxy) + GITHUB_API_PROXY="$2" + shift 2 + ;; --version) VERSION="${2:-}" shift 2 @@ -123,6 +161,7 @@ while [[ $# -gt 0 ]]; do ;; --published-at) PUBLISHED_AT="${2:-}" + PUBLISHED_AT_SET=1 shift 2 ;; --note) @@ -175,6 +214,128 @@ require_cmd stat require_cmd tar require_cmd mktemp +[[ "$SITE_NAME" =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]] || fail "Invalid --site-name: ${SITE_NAME}" +BUILD_DIR="$(mktemp -d)" +trap 'rm -rf "$BUILD_DIR"' EXIT + +download_github() { + local url="$1" dest="$2" proxy="$3" + local -a candidates=() + [[ -z "$proxy" ]] || candidates+=("${proxy%/}/${url}") + candidates+=("$url") + local candidate + for candidate in "${candidates[@]}"; do + echo "Downloading: $candidate" >&2 + if curl --fail --location --show-error --connect-timeout 15 --max-time 300 \ + --retry 2 --retry-delay 2 --output "${dest}.part" "$candidate"; then + # 一些代理返回 HTTP 200 的 HTML 页面,不能当作 deb 或 API 响应。 + if [[ "$dest" == *.deb ]]; then + dpkg-deb --info "${dest}.part" >/dev/null 2>&1 || continue + else + python3 -c 'import json,sys; r=json.load(open(sys.argv[1])); assert isinstance(r,dict) and isinstance(r.get("assets"),list)' \ + "${dest}.part" >/dev/null 2>&1 || continue + fi + mv "${dest}.part" "$dest" + return 0 + fi + echo "Download failed or invalid response; trying next source." >&2 + done + fail "Unable to download $url" +} + +if [[ -n "$GITHUB_RELEASE" || ( -z "$ARTIFACTS_DIR" && "$X86_64_SET" -eq 0 && "$AARCH64_SET" -eq 0 && "$ARMV7_SET" -eq 0 ) ]]; then + [[ -z "$ARTIFACTS_DIR" && "$X86_64_SET" -eq 0 && "$AARCH64_SET" -eq 0 && "$ARMV7_SET" -eq 0 ]] || \ + fail "--release cannot be combined with local artifact input" + require_cmd curl + require_cmd python3 + require_cmd dpkg-deb + GITHUB_RELEASE="${GITHUB_RELEASE:-latest}" + if [[ "$GITHUB_RELEASE" =~ ^https://github\.com/([^/]+/[^/]+)/releases/tag/([^/?#]+)/?$ ]]; then + GITHUB_REPO="${BASH_REMATCH[1]}" + GITHUB_RELEASE="${BASH_REMATCH[2]}" + fi + [[ "$GITHUB_REPO" =~ ^[a-zA-Z0-9_.-]+/[a-zA-Z0-9_.-]+$ ]] || fail "Invalid --github-repo" + for proxy in "$GITHUB_PROXY" "$GITHUB_API_PROXY"; do + [[ -z "$proxy" || "$proxy" =~ ^https?://[^[:space:]]+$ ]] || fail "Invalid GitHub proxy URL: $proxy" + done + release_path="latest" + if [[ "$GITHUB_RELEASE" != latest ]]; then + release_tag="$(python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(urllib.parse.unquote(sys.argv[1]), safe=""))' "$GITHUB_RELEASE")" + release_path="tags/${release_tag}" + fi + release_json="${BUILD_DIR}/release.json" + download_github "https://api.github.com/repos/${GITHUB_REPO}/releases/${release_path}" "$release_json" "$GITHUB_API_PROXY" + python3 - "$release_json" "${BUILD_DIR}/assets.tsv" "${BUILD_DIR}/metadata.json" "$VERSION" <<'PY' +import json +import re +import sys + +release = json.load(open(sys.argv[1], encoding="utf-8")) +assets = [] +seen = set() +for asset in release["assets"]: + match = re.fullmatch(r"one-kvm_(\d+\.\d+\.\d+)_(amd64|arm64|armhf)\.deb", asset["name"]) + if not match: + continue + version, arch = match.groups() + if arch in seen: + sys.exit(f"Error: Duplicate deb architecture: {arch}") + seen.add(arch) + url = asset["browser_download_url"] + if not url.startswith("https://github.com/") or any(c in url for c in "\t\r\n"): + sys.exit("Error: Invalid GitHub asset URL") + digest = asset.get("digest") or "-" + if digest != "-" and not re.fullmatch(r"sha256:[a-fA-F0-9]{64}", digest): + sys.exit("Error: Invalid GitHub asset digest") + assets.append((arch, version, url, digest, str(asset["size"]))) +if not assets: + sys.exit("Error: Release has no supported One-KVM deb assets") +versions = {a[1] for a in assets} +if len(versions) != 1: + sys.exit("Error: Release deb versions differ") +version = versions.pop() +if sys.argv[4] and sys.argv[4] != version: + sys.exit(f"Error: --version {sys.argv[4]} differs from GitHub deb version {version}") +with open(sys.argv[2], "w", encoding="utf-8") as output: + for asset in assets: + output.write("\t".join(asset) + "\n") +with open(sys.argv[3], "w", encoding="utf-8") as output: + json.dump(dict(version=version, published_at=release["published_at"], + notes=release.get("body") or ""), output, ensure_ascii=False) +PY + VERSION="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["version"])' "${BUILD_DIR}/metadata.json")" + if [[ "$PUBLISHED_AT_SET" -eq 0 ]]; then + PUBLISHED_AT="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["published_at"])' "${BUILD_DIR}/metadata.json")" + fi + if [[ ${#NOTES[@]} -eq 0 ]]; then + mapfile -t NOTES < <(python3 -c 'import json,re,sys; body=json.load(open(sys.argv[1]))["notes"]; print("\n".join(re.sub(r"^[-*+]\s+", "", line.strip()) for line in body.splitlines() if line.strip()), end="")' "${BUILD_DIR}/metadata.json") + fi + ARTIFACTS_DIR="${BUILD_DIR}/binaries" + mkdir -p "$ARTIFACTS_DIR" + while IFS=$'\t' read -r arch deb_version url digest expected_size; do + case "$arch" in + amd64) triple="x86_64-unknown-linux-gnu" ;; + arm64) triple="aarch64-unknown-linux-gnu" ;; + armhf) triple="armv7-unknown-linux-gnueabihf" ;; + esac + deb="${BUILD_DIR}/one-kvm_${deb_version}_${arch}.deb" + download_github "$url" "$deb" "$GITHUB_PROXY" + [[ "$(stat -c%s "$deb")" == "$expected_size" ]] || fail "deb size mismatch: $arch" + if [[ "$digest" != - ]]; then + actual_sha="$(sha256sum "$deb" | awk '{print $1}')" + [[ "$actual_sha" == "${digest#sha256:}" ]] || fail "deb SHA256 mismatch: $arch" + fi + [[ "$(dpkg-deb -f "$deb" Package)" == one-kvm ]] || fail "Unexpected deb package: $arch" + [[ "$(dpkg-deb -f "$deb" Version)" == "$VERSION" ]] || fail "deb version mismatch: $arch" + [[ "$(dpkg-deb -f "$deb" Architecture)" == "$arch" ]] || fail "deb architecture mismatch: $arch" + extract_dir="${BUILD_DIR}/extract-${arch}" + dpkg-deb -x "$deb" "$extract_dir" + binary="${extract_dir}/usr/bin/one-kvm" + [[ -f "$binary" && ! -L "$binary" && -s "$binary" ]] || fail "deb missing /usr/bin/one-kvm: $arch" + cp "$binary" "${ARTIFACTS_DIR}/one-kvm-${triple}" + done < "${BUILD_DIR}/assets.tsv" +fi + [[ -n "$VERSION" ]] || fail "--version is required" is_valid_version "$VERSION" || fail "Invalid --version: ${VERSION} (expected x.x.x)" is_valid_channel "$RELEASE_CHANNEL" || fail "Invalid --release-channel: ${RELEASE_CHANNEL}" @@ -220,9 +381,6 @@ if [[ ${#SRC_BY_TRIPLE[@]} -eq 0 ]]; then fail "No artifact found. Provide --artifacts-dir or at least one of --x86_64/--aarch64/--armv7." fi -BUILD_DIR="$(mktemp -d)" -trap 'rm -rf "$BUILD_DIR"' EXIT - SITE_DIR="${BUILD_DIR}/${SITE_NAME}" V1_DIR="${SITE_DIR}/v1" BIN_DIR="${V1_DIR}/bin/${VERSION}" @@ -268,7 +426,7 @@ RELEASES_FILE="${V1_DIR}/releases.json" echo ' {' echo " \"version\": \"${VERSION}\"," echo " \"channel\": \"${RELEASE_CHANNEL}\"," - echo " \"published_at\": \"${PUBLISHED_AT}\"," + echo " \"published_at\": \"$(json_escape "$PUBLISHED_AT")\"," if [[ ${#NOTES[@]} -eq 0 ]]; then echo ' "notes": [],'