fix: fall back to ChuRepo for release installation and updates
This commit is contained in:
1 parent
44766755d8
commit
30364b1c4b
7 files changed
+293
-26
No files matched your search
@@ -31,6 +31,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: installer release fallback tests
|
||||||
|
run: python3 scripts/test-release-fallback.py
|
||||||
|
|
||||||
# ---------- Rust 存储 ----------
|
# ---------- Rust 存储 ----------
|
||||||
- name: Rust toolchain
|
- name: Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
# 装的人拿到的就是编好的二进制,不需要 GHC / Rust:
|
# 装的人拿到的就是编好的二进制,不需要 GHC / Rust:
|
||||||
# curl -fsSL https://raw.githubusercontent.com/TheBadRoger/ChuSQL/main/scripts/install.sh \
|
# curl -fsSL https://raw.githubusercontent.com/TheBadRoger/ChuSQL/main/scripts/install.sh \
|
||||||
# | sh -s -- --component web
|
# | sh -s -- --component web
|
||||||
# 也可下载 ChuRepo 脚本(发行包仍默认取自 GitHub):
|
# 也可下载 ChuRepo 脚本(发行包下载失败时自动回退):
|
||||||
# curl -fsSL https://repo.chunana.cn/ChuNa7a/ChuSQL/raw/branch/main/scripts/install.sh -o install.sh
|
# curl -fsSL https://repo.chunana.cn/ChuNa7a/ChuSQL/raw/branch/main/scripts/install.sh -o install.sh
|
||||||
# sh install.sh --component web
|
# sh install.sh --component web
|
||||||
# 两站发行包与源码安装入口见 docs/install.md。
|
# 两站发行包与源码安装入口见 docs/install.md。
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ ChuSQL 是采用 Haskell SQL 引擎与 Rust 存储层的数据库项目,提供
|
|||||||
Linux / macOS:
|
Linux / macOS:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl -fsSL https://raw.githubusercontent.com/TheBadRoger/ChuSQL/main/scripts/install.sh | sh -s -- --component web
|
(curl -fsSL --connect-timeout 10 \
|
||||||
|
https://raw.githubusercontent.com/TheBadRoger/ChuSQL/main/scripts/install.sh -o install.sh ||
|
||||||
|
curl -fsSL --connect-timeout 10 \
|
||||||
|
https://repo.chunana.cn/ChuNa7a/ChuSQL/raw/branch/main/scripts/install.sh -o install.sh) &&
|
||||||
|
sh install.sh --component web
|
||||||
```
|
```
|
||||||
|
|
||||||
也可从 ChuRepo 下载后执行:
|
也可从 ChuRepo 下载后执行:
|
||||||
@@ -35,7 +39,11 @@ sh install.sh --component web
|
|||||||
Windows(PowerShell 里下下来再跑;也有下载 zip 解压的包内安装):
|
Windows(PowerShell 里下下来再跑;也有下载 zip 解压的包内安装):
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
irm https://raw.githubusercontent.com/TheBadRoger/ChuSQL/main/scripts/install.ps1 -OutFile "$env:TEMP\install.ps1"
|
try {
|
||||||
|
irm https://raw.githubusercontent.com/TheBadRoger/ChuSQL/main/scripts/install.ps1 -OutFile "$env:TEMP\install.ps1" -TimeoutSec 60 -ErrorAction Stop
|
||||||
|
} catch {
|
||||||
|
irm https://repo.chunana.cn/ChuNa7a/ChuSQL/raw/branch/main/scripts/install.ps1 -OutFile "$env:TEMP\install.ps1" -TimeoutSec 60 -ErrorAction Stop
|
||||||
|
}
|
||||||
& "$env:TEMP\install.ps1" -Component web
|
& "$env:TEMP\install.ps1" -Component web
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -46,7 +54,7 @@ irm https://repo.chunana.cn/ChuNa7a/ChuSQL/raw/branch/main/scripts/install.ps1 -
|
|||||||
& "$env:TEMP\install.ps1" -Component web
|
& "$env:TEMP\install.ps1" -Component web
|
||||||
```
|
```
|
||||||
|
|
||||||
以上在线安装脚本默认从 GitHub 获取发行包,脚本下载来源不会改变发行包来源。GitHub 不可用时,参见[包内安装](docs/install.md#包内安装)或[从源码安装](docs/install.md#从源码安装)。
|
上述命令下载脚本时优先 GitHub,失败后尝试 ChuRepo。脚本安装和更新时也会自动回退:版本查询或发行包下载失败时尝试 ChuRepo;已选定的版本保持不变,校验失败直接报错。两站均无对应发行包时需使用包内安装或源码安装。显式指定其他仓库或归档 URL 时不会回退到官方仓库。
|
||||||
|
|
||||||
### 启动
|
### 启动
|
||||||
|
|
||||||
|
|||||||
+11
-3
@@ -13,7 +13,11 @@
|
|||||||
Linux / macOS:
|
Linux / macOS:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl -fsSL https://raw.githubusercontent.com/TheBadRoger/ChuSQL/main/scripts/install.sh | sh -s -- --component web
|
(curl -fsSL --connect-timeout 10 \
|
||||||
|
https://raw.githubusercontent.com/TheBadRoger/ChuSQL/main/scripts/install.sh -o install.sh ||
|
||||||
|
curl -fsSL --connect-timeout 10 \
|
||||||
|
https://repo.chunana.cn/ChuNa7a/ChuSQL/raw/branch/main/scripts/install.sh -o install.sh) &&
|
||||||
|
sh install.sh --component web
|
||||||
```
|
```
|
||||||
|
|
||||||
也可从 ChuRepo 下载后执行:
|
也可从 ChuRepo 下载后执行:
|
||||||
@@ -28,7 +32,11 @@ sh install.sh --component web
|
|||||||
Windows PowerShell:
|
Windows PowerShell:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
irm https://raw.githubusercontent.com/TheBadRoger/ChuSQL/main/scripts/install.ps1 -OutFile "$env:TEMP\install.ps1"
|
try {
|
||||||
|
irm https://raw.githubusercontent.com/TheBadRoger/ChuSQL/main/scripts/install.ps1 -OutFile "$env:TEMP\install.ps1" -TimeoutSec 60 -ErrorAction Stop
|
||||||
|
} catch {
|
||||||
|
irm https://repo.chunana.cn/ChuNa7a/ChuSQL/raw/branch/main/scripts/install.ps1 -OutFile "$env:TEMP\install.ps1" -TimeoutSec 60 -ErrorAction Stop
|
||||||
|
}
|
||||||
& "$env:TEMP\install.ps1" -Component web
|
& "$env:TEMP\install.ps1" -Component web
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -39,7 +47,7 @@ irm https://repo.chunana.cn/ChuNa7a/ChuSQL/raw/branch/main/scripts/install.ps1 -
|
|||||||
& "$env:TEMP\install.ps1" -Component web
|
& "$env:TEMP\install.ps1" -Component web
|
||||||
```
|
```
|
||||||
|
|
||||||
以上在线安装脚本默认从 GitHub 获取发行包,脚本下载来源不会改变发行包来源。GitHub 不可用时,参见[包内安装](#包内安装)或[从源码安装](#从源码安装)。
|
上述命令下载脚本时优先 GitHub,失败后尝试 ChuRepo。脚本安装和更新时也会自动回退:版本查询或发行包下载失败时尝试 ChuRepo;已选定的版本保持不变,校验失败直接报错。两站均无对应发行包时需使用包内安装或源码安装。显式指定其他仓库或归档 URL 时不会回退到官方仓库。
|
||||||
|
|
||||||
## 安装方式
|
## 安装方式
|
||||||
|
|
||||||
|
|||||||
+29
-9
@@ -37,6 +37,8 @@ function Note([string]$msg) { Write-Host " note: $msg" }
|
|||||||
|
|
||||||
# ---- 发行站、平台标签 ----
|
# ---- 发行站、平台标签 ----
|
||||||
$githubBase = if ($env:CHUSQL_GITHUB_BASE) { $env:CHUSQL_GITHUB_BASE.TrimEnd('/') } else { 'https://github.com' }
|
$githubBase = if ($env:CHUSQL_GITHUB_BASE) { $env:CHUSQL_GITHUB_BASE.TrimEnd('/') } else { 'https://github.com' }
|
||||||
|
$churepoBase = if ($env:CHUSQL_CHUREPO_BASE) { $env:CHUSQL_CHUREPO_BASE.TrimEnd('/') } else { 'https://repo.chunana.cn' }
|
||||||
|
$churepoRepo = 'ChuNa7a/ChuSQL'
|
||||||
$apiBase = if ($githubBase -match '^https?://github\.com$') { 'https://api.github.com' } else { "$githubBase/api/v3" }
|
$apiBase = if ($githubBase -match '^https?://github\.com$') { 'https://api.github.com' } else { "$githubBase/api/v3" }
|
||||||
# 查版本走 API,未认证只有 60 次/小时;给了 token 就带上(CI 里 GITHUB_TOKEN 一般就有)
|
# 查版本走 API,未认证只有 60 次/小时;给了 token 就带上(CI 里 GITHUB_TOKEN 一般就有)
|
||||||
$githubToken = if ($env:CHUSQL_GITHUB_TOKEN) { $env:CHUSQL_GITHUB_TOKEN } elseif ($env:GITHUB_TOKEN) { $env:GITHUB_TOKEN } else { '' }
|
$githubToken = if ($env:CHUSQL_GITHUB_TOKEN) { $env:CHUSQL_GITHUB_TOKEN } elseif ($env:GITHUB_TOKEN) { $env:GITHUB_TOKEN } else { '' }
|
||||||
@@ -69,18 +71,22 @@ function Select-Tag([bool]$stableOnly, [bool]$needPackage, $releases) {
|
|||||||
|
|
||||||
# 取 URL 的文本内容(字节数组先转字符串)。
|
# 取 URL 的文本内容(字节数组先转字符串)。
|
||||||
function Get-ResponseText([string]$url) {
|
function Get-ResponseText([string]$url) {
|
||||||
$response = Invoke-WebRequest -Uri $url -Headers $apiHeaders -UseBasicParsing
|
$headers = if ($url.StartsWith("$churepoBase/")) { @{ 'User-Agent' = 'chusql-install' } } else { $apiHeaders }
|
||||||
|
$response = Invoke-WebRequest -Uri $url -Headers $headers -UseBasicParsing -TimeoutSec 300
|
||||||
if ($response.Content -is [byte[]]) { return [System.Text.Encoding]::UTF8.GetString($response.Content) }
|
if ($response.Content -is [byte[]]) { return [System.Text.Encoding]::UTF8.GetString($response.Content) }
|
||||||
return [string]$response.Content
|
return [string]$response.Content
|
||||||
}
|
}
|
||||||
|
|
||||||
# 拉取 release 列表并逐个输出。
|
# 拉取 release 列表并逐个输出。
|
||||||
function Get-Releases {
|
function Get-Releases {
|
||||||
try { $text = Get-ResponseText "$apiBase/repos/$Repo/releases?per_page=100" } catch { return }
|
$urls = @("$apiBase/repos/$Repo/releases?per_page=100")
|
||||||
try { $parsed = ConvertFrom-Json -InputObject $text } catch { return }
|
if ($Repo -eq 'TheBadRoger/ChuSQL') { $urls += "$churepoBase/api/v1/repos/$churepoRepo/releases?limit=100" }
|
||||||
if ($null -eq $parsed) { return }
|
foreach ($url in $urls) {
|
||||||
foreach ($release in @($parsed)) {
|
try {
|
||||||
if ($null -ne $release) { Write-Output $release }
|
$parsed = @(ConvertFrom-Json -InputObject (Get-ResponseText $url))
|
||||||
|
$releases = @($parsed | Where-Object { $null -ne $_ -and $_.tag_name })
|
||||||
|
if ($releases.Count -gt 0) { return $releases }
|
||||||
|
} catch { Note "release API unavailable: $url" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,13 +247,27 @@ if (-not (Test-Path (Join-Path $pack 'bin\chusql_core_storage.dll'))) {
|
|||||||
$tryUrl = if ($Version -eq 'latest') { "$githubBase/$Repo/releases/latest/download/$candidate" }
|
$tryUrl = if ($Version -eq 'latest') { "$githubBase/$Repo/releases/latest/download/$candidate" }
|
||||||
else { "$githubBase/$Repo/releases/download/$Version/$candidate" }
|
else { "$githubBase/$Repo/releases/download/$Version/$candidate" }
|
||||||
try {
|
try {
|
||||||
Invoke-WebRequest -Uri $tryUrl -Headers $apiHeaders -OutFile $zip -UseBasicParsing
|
Invoke-WebRequest -Uri $tryUrl -Headers $apiHeaders -OutFile $zip -UseBasicParsing -TimeoutSec 300
|
||||||
$asset = $candidate
|
$asset = $candidate
|
||||||
$assetUrl = $tryUrl
|
$assetUrl = $tryUrl
|
||||||
break
|
break
|
||||||
} catch {
|
} catch { Note "asset unavailable: $tryUrl" }
|
||||||
continue
|
if ($Repo -ne 'TheBadRoger/ChuSQL') { continue }
|
||||||
|
try {
|
||||||
|
$fallbackVersion = $Version
|
||||||
|
if ($fallbackVersion -eq 'latest') {
|
||||||
|
$latest = ConvertFrom-Json -InputObject (Get-ResponseText "$churepoBase/api/v1/repos/$churepoRepo/releases/latest")
|
||||||
|
$fallbackVersion = $latest.tag_name
|
||||||
|
if (-not $fallbackVersion) { throw 'ChuRepo returned no release tag' }
|
||||||
}
|
}
|
||||||
|
$tryUrl = "$churepoBase/$churepoRepo/releases/download/$fallbackVersion/$candidate"
|
||||||
|
Note "trying ChuRepo: $candidate ($fallbackVersion)"
|
||||||
|
Invoke-WebRequest -Uri $tryUrl -OutFile $zip -UseBasicParsing -TimeoutSec 300
|
||||||
|
$asset = $candidate
|
||||||
|
$assetUrl = $tryUrl
|
||||||
|
$Version = $fallbackVersion
|
||||||
|
break
|
||||||
|
} catch { Note "ChuRepo asset unavailable: $candidate" }
|
||||||
}
|
}
|
||||||
if (-not $asset) {
|
if (-not $asset) {
|
||||||
Fail "no package for windows-$arch in $Repo ($Version); try -ListVersions, or install from an archive you downloaded yourself"
|
Fail "no package for windows-$arch in $Repo ($Version); try -ListVersions, or install from an archive you downloaded yourself"
|
||||||
|
|||||||
+51
-10
@@ -17,6 +17,9 @@ esac
|
|||||||
|
|
||||||
# 查版本走 API,未认证只有 60 次/小时;给了 token 就带上(CI 里 GITHUB_TOKEN 一般就有)
|
# 查版本走 API,未认证只有 60 次/小时;给了 token 就带上(CI 里 GITHUB_TOKEN 一般就有)
|
||||||
github_token="${CHUSQL_GITHUB_TOKEN:-${GITHUB_TOKEN:-}}"
|
github_token="${CHUSQL_GITHUB_TOKEN:-${GITHUB_TOKEN:-}}"
|
||||||
|
churepo_base="${CHUSQL_CHUREPO_BASE:-https://repo.chunana.cn}"
|
||||||
|
churepo_base="${churepo_base%/}"
|
||||||
|
churepo_repo='ChuNa7a/ChuSQL'
|
||||||
|
|
||||||
# 打印用法、选项与环境变量说明。
|
# 打印用法、选项与环境变量说明。
|
||||||
usage() {
|
usage() {
|
||||||
@@ -44,7 +47,7 @@ usage: ./install.sh [--component web|cli|both] [options]
|
|||||||
--no-service skip Linux systemd registration and boot autostart
|
--no-service skip Linux systemd registration and boot autostart
|
||||||
|
|
||||||
where the files come from:
|
where the files come from:
|
||||||
(default) a package next to this script, else the newest GitHub release
|
(default) a local package, else GitHub releases with ChuRepo fallback
|
||||||
--url URL|PATH install from this archive (URL, file:// URL or local path)
|
--url URL|PATH install from this archive (URL, file:// URL or local path)
|
||||||
--repo OWNER/NAME GitHub repository (default: TheBadRoger/ChuSQL)
|
--repo OWNER/NAME GitHub repository (default: TheBadRoger/ChuSQL)
|
||||||
--version TAG release tag to download / git ref to build
|
--version TAG release tag to download / git ref to build
|
||||||
@@ -59,6 +62,8 @@ environment:
|
|||||||
--password
|
--password
|
||||||
CHUSQL_GITHUB_BASE GitHub base URL: GitHub Enterprise, a mirror, or a
|
CHUSQL_GITHUB_BASE GitHub base URL: GitHub Enterprise, a mirror, or a
|
||||||
local test fixture (default: https://github.com)
|
local test fixture (default: https://github.com)
|
||||||
|
CHUSQL_CHUREPO_BASE fallback base URL (default: https://repo.chunana.cn)
|
||||||
|
fallback only applies to the default repository
|
||||||
CHUSQL_GITHUB_TOKEN token for the version API (or GITHUB_TOKEN); without
|
CHUSQL_GITHUB_TOKEN token for the version API (or GITHUB_TOKEN); without
|
||||||
one the API allows 60 requests per hour
|
one the API allows 60 requests per hour
|
||||||
EOF
|
EOF
|
||||||
@@ -276,15 +281,15 @@ new_tmp() {
|
|||||||
download() {
|
download() {
|
||||||
if command -v curl >/dev/null 2>&1; then
|
if command -v curl >/dev/null 2>&1; then
|
||||||
if [ -n "${3:-}" ]; then
|
if [ -n "${3:-}" ]; then
|
||||||
curl -fsSL -H "$3" -H 'Accept: application/vnd.github+json' -o "$2" "$1" || return 1
|
curl -fsSL --connect-timeout 10 --max-time 300 -H "$3" -H 'Accept: application/vnd.github+json' -o "$2" "$1" || return 1
|
||||||
else
|
else
|
||||||
curl -fsSL -o "$2" "$1" || return 1
|
curl -fsSL --connect-timeout 10 --max-time 300 -o "$2" "$1" || return 1
|
||||||
fi
|
fi
|
||||||
elif command -v wget >/dev/null 2>&1; then
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
if [ -n "${3:-}" ]; then
|
if [ -n "${3:-}" ]; then
|
||||||
wget -q --header="$3" --header='Accept: application/vnd.github+json' -O "$2" "$1" || return 1
|
wget -q --timeout=30 --tries=1 --header="$3" --header='Accept: application/vnd.github+json' -O "$2" "$1" || return 1
|
||||||
else
|
else
|
||||||
wget -q -O "$2" "$1" || return 1
|
wget -q --timeout=30 --tries=1 -O "$2" "$1" || return 1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
fail 'need curl or wget to download the package'
|
fail 'need curl or wget to download the package'
|
||||||
@@ -342,18 +347,41 @@ detect_platform() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# ---- 发行 tag:问 GitHub API(不依赖 jq,用 awk 读 JSON)----
|
# ---- 发行 tag:问 GitHub API(不依赖 jq,用 awk 读 JSON)----
|
||||||
# 请求 GitHub API 并落盘。
|
# 请求发行列表,默认仓库失败或无版本时回退。
|
||||||
api_get() {
|
api_get() {
|
||||||
if [ -n "$github_token" ]; then
|
if [ -n "$github_token" ]; then
|
||||||
download "$api_base$1" "$2" "Authorization: Bearer $github_token"
|
download "$api_base$1" "$2" "Authorization: Bearer $github_token" &&
|
||||||
|
grep -q '"tag_name"' "$2" && return 0
|
||||||
else
|
else
|
||||||
download "$api_base$1" "$2"
|
download "$api_base$1" "$2" && grep -q '"tag_name"' "$2" && return 0
|
||||||
fi
|
fi
|
||||||
|
[ "$repo" = "$default_repo" ] || return 1
|
||||||
|
note 'release API unavailable or empty; trying ChuRepo' >&2
|
||||||
|
download "$churepo_base/api/v1/repos/$churepo_repo/releases?limit=100" "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 将紧凑 JSON 按字符串外的分隔符换行。
|
||||||
|
json_lines() {
|
||||||
|
awk '
|
||||||
|
{
|
||||||
|
for (i = 1; i <= length($0); i++) {
|
||||||
|
c = substr($0, i, 1)
|
||||||
|
printf "%s", c
|
||||||
|
if (quoted) {
|
||||||
|
if (escaped) escaped = 0
|
||||||
|
else if (c == "\\") escaped = 1
|
||||||
|
else if (c == "\"") quoted = 0
|
||||||
|
} else if (c == "\"") quoted = 1
|
||||||
|
else if (c ~ /[][{},]/) printf "\n"
|
||||||
|
}
|
||||||
|
printf "\n"
|
||||||
|
}
|
||||||
|
' "$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 把 release JSON 解析成 tag 与资源行。
|
# 把 release JSON 解析成 tag 与资源行。
|
||||||
parse_releases() {
|
parse_releases() {
|
||||||
awk '
|
json_lines "$1" | awk '
|
||||||
in_assets {
|
in_assets {
|
||||||
if ($0 ~ /^[[:space:]]*\]/) { in_assets = 0; next }
|
if ($0 ~ /^[[:space:]]*\]/) { in_assets = 0; next }
|
||||||
if ($0 ~ /"name":/) {
|
if ($0 ~ /"name":/) {
|
||||||
@@ -372,7 +400,7 @@ parse_releases() {
|
|||||||
}
|
}
|
||||||
/"prerelease":/ { pre = ($0 ~ /true/) ? 1 : 0; next }
|
/"prerelease":/ { pre = ($0 ~ /true/) ? 1 : 0; next }
|
||||||
END { if (tag != "") { print tag "\t" pre "\t" assets } }
|
END { if (tag != "") { print tag "\t" pre "\t" assets } }
|
||||||
' "$1"
|
'
|
||||||
}
|
}
|
||||||
|
|
||||||
# 这份 release 里有没有本平台的包。
|
# 这份 release 里有没有本平台的包。
|
||||||
@@ -468,6 +496,19 @@ fetch_asset() {
|
|||||||
note "asset $asset"
|
note "asset $asset"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
[ "$repo" = "$default_repo" ] || return 1
|
||||||
|
fallback_version="$version"
|
||||||
|
if [ "$fallback_version" = 'latest' ]; then
|
||||||
|
download "$churepo_base/api/v1/repos/$churepo_repo/releases/latest" "$tmp/latest.json" 2>/dev/null || return 1
|
||||||
|
fallback_version=$(parse_releases "$tmp/latest.json" | cut -f1)
|
||||||
|
[ -n "$fallback_version" ] || return 1
|
||||||
|
fi
|
||||||
|
url="$churepo_base/$churepo_repo/releases/download/$fallback_version/$asset"
|
||||||
|
note "trying ChuRepo: $asset ($fallback_version)"
|
||||||
|
if download "$url" "$archive" 2>/dev/null; then
|
||||||
|
version="$fallback_version"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import hashlib
|
||||||
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
# 用本机 HTTP 夹具验证发行源回退、版本固定与令牌隔离。
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseFallback(unittest.TestCase):
|
||||||
|
# 启动只响应测试路由的发行站。
|
||||||
|
def setUp(self):
|
||||||
|
self.routes = {}
|
||||||
|
self.requests = []
|
||||||
|
fixture = self
|
||||||
|
|
||||||
|
class Handler(BaseHTTPRequestHandler):
|
||||||
|
# 按夹具返回响应并记录认证头。
|
||||||
|
def do_GET(self):
|
||||||
|
fixture.requests.append((self.path, self.headers.get("Authorization")))
|
||||||
|
status, body = fixture.routes.get(self.path, (404, b"missing"))
|
||||||
|
self.send_response(status)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
# 隐藏 HTTP 访问日志。
|
||||||
|
def log_message(self, *args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
||||||
|
self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)
|
||||||
|
self.thread.start()
|
||||||
|
self.base = f"http://127.0.0.1:{self.server.server_port}"
|
||||||
|
self.temp = tempfile.TemporaryDirectory()
|
||||||
|
self.root = Path(self.temp.name)
|
||||||
|
source = (Path(__file__).parent / "install.sh").read_text()
|
||||||
|
self.functions = "\n".join(re.findall(r"^[a-z_]+\(\) \{[^\n]*\}$|^[a-z_]+\(\) \{\n.*?^\}", source, re.M | re.S))
|
||||||
|
self.setup = f"""set -eu
|
||||||
|
{self.functions}
|
||||||
|
default_repo=TheBadRoger/ChuSQL
|
||||||
|
repo=$default_repo
|
||||||
|
github_base={self.base}/github
|
||||||
|
api_base=$github_base/api/v3
|
||||||
|
churepo_base={self.base}/churepo
|
||||||
|
churepo_repo=ChuNa7a/ChuSQL
|
||||||
|
github_token=secret-test-token
|
||||||
|
tmp={self.root}
|
||||||
|
version=v-test
|
||||||
|
labels=linux-x86_64
|
||||||
|
os=linux
|
||||||
|
arch=x86_64
|
||||||
|
ext=tar.gz
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 停止夹具并清理临时文件。
|
||||||
|
def tearDown(self):
|
||||||
|
self.server.shutdown()
|
||||||
|
self.server.server_close()
|
||||||
|
self.thread.join()
|
||||||
|
self.temp.cleanup()
|
||||||
|
|
||||||
|
# 执行真实安装脚本中的发行函数。
|
||||||
|
def shell(self, command):
|
||||||
|
return subprocess.run(["sh", "-c", self.setup + command], capture_output=True, text=True, timeout=15)
|
||||||
|
|
||||||
|
# 验证紧凑 API、附件回退、同站校验与认证隔离。
|
||||||
|
def test_api_and_asset_fallback(self):
|
||||||
|
releases = [{"tag_name": "v-test", "prerelease": False,
|
||||||
|
"body": 'text with [brackets], \"quotes\"',
|
||||||
|
"assets": [{"name": "chusql-linux-x86_64.tar.gz"}]}]
|
||||||
|
api = "/churepo/api/v1/repos/ChuNa7a/ChuSQL/releases?limit=100"
|
||||||
|
self.routes[api] = (200, json.dumps(releases).encode())
|
||||||
|
asset = "/churepo/ChuNa7a/ChuSQL/releases/download/v-test/chusql-linux-x86_64.tar.gz"
|
||||||
|
self.routes[asset] = (200, b"release-package")
|
||||||
|
digest = hashlib.sha256(b"release-package").hexdigest()
|
||||||
|
self.routes[asset + ".sha256"] = (200, digest.encode())
|
||||||
|
result = self.shell('version=latest; resolve_version; [ "$version" = v-test ]; fetch_asset chusql-linux-x86_64.tar.gz; verify_checksum "$url" "$archive"')
|
||||||
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||||
|
self.assertIn("(ok)", result.stdout)
|
||||||
|
self.assertEqual((self.root / "chusql-linux-x86_64.tar.gz").read_bytes(), b"release-package")
|
||||||
|
self.assertTrue(any(auth for path, auth in self.requests if path.startswith("/github/")))
|
||||||
|
self.assertTrue(all(auth is None for path, auth in self.requests if path.startswith("/churepo/")))
|
||||||
|
|
||||||
|
# 列表不可用时从备用站解析 latest 的具体标签。
|
||||||
|
def test_latest_redirect_fallback(self):
|
||||||
|
self.routes["/churepo/api/v1/repos/ChuNa7a/ChuSQL/releases/latest"] = (
|
||||||
|
200, json.dumps({"tag_name": "v-latest", "assets": []}).encode())
|
||||||
|
asset = "/churepo/ChuNa7a/ChuSQL/releases/download/v-latest/package.tar.gz"
|
||||||
|
self.routes[asset] = (200, b"latest-package")
|
||||||
|
result = self.shell('version=latest; fetch_asset package.tar.gz; [ "$version" = v-latest ]')
|
||||||
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||||
|
|
||||||
|
# 两站缺少指定版本时不得静默换版。
|
||||||
|
def test_missing_pinned_version_fails(self):
|
||||||
|
result = self.shell('fetch_asset chusql-linux-x86_64.tar.gz')
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertTrue(all("latest" not in path for path, _ in self.requests))
|
||||||
|
|
||||||
|
# 主站可用时无需访问备用站。
|
||||||
|
def test_primary_success(self):
|
||||||
|
path = "/github/TheBadRoger/ChuSQL/releases/download/v-test/package.tar.gz"
|
||||||
|
self.routes[path] = (200, b"primary")
|
||||||
|
result = self.shell('fetch_asset package.tar.gz')
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertEqual(self.requests, [(path, None)])
|
||||||
|
|
||||||
|
# 显式仓库不回退至官方仓库。
|
||||||
|
def test_custom_repo_does_not_fallback(self):
|
||||||
|
result = self.shell('repo=custom/project; fetch_asset package.tar.gz')
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertTrue(all(path.startswith("/github/") for path, _ in self.requests))
|
||||||
|
|
||||||
|
# 校验失败须中止,不能当作网络失败。
|
||||||
|
def test_corrupt_checksum_fails(self):
|
||||||
|
path = "/churepo/ChuNa7a/ChuSQL/releases/download/v-test/package.tar.gz"
|
||||||
|
self.routes[path] = (200, b"corrupt")
|
||||||
|
self.routes[path + ".sha256"] = (200, b"0" * 64)
|
||||||
|
result = self.shell('fetch_asset package.tar.gz; verify_checksum "$url" "$archive"')
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertIn("checksum mismatch", result.stderr)
|
||||||
|
|
||||||
|
# 执行 PowerShell 的真实版本查询和下载校验代码。
|
||||||
|
def powershell(self, command, download=False):
|
||||||
|
if not shutil.which("pwsh"):
|
||||||
|
self.skipTest("pwsh is required for PowerShell fallback tests")
|
||||||
|
source = (Path(__file__).parent / "install.ps1").resolve()
|
||||||
|
script = f"""$ErrorActionPreference = 'Stop'
|
||||||
|
$tokens = $null; $errors = $null
|
||||||
|
$ast = [System.Management.Automation.Language.Parser]::ParseFile('{source}', [ref]$tokens, [ref]$errors)
|
||||||
|
if ($errors.Count) {{ throw ($errors | Out-String) }}
|
||||||
|
$ast.FindAll({{ param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] }}, $true) | ForEach-Object {{ Invoke-Expression $_.Extent.Text }}
|
||||||
|
$Repo = 'TheBadRoger/ChuSQL'
|
||||||
|
$githubBase = '{self.base}/github'
|
||||||
|
$apiBase = \"$githubBase/api/v3\"
|
||||||
|
$churepoBase = '{self.base}/churepo'
|
||||||
|
$churepoRepo = 'ChuNa7a/ChuSQL'
|
||||||
|
$apiHeaders = @{{ Authorization = 'Bearer secret-test-token' }}
|
||||||
|
$Version = 'v-test'
|
||||||
|
$arch = 'x86_64'
|
||||||
|
$labels = @('windows-x86_64')
|
||||||
|
$tempDir = '{self.root}'
|
||||||
|
"""
|
||||||
|
if download:
|
||||||
|
text = source.read_text()
|
||||||
|
script += text[text.index(' # 一个平台一个包:'):text.index(" Step 'Unpacking the package'")]
|
||||||
|
script += "\n" + command
|
||||||
|
self.root.mkdir(exist_ok=True)
|
||||||
|
path = self.root / "test.ps1"
|
||||||
|
path.write_text(script)
|
||||||
|
return subprocess.run(["pwsh", "-NoProfile", "-File", str(path)], capture_output=True, text=True, timeout=20)
|
||||||
|
|
||||||
|
# Windows 下载回退并校验,备用站不接收主站令牌。
|
||||||
|
def test_powershell_fallback(self):
|
||||||
|
api = "/churepo/api/v1/repos/ChuNa7a/ChuSQL/releases?limit=100"
|
||||||
|
self.routes[api] = (200, json.dumps([{"tag_name": "v-test", "assets": []}]).encode())
|
||||||
|
asset = "/churepo/ChuNa7a/ChuSQL/releases/download/v-test/chusql-windows-x86_64.zip"
|
||||||
|
self.routes[asset] = (200, b"windows-package")
|
||||||
|
digest = hashlib.sha256(b"windows-package").hexdigest()
|
||||||
|
self.routes[asset + ".sha256"] = (200, digest.encode())
|
||||||
|
result = self.powershell('if (@(Get-Releases)[0].tag_name -ne "v-test") { throw "wrong release" }')
|
||||||
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||||
|
result = self.powershell('if ($Version -ne "v-test") { throw "version changed" }', download=True)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||||
|
self.assertIn("(ok)", result.stdout)
|
||||||
|
self.assertTrue(all(auth is None for path, auth in self.requests if path.startswith("/churepo/")))
|
||||||
|
|
||||||
|
# Windows 双站缺包与校验错误均须失败。
|
||||||
|
def test_powershell_errors(self):
|
||||||
|
result = self.powershell('', download=True)
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertTrue(all("latest" not in path for path, _ in self.requests))
|
||||||
|
asset = "/churepo/ChuNa7a/ChuSQL/releases/download/v-test/chusql-windows-x86_64.zip"
|
||||||
|
self.routes[asset] = (200, b"corrupt")
|
||||||
|
self.routes[asset + ".sha256"] = (200, b"0" * 64)
|
||||||
|
result = self.powershell('', download=True)
|
||||||
|
self.assertNotEqual(result.returncode, 0)
|
||||||
|
self.assertIn("checksum mismatch", result.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in new issue
Block a user