feat(updater): add GitCode fallback for GUI and CLIProxyAPI core releases
- split GitCode repositories into GUI and core build variables - query GitCode latest release when GitHub version discovery fails - construct GitCode attachment URLs from repository, tag, and asset filename - retry core downloads through GitCode when GitHub downloads fail
This commit is contained in:
1 parent
dd86eabefa
commit
e8b2ced3f8
7 files changed
+200
-51
No files matched your search
@@ -166,7 +166,8 @@ jobs:
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
- name: Build GUI
|
||||
env:
|
||||
GITCODE_REPOSITORY: ${{ vars.GITCODE_REPOSITORY }}
|
||||
GITCODE_GUI_REPOSITORY: ${{ vars.GITCODE_GUI_REPOSITORY }}
|
||||
GITCODE_CORE_REPOSITORY: ${{ vars.GITCODE_CORE_REPOSITORY }}
|
||||
RUSTFLAGS: -D warnings
|
||||
run: |
|
||||
bun install --frozen-lockfile
|
||||
@@ -418,7 +419,7 @@ jobs:
|
||||
--directory artifacts
|
||||
--output artifacts/portable-update-windows.json
|
||||
--repository "${{ github.repository }}"
|
||||
--gitcode-repository "${{ vars.GITCODE_REPOSITORY }}"
|
||||
--gitcode-repository "${{ vars.GITCODE_GUI_REPOSITORY }}"
|
||||
--tag "$RELEASE_TAG"
|
||||
- name: Publish release
|
||||
shell: bash
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
param(
|
||||
[int]$BuildJobs,
|
||||
|
||||
[string]$GitCodeRepository = 'lzt404/EasyCLIProxyAPI'
|
||||
[string]$GitCodeGuiRepository = 'lzt404/EasyCLIProxyAPI',
|
||||
|
||||
[string]$GitCodeCoreRepository = 'lzt404/CLIProxyAPI'
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
@@ -27,8 +29,11 @@ if (-not $PSBoundParameters.ContainsKey('BuildJobs')) {
|
||||
if ($BuildJobs -lt 1 -or $BuildJobs -gt 256) {
|
||||
throw 'BuildJobs must be between 1 and 256.'
|
||||
}
|
||||
if ($GitCodeRepository -notmatch '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$') {
|
||||
throw 'GitCodeRepository must use the owner/repository format.'
|
||||
if ($GitCodeGuiRepository -notmatch '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$') {
|
||||
throw 'GitCodeGuiRepository must use the owner/repository format.'
|
||||
}
|
||||
if ($GitCodeCoreRepository -notmatch '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$') {
|
||||
throw 'GitCodeCoreRepository must use the owner/repository format.'
|
||||
}
|
||||
|
||||
if (-not (Get-Command bun -ErrorAction SilentlyContinue)) {
|
||||
@@ -36,7 +41,8 @@ if (-not (Get-Command bun -ErrorAction SilentlyContinue)) {
|
||||
}
|
||||
|
||||
Write-Host "Cargo build jobs: $BuildJobs"
|
||||
Write-Host "GitCode fallback repository: $GitCodeRepository"
|
||||
Write-Host "GitCode GUI fallback repository: $GitCodeGuiRepository"
|
||||
Write-Host "GitCode core fallback repository: $GitCodeCoreRepository"
|
||||
|
||||
& bun install
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
@@ -44,10 +50,12 @@ if ($LASTEXITCODE -ne 0) {
|
||||
}
|
||||
|
||||
$PreviousBuildJobs = $env:CARGO_BUILD_JOBS
|
||||
$PreviousGitCodeRepository = $env:GITCODE_REPOSITORY
|
||||
$PreviousGitCodeGuiRepository = $env:GITCODE_GUI_REPOSITORY
|
||||
$PreviousGitCodeCoreRepository = $env:GITCODE_CORE_REPOSITORY
|
||||
try {
|
||||
$env:CARGO_BUILD_JOBS = [string]$BuildJobs
|
||||
$env:GITCODE_REPOSITORY = $GitCodeRepository
|
||||
$env:GITCODE_GUI_REPOSITORY = $GitCodeGuiRepository
|
||||
$env:GITCODE_CORE_REPOSITORY = $GitCodeCoreRepository
|
||||
& bun tauri build --no-bundle
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Tauri build failed with exit code $LASTEXITCODE."
|
||||
@@ -60,11 +68,17 @@ finally {
|
||||
else {
|
||||
$env:CARGO_BUILD_JOBS = $PreviousBuildJobs
|
||||
}
|
||||
if ($null -eq $PreviousGitCodeRepository) {
|
||||
Remove-Item Env:GITCODE_REPOSITORY -ErrorAction SilentlyContinue
|
||||
if ($null -eq $PreviousGitCodeGuiRepository) {
|
||||
Remove-Item Env:GITCODE_GUI_REPOSITORY -ErrorAction SilentlyContinue
|
||||
}
|
||||
else {
|
||||
$env:GITCODE_REPOSITORY = $PreviousGitCodeRepository
|
||||
$env:GITCODE_GUI_REPOSITORY = $PreviousGitCodeGuiRepository
|
||||
}
|
||||
if ($null -eq $PreviousGitCodeCoreRepository) {
|
||||
Remove-Item Env:GITCODE_CORE_REPOSITORY -ErrorAction SilentlyContinue
|
||||
}
|
||||
else {
|
||||
$env:GITCODE_CORE_REPOSITORY = $PreviousGitCodeCoreRepository
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
BUILD_JOBS="${CARGO_BUILD_JOBS:-20}"
|
||||
GITCODE_REPOSITORY="${GITCODE_REPOSITORY:-lzt404/EasyCLIProxyAPI}"
|
||||
GITCODE_GUI_REPOSITORY="${GITCODE_GUI_REPOSITORY:-lzt404/EasyCLIProxyAPI}"
|
||||
GITCODE_CORE_REPOSITORY="${GITCODE_CORE_REPOSITORY:-lzt404/CLIProxyAPI}"
|
||||
APP_BIN="$ROOT_DIR/src-tauri/target/release/cpa-gui"
|
||||
BIN_DIR="$ROOT_DIR/bin-work"
|
||||
BIN_OUT="$BIN_DIR/EasyCLIProxyAPI"
|
||||
@@ -16,9 +17,10 @@ if ! command -v bun >/dev/null 2>&1; then
|
||||
fi
|
||||
|
||||
echo "Cargo build jobs: $BUILD_JOBS"
|
||||
echo "GitCode fallback repository: $GITCODE_REPOSITORY"
|
||||
echo "GitCode GUI fallback repository: $GITCODE_GUI_REPOSITORY"
|
||||
echo "GitCode core fallback repository: $GITCODE_CORE_REPOSITORY"
|
||||
bun install
|
||||
CARGO_BUILD_JOBS="$BUILD_JOBS" GITCODE_REPOSITORY="$GITCODE_REPOSITORY" bun tauri build --no-bundle
|
||||
CARGO_BUILD_JOBS="$BUILD_JOBS" GITCODE_GUI_REPOSITORY="$GITCODE_GUI_REPOSITORY" GITCODE_CORE_REPOSITORY="$GITCODE_CORE_REPOSITORY" bun tauri build --no-bundle
|
||||
|
||||
if [ ! -x "$APP_BIN" ]; then
|
||||
echo "Build finished, but executable not found: $APP_BIN"
|
||||
|
||||
Generated
+1
-1
@@ -582,7 +582,7 @@ checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
|
||||
|
||||
[[package]]
|
||||
name = "cpa-gui"
|
||||
version = "0.2.14"
|
||||
version = "0.2.16"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "cpa-gui"
|
||||
# Single source of truth for the desktop app version.
|
||||
version = "0.2.14"
|
||||
version = "0.2.16"
|
||||
description = "CPA desktop management shell"
|
||||
authors = ["lzt404"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-env-changed=GITCODE_GUI_REPOSITORY");
|
||||
println!("cargo:rerun-if-env-changed=GITCODE_CORE_REPOSITORY");
|
||||
tauri_build::build()
|
||||
}
|
||||
+165
-35
@@ -1563,6 +1563,8 @@ struct GithubRelease {
|
||||
struct GithubAsset {
|
||||
name: String,
|
||||
browser_download_url: String,
|
||||
#[serde(default)]
|
||||
fallback_download_urls: Vec<String>,
|
||||
size: Option<u64>,
|
||||
digest: Option<String>,
|
||||
}
|
||||
@@ -10784,7 +10786,7 @@ async fn fetch_portable_update_manifest(
|
||||
match fetch_portable_update_manifest_url(client, APP_UPDATE_MANIFEST_URL).await {
|
||||
Ok(manifest) => Ok(manifest),
|
||||
Err(github_error) => {
|
||||
let Some(repository) = configured_gitcode_repository() else {
|
||||
let Some(repository) = configured_gitcode_gui_repository() else {
|
||||
return Err(github_error);
|
||||
};
|
||||
let fallback = async {
|
||||
@@ -10840,8 +10842,16 @@ async fn fetch_portable_update_manifest_url(
|
||||
Ok(manifest)
|
||||
}
|
||||
|
||||
fn configured_gitcode_repository() -> Option<&'static str> {
|
||||
let repository = option_env!("GITCODE_REPOSITORY")?.trim();
|
||||
fn configured_gitcode_gui_repository() -> Option<&'static str> {
|
||||
configured_gitcode_repository(option_env!("GITCODE_GUI_REPOSITORY"))
|
||||
}
|
||||
|
||||
fn configured_gitcode_core_repository() -> Option<&'static str> {
|
||||
configured_gitcode_repository(option_env!("GITCODE_CORE_REPOSITORY"))
|
||||
}
|
||||
|
||||
fn configured_gitcode_repository(repository: Option<&'static str>) -> Option<&'static str> {
|
||||
let repository = repository?.trim();
|
||||
let mut parts = repository.split('/');
|
||||
let valid_part = |part: &str| {
|
||||
!part.is_empty()
|
||||
@@ -11048,7 +11058,7 @@ fn validate_portable_update_asset_fallbacks(
|
||||
asset,
|
||||
tag,
|
||||
expected_filenames,
|
||||
configured_gitcode_repository(),
|
||||
configured_gitcode_gui_repository(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11807,17 +11817,7 @@ async fn install_core_version_inner(
|
||||
.ok_or_else(|| format!("非法 asset 文件名: {}", asset.name))?;
|
||||
let archive_path = download_dir.join(archive_file_name);
|
||||
|
||||
let downloaded = download_asset(
|
||||
&client,
|
||||
&asset.browser_download_url,
|
||||
&archive_path,
|
||||
asset.size,
|
||||
asset.digest.as_deref(),
|
||||
window,
|
||||
state,
|
||||
&token,
|
||||
)
|
||||
.await?;
|
||||
let downloaded = download_asset(&client, asset, &archive_path, window, state, &token).await?;
|
||||
validate_downloaded_asset(asset, &downloaded)?;
|
||||
|
||||
ensure_not_cancelled(&token, Some(&archive_path))?;
|
||||
@@ -11933,14 +11933,50 @@ async fn fetch_release(
|
||||
return Ok(release_from_tag(version));
|
||||
}
|
||||
let atom_result = fetch_release_from_atom(client).await;
|
||||
match atom_result {
|
||||
let github_result = match atom_result {
|
||||
Ok(release) => Ok(release),
|
||||
Err(atom_error) => fetch_release_from_page(client).await.map_err(|page_error| {
|
||||
format!("GitHub 发布源请求失败: {atom_error};release 页面请求失败: {page_error}")
|
||||
}),
|
||||
};
|
||||
match github_result {
|
||||
Ok(release) => Ok(release),
|
||||
Err(github_error) => {
|
||||
let Some(repository) = configured_gitcode_core_repository() else {
|
||||
return Err(github_error);
|
||||
};
|
||||
fetch_release_from_gitcode(client, repository)
|
||||
.await
|
||||
.map_err(|gitcode_error| {
|
||||
format!(
|
||||
"GitHub 内核发布源失败: {github_error};GitCode 回退源失败: {gitcode_error}"
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_release_from_gitcode(
|
||||
client: &reqwest::Client,
|
||||
repository: &str,
|
||||
) -> Result<GithubRelease, String> {
|
||||
let release_url = format!("https://api.gitcode.com/api/v5/repos/{repository}/releases/latest");
|
||||
let release = client
|
||||
.get(release_url)
|
||||
.header(reqwest::header::ACCEPT, "application/json")
|
||||
.header(reqwest::header::USER_AGENT, USER_AGENT)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| format!("查询 GitCode 最新内核发行版失败: {error}"))?
|
||||
.error_for_status()
|
||||
.map_err(|error| format!("读取 GitCode 最新内核发行版失败: {error}"))?
|
||||
.json::<GitcodeRelease>()
|
||||
.await
|
||||
.map_err(|error| format!("解析 GitCode 最新内核发行版失败: {error}"))?;
|
||||
validate_release_tag(&release.tag_name)?;
|
||||
Ok(release_from_gitcode_tag(&release.tag_name, repository))
|
||||
}
|
||||
|
||||
async fn fetch_release_from_page(client: &reqwest::Client) -> Result<GithubRelease, String> {
|
||||
let response = client
|
||||
.get(RELEASE_PAGE_URL)
|
||||
@@ -12010,6 +12046,18 @@ fn release_tag_from_atom(xml: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
fn release_from_tag(tag: &str) -> GithubRelease {
|
||||
release_from_tag_for_repositories(tag, configured_gitcode_core_repository(), false)
|
||||
}
|
||||
|
||||
fn release_from_gitcode_tag(tag: &str, repository: &str) -> GithubRelease {
|
||||
release_from_tag_for_repositories(tag, Some(repository), true)
|
||||
}
|
||||
|
||||
fn release_from_tag_for_repositories(
|
||||
tag: &str,
|
||||
gitcode_repository: Option<&str>,
|
||||
prefer_gitcode: bool,
|
||||
) -> GithubRelease {
|
||||
let tag = normalize_version(tag);
|
||||
let version = tag.trim_start_matches('v');
|
||||
let assets = [
|
||||
@@ -12023,8 +12071,20 @@ fn release_from_tag(tag: &str) -> GithubRelease {
|
||||
.into_iter()
|
||||
.map(|(os, arch, extension)| {
|
||||
let name = format!("CLIProxyAPI_{version}_{os}_{arch}.{extension}");
|
||||
let github_url = format!("{RELEASE_DOWNLOAD_PREFIX}{tag}/{name}");
|
||||
let gitcode_url = gitcode_repository
|
||||
.map(|repository| gitcode_release_attachment_url(repository, &tag, &name));
|
||||
let (browser_download_url, fallback_download_urls) = if prefer_gitcode {
|
||||
(
|
||||
gitcode_url.unwrap_or_else(|| github_url.clone()),
|
||||
Vec::new(),
|
||||
)
|
||||
} else {
|
||||
(github_url, gitcode_url.into_iter().collect())
|
||||
};
|
||||
GithubAsset {
|
||||
browser_download_url: format!("{RELEASE_DOWNLOAD_PREFIX}{tag}/{name}"),
|
||||
browser_download_url,
|
||||
fallback_download_urls,
|
||||
name,
|
||||
size: None,
|
||||
digest: None,
|
||||
@@ -12099,6 +12159,7 @@ fn parse_release_assets(html: &str) -> Vec<GithubAsset> {
|
||||
assets.push(GithubAsset {
|
||||
name: name.to_string(),
|
||||
browser_download_url,
|
||||
fallback_download_urls: Vec::new(),
|
||||
size: None,
|
||||
digest,
|
||||
});
|
||||
@@ -12136,6 +12197,7 @@ async fn fetch_release_cancelable(
|
||||
|
||||
fn http_client() -> Result<reqwest::Client, String> {
|
||||
reqwest::Client::builder()
|
||||
.redirect(release_https_redirect_policy())
|
||||
.connect_timeout(Duration::from_secs(15))
|
||||
.read_timeout(Duration::from_secs(30))
|
||||
.timeout(Duration::from_secs(600))
|
||||
@@ -12176,30 +12238,53 @@ fn core_release_asset_name(version: &str, platform: &CorePlatform) -> String {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn download_asset(
|
||||
client: &reqwest::Client,
|
||||
url: &str,
|
||||
asset: &GithubAsset,
|
||||
archive_path: &Path,
|
||||
expected_total: Option<u64>,
|
||||
expected_digest: Option<&str>,
|
||||
window: &tauri::Window,
|
||||
state: &CoreDownloadState,
|
||||
token: &CancellationToken,
|
||||
) -> Result<DownloadedArchive, String> {
|
||||
let result = download_asset_inner(
|
||||
client,
|
||||
url,
|
||||
archive_path,
|
||||
expected_total,
|
||||
expected_digest,
|
||||
window,
|
||||
state,
|
||||
token,
|
||||
)
|
||||
.await;
|
||||
if result.is_err() {
|
||||
let _ = fs::remove_file(archive_path);
|
||||
let urls =
|
||||
std::iter::once(&asset.browser_download_url).chain(asset.fallback_download_urls.iter());
|
||||
let mut failures = Vec::new();
|
||||
for (index, url) in urls.enumerate() {
|
||||
if index > 0 {
|
||||
state.progress(
|
||||
window,
|
||||
"GitHub 下载失败,正在切换到 GitCode",
|
||||
0,
|
||||
asset.size,
|
||||
true,
|
||||
);
|
||||
}
|
||||
let result = download_asset_inner(
|
||||
client,
|
||||
url,
|
||||
archive_path,
|
||||
asset.size,
|
||||
asset.digest.as_deref(),
|
||||
window,
|
||||
state,
|
||||
token,
|
||||
)
|
||||
.await;
|
||||
match result {
|
||||
Ok(downloaded) => return Ok(downloaded),
|
||||
Err(error) if token.is_cancelled() => {
|
||||
let _ = fs::remove_file(archive_path);
|
||||
return Err(error);
|
||||
}
|
||||
Err(error) => {
|
||||
let _ = fs::remove_file(archive_path);
|
||||
failures.push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
if failures.is_empty() {
|
||||
let _ = fs::remove_file(archive_path);
|
||||
return Err("内核发行版没有可用的下载地址".to_string());
|
||||
}
|
||||
Err(format!("所有内核下载源均失败: {}", failures.join(";")))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -24027,6 +24112,51 @@ custom_option = "keep-original"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn synthetic_core_release_uses_gitcode_as_download_fallback() {
|
||||
let release = release_from_tag_for_repositories(
|
||||
"7.2.80",
|
||||
Some("lzt404/CLIProxyAPI"),
|
||||
false,
|
||||
);
|
||||
let platform = CorePlatform {
|
||||
os: "windows".to_string(),
|
||||
arch: "x86_64".to_string(),
|
||||
asset_os: "windows".to_string(),
|
||||
asset_arch: "amd64".to_string(),
|
||||
archive_kind: "zip".to_string(),
|
||||
};
|
||||
let asset = select_release_asset(&release, &platform).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
asset.browser_download_url,
|
||||
"https://github.com/router-for-me/CLIProxyAPI/releases/download/v7.2.80/CLIProxyAPI_7.2.80_windows_amd64.zip"
|
||||
);
|
||||
assert_eq!(
|
||||
asset.fallback_download_urls,
|
||||
["https://api.gitcode.com/api/v5/repos/lzt404/CLIProxyAPI/releases/v7.2.80/attach_files/CLIProxyAPI_7.2.80_windows_amd64.zip/download"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gitcode_discovered_core_release_downloads_from_gitcode_first() {
|
||||
let release = release_from_gitcode_tag("v7.2.80", "lzt404/CLIProxyAPI");
|
||||
let platform = CorePlatform {
|
||||
os: "linux".to_string(),
|
||||
arch: "aarch64".to_string(),
|
||||
asset_os: "linux".to_string(),
|
||||
asset_arch: "aarch64".to_string(),
|
||||
archive_kind: "tar.gz".to_string(),
|
||||
};
|
||||
let asset = select_release_asset(&release, &platform).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
asset.browser_download_url,
|
||||
"https://api.gitcode.com/api/v5/repos/lzt404/CLIProxyAPI/releases/v7.2.80/attach_files/CLIProxyAPI_7.2.80_linux_aarch64.tar.gz/download"
|
||||
);
|
||||
assert!(asset.fallback_download_urls.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_asset_names_cover_the_six_supported_gui_targets() {
|
||||
let targets = [
|
||||
|
||||
Reference in new issue
Block a user