diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1da99e5..f20a4e7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,13 +6,6 @@ # mqttx-v--mqttx[.exe].7z 压缩包(单条目,内部文件名 = 裸二进制名) # mqttx-v--mqttx[.exe].7z.sha256 # 7z 是 OTA 主通道;裸二进制供不识别 7z 的旧客户端(≤v1.0.3)升级。 -# -# 可选:镜像发布到自建 Gitea(已装机的 v1.0.x 客户端 OTA 源仍在 Gitea)。 -# 在仓库 Settings → Secrets and variables → Actions 配置后自动启用,缺省跳过: -# GITEA_URL 实例地址,如 https://gitea.heavenlybook.cn -# GITEA_TOKEN Gitea 令牌(package + repo 写权限) -# GITEA_OWNER Gitea 侧 owner(如 JoeAllen) -# 客户端 OTA 更新源切换到 GitHub 后可删除 mirror-gitea job。 name: Release on: @@ -187,44 +180,3 @@ jobs: gh release create "$VER" -R "$GITHUB_REPOSITORY" --title "$VER" --generate-notes fi gh release upload "$VER" -R "$GITHUB_REPOSITORY" artifacts/* --clobber - -# ─── 可选:镜像发布到自建 Gitea ─────────────────────────────── -# 客户端 OTA 源已切换到 GitHub Releases,但已装机的 v1.0.x 老客户端仍轮询 -# Gitea Release 附件;配置 GITEA_URL / GITEA_TOKEN / GITEA_OWNER 三个 Secrets 后 -# 本 job 生效,把这些老客户端喂到能升级到 OTA 切换后的版本为止。未配置时自动 -# 跳过(不影响 GitHub Release 发布)。老客户端升级完毕后可删除本 job。 - mirror-gitea: - name: Mirror to Gitea (optional) - needs: [meta, build] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/download-artifact@v4 - with: - path: artifacts - merge-multiple: true - - - name: Install 7-Zip - run: | - sudo apt-get update - sudo apt-get install -y p7zip-full - - # 只传裸二进制:脚本自行生成 .sha256 侧车与 7z 压缩包(条目名约定在脚本内保证) - - name: Mirror release assets to Gitea - env: - GITEA_URL: ${{ secrets.GITEA_URL }} - GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} - GITEA_OWNER: JoeAllen - GITEA_REPO: mqttx - GITEA_PKG: mqttx - VER: ${{ needs.meta.outputs.version }} - if: ${{ env.GITEA_URL != '' && env.GITEA_TOKEN != '' }} - run: | - set -euo pipefail - chmod +x scripts/publish-gitea.sh - files=() - for f in artifacts/mqttx-${VER}-*; do - case "$f" in *.sha256) ;; *) files+=("$f") ;; esac - done - ./scripts/publish-gitea.sh "${files[@]}" "$VER" diff --git a/README.md b/README.md index 4fab9a3..2a58fd3 100644 --- a/README.md +++ b/README.md @@ -136,43 +136,8 @@ MQTTX_LIVE=1 cargo test -p mqttx-desktop --test engine_live -- --ignored > **应用内 OTA 只认 Release 附件**,附件名必须匹配 `mqttx-v--mqttx[.exe]`。 > 7z 是 OTA 主通道;裸二进制供不识别 7z 的旧客户端(≤v1.0.3)升级。 - -### 可选:镜像到自建 Gitea(老客户端 OTA 源) - -已装机的 v1.0.x 客户端 OTA 更新源仍指向 Gitea(master 上的客户端自 v1.0.1 -起已切换到 GitHub Releases)。在仓库 -**Settings → Secrets and variables → Actions** 配置以下 Secrets 后,每次发布 -会自动把产物同步镜像到 Gitea Release(`mirror-gitea` job,调用 -`scripts/publish-gitea.sh`);未配置时该 job 自动跳过: - -- `GITEA_URL` — 实例地址,如 `https://gitea.heavenlybook.cn` -- `GITEA_TOKEN` — Gitea 访问令牌(需 `package` + repo 写权限) -- `GITEA_OWNER` — Gitea 侧 owner(如 `JoeAllen`) - -待 v1.0.x 老客户端全部升级完毕,可删除 `mirror-gitea` job 与 -`scripts/publish-gitea.*`。本地手动镜像(应急)仍可用: - -```powershell -# Windows(PowerShell) -$env:GITEA_URL = "https://gitea.example.com" -$env:GITEA_TOKEN = "gta_xxx" -.\scripts\publish-gitea.ps1 # 默认产物 + 自动推断版本/owner -.\scripts\publish-gitea.ps1 -Version v1.0.1 # 指定版本 -``` - -```bash -# macOS / Linux(bash) -export GITEA_URL=https://gitea.example.com -export GITEA_TOKEN=gta_xxx -./scripts/publish-gitea.sh # 默认产物 -./scripts/publish-gitea.sh target/release/mqttx v1.0.1 -``` - -- **owner** 自动从 `git remote origin` 推断(可用 `GITEA_OWNER` / `-Owner` 覆盖) -- **版本** 依次取参数 → 当前提交的精确 tag(`git describe --tags --exact-match`)→ `Cargo.toml`; - 必须是合法 semver(形如 `v1.0.1` 或 `1.0.1-rc.1`),否则脚本直接拒绝发布 - —— 非 semver 版本(如 `v1.0.0-4-gabc1234`)会被客户端截断比对,更新永远发不出去 -- 每个文件附带 `.sha256` 校验侧车;同名版本重复上传按 409 跳过(Gitea 不允许覆盖) +> +> GitHub 侧发布历史自 v1.0.0 起独立开始,不向其他渠道镜像(Gitea 为历史渠道,已弃用)。 ## 数据目录 diff --git a/gpui-app/src/update/mod.rs b/gpui-app/src/update/mod.rs index d3864b3..155eb15 100644 --- a/gpui-app/src/update/mod.rs +++ b/gpui-app/src/update/mod.rs @@ -23,8 +23,8 @@ pub use install::{ /// - api.github.com 未认证配额为 60 次/小时/IP:共享出口 IP 打满时检查会失败, /// 按「错误仅记录、不打扰用户」静默降级,下次成功检查自动追上; /// 附件下载走 github.com CDN,不受 API 配额限制。 -/// - v1.0.x 老客户端的 OTA 源仍是自建 Gitea,CI 的 mirror-gitea job 会把每次 -/// 发布镜像回 Gitea,老客户端升级完毕后可移除该 job。 +/// - Gitea(gitea.heavenlybook.cn)是 v1.0.0 之前的历史发布渠道,已弃用: +/// GitHub 侧发布历史自 v1.0.0 起独立开始,不再向 Gitea 镜像。 pub const RELEASES_API: &str = "https://api.github.com/repos/LiJoeAllen/mqttx/releases"; /// Releases 页面(github.com,非 API、无配额限制):api.github.com 被限流时 /// 兜底发现最新 tag —— 跟踪 `{RELEASES_PAGE}/latest` 的 302,从最终 URL 取 tag, diff --git a/scripts/publish-gitea.ps1 b/scripts/publish-gitea.ps1 deleted file mode 100644 index d33d2ef..0000000 --- a/scripts/publish-gitea.ps1 +++ /dev/null @@ -1,319 +0,0 @@ -<# -.SYNOPSIS - 发布构建产物到 Gitea:Generic Package + Release 附件(OTA 更新源)。 - -.DESCRIPTION - 1) Generic Package API 上传: - PUT {GITEA_URL}/api/packages/{owner}/generic/{pkg}/{version}/{filename} - 2) Release 附件上传(应用内 OTA 只认 Release 附件): - POST {GITEA_URL}/api/v1/repos/{owner}/{repo}/releases/{id}/assets?name=... - 命名约定 mqttx-v--mqttx[.exe],含 .sha256 侧车。 - 认证:Authorization: token - 同名包版本重复上传会得到 409,脚本按"已存在"处理。 - -.EXAMPLE - $env:GITEA_URL = "https://gitea.example.com" - $env:GITEA_TOKEN = "xxxxxxxx" - .\scripts\publish-gitea.ps1 # 上传默认产物,版本自动推断 - .\scripts\publish-gitea.ps1 -Version v1.0.1 # 指定版本 - .\scripts\publish-gitea.ps1 -Files a.exe, b.zip # 指定文件 - -.PARAMETER Files - 待上传文件列表。缺省时自动探测 target/release/mqttx(.exe)。 -.PARAMETER Version - 软件包版本。缺省时依次尝试当前提交的精确 tag(git describe --tags --exact-match)、 - gpui-app/Cargo.toml。必须是合法 semver,否则脚本拒绝发布。 -.PARAMETER Owner - 覆盖 GITEA_OWNER(缺省时从 git remote origin 推断)。 -.PARAMETER Repo - 覆盖 GITEA_REPO(缺省时从 git remote origin 推断,失败为 mqttx)。 -.PARAMETER PackageName - 覆盖 GITEA_PKG(默认 mqttx)。 -#> -[CmdletBinding()] -param( - [string[]]$Files = @(), - [string]$Version = "", - [string]$Owner = "", - [string]$Repo = "", - [string]$PackageName = "" -) - -$ErrorActionPreference = "Stop" -# Windows PowerShell 5.1 默认可能协商 TLS1.0,现代 Gitea 实例要求 TLS1.2+ -try { - [Net.ServicePointManager]::SecurityProtocol = ` - [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 -} catch { } - -function Die($msg) { Write-Error "错误: $msg"; exit 1 } - -$Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) -$Pkg = if ($PackageName) { $PackageName } elseif ($env:GITEA_PKG) { $env:GITEA_PKG } else { "mqttx" } - -# ── 必填配置 ──────────────────────────────────────────────────────────────── -# 地址:优先环境变量,否则从 git remote origin 推断 -$GiteaUrl = $env:GITEA_URL -if (-not $GiteaUrl) { - $remote = git -C $Root remote get-url origin 2>$null - if ($remote) { - if ($remote -match "://") { $hostPart = (($remote -split "://", 2)[-1] -split "/", 2)[0] } - else { $hostPart = ($remote -split ":", 2)[0] } - $hostPart = ($hostPart -split "@")[-1] - if ($hostPart) { $GiteaUrl = "https://$hostPart" } - } -} -if (-not $GiteaUrl) { Die "缺少环境变量 GITEA_URL(如 https://gitea.example.com),且无法从 git remote 推断" } -$GiteaUrl = $GiteaUrl.TrimEnd("/") - -# 令牌:优先环境变量,否则取 Git Credential Manager 中同一主机的凭证 -# (即推送用的那条 OAuth 凭证,无需另行创建 API token) -$Token = $env:GITEA_TOKEN -if (-not $Token) { - # 从 Git Credential Manager 取同一主机的凭证(与 git 推送同源)。 - # 注意:git credential fill 对 CRLF 管道输入敏感(PS 管道送出 \r\n - # 会被拒收),必须写 LF 临时文件、由 cmd 做重定向,stderr 也留在 cmd - # 内部(PS 5.1 在 EAP=Stop 下对原生命令的 2>$null 会抛异常)。 - $credHost = $GiteaUrl -replace "^https?://", "" - $req = Join-Path $env:TEMP ("mqttx-cred-" + [guid]::NewGuid().ToString("N") + ".txt") - [IO.File]::WriteAllText($req, "protocol=https`nhost=$credHost`n`n") - try { - $raw = cmd /c "git credential fill < ""$req"" 2>nul" - $Token = ($raw | Where-Object { $_ -like "password=*" }) -replace "^password=", "" - } - catch { $Token = $null } - finally { Remove-Item $req -ErrorAction SilentlyContinue } -} -if (-not $Token) { Die "缺少环境变量 GITEA_TOKEN,且无法从 git credential store 自动获取" } - -# ── owner/repo:参数 > 环境变量 > git remote origin 推断 ───────────────────── -if (-not $Owner) { $Owner = $env:GITEA_OWNER } -if (-not $Repo) { $Repo = $env:GITEA_REPO } -if (-not $Owner -or -not $Repo) { - try { $remote = git -C $Root remote get-url origin 2>$null } catch { $remote = $null } - if ($remote) { - # 兼容 git@host:owner/repo.git 与 https://host/owner/repo.git - if ($remote -match "://") { - # URL 形式:取 host 之后的路径段 - $path = ($remote -split "/", 4)[-1] - } - else { - # SCP 形式 git@host:owner/repo - $path = ($remote -split ":", 2)[-1] - } - $path = $path -replace "\.git$", "" - $seg = @($path -split "/" | Where-Object { $_ }) - if ($seg.Count -ge 2) { - if (-not $Owner) { $Owner = $seg[0] } - if (-not $Repo) { $Repo = $seg[1] } - } - } -} -if (-not $Owner) { Die "无法推断 owner,请用 -Owner 或 GITEA_OWNER 指定" } -if (-not $Repo) { $Repo = $Pkg } - -# ── 版本:参数 > 当前提交的精确 tag > Cargo.toml ──────────────────────────── -# 严禁用 'git describe --tags --always' 兜底:它会产出 v1.0.0-4-gabc1234 这类非 -# semver 描述串,据此创建的 Release 会被客户端 release_part() 截成 1.0.0 并判定 -# "不比当前版本新" —— 更新永远发不出去,而且该 Release 会占据 /releases/latest, -# 把真正的更新挡在后面。 -$CargoToml = Join-Path $Root "gpui-app\Cargo.toml" -$CargoVer = "" -$m = Select-String -Path $CargoToml -Pattern '^version = "(.+)"' | Select-Object -First 1 -if ($m) { $CargoVer = $m.Matches[0].Groups[1].Value } -if (-not $Version) { - $Version = (git -C $Root describe --tags --exact-match 2>$null) - if ($Version) { Write-Host "→ 版本取自当前提交的 tag: $Version" } -} -if (-not $Version) { - $Version = $CargoVer - if ($Version) { Write-Host "→ HEAD 无 tag,版本回退为 gpui-app/Cargo.toml: $Version" -ForegroundColor Yellow } -} -if (-not $Version) { Die "无法确定版本号:请用 -Version 指定(如 v1.0.1)或先给当前提交打 tag" } -$Version = $Version -replace "/", "-" # Gitea 版本段不允许 "/" -if ($Version -notmatch '^[vV]?\d+\.\d+\.\d+(-[0-9A-Za-z.]+)?$') { - Die "版本号 '$Version' 不是合法 semver(应形如 v1.0.1 或 1.0.1-rc.1)" -} -if ($CargoVer -and (($Version -replace '^[vV]', '') -ne $CargoVer)) { - Write-Host " ! 警告:版本 '$Version' 与 gpui-app/Cargo.toml ($CargoVer) 不一致" -ForegroundColor Yellow -} - -# ── 默认产物 ──────────────────────────────────────────────────────────────── -if (-not $Files -or $Files.Count -eq 0) { - $exe = Join-Path $Root "target\release\mqttx.exe" - $bin = Join-Path $Root "target\release\mqttx" - if (Test-Path $exe) { $Files = @($exe) } - elseif (Test-Path $bin) { $Files = @($bin) } - else { Die "未找到产物,请先 cargo build --release,或用 -Files 指定" } -} - -Write-Host "→ Gitea: $GiteaUrl" -Write-Host "→ 软件包: $Owner/$Pkg@$Version" -Write-Host "→ 仓库: $Owner/$Repo" -Write-Host "→ 产物: $($Files -join ', ')" - -foreach ($f in $Files) { - if (-not (Test-Path $f)) { Die "文件不存在: $f" } - $name = Split-Path -Leaf $f - $size = "{0:N1} MB" -f ((Get-Item $f).Length / 1MB) - Write-Host "→ 上传 $name ($size)" - - $url = "$GiteaUrl/api/packages/$Owner/generic/$Pkg/$Version/$name" - # Windows PowerShell 5.1 无 -InFile 的 token 认证上传也走 Invoke-WebRequest - try { - Invoke-RestMethod -Method Put ` - -Headers @{ Authorization = "token $Token" } ` - -InFile $f ` - -ContentType "application/octet-stream" ` - -Uri $url | Out-Null - Write-Host " ✓ 已发布 $name" -ForegroundColor Green - } - catch { - $status = $null - if ($_.Exception.Response) { $status = [int]$_.Exception.Response.StatusCode } - if ($status -eq 409) { - Write-Host " ! 已存在同名版本(409),跳过:$name" -ForegroundColor Yellow - continue - } - Die "上传失败: $($_.Exception.Message)" - } - - # 附带 sha256 校验文件 - $sum = (Get-FileHash -Algorithm SHA256 $f).Hash.ToLower() - $sidecar = "$f.sha256" - "$sum $name" | Set-Content -NoNewline -Encoding ascii $sidecar - try { - Invoke-RestMethod -Method Put ` - -Headers @{ Authorization = "token $Token" } ` - -InFile $sidecar ` - -ContentType "text/plain" ` - -Uri "$url.sha256" | Out-Null - Write-Host " ✓ 已发布 $name.sha256" -ForegroundColor Green - } - catch { - Write-Host " ! sha256 文件上传失败" -ForegroundColor Yellow - } -} - -Write-Host "完成。软件包页面: $GiteaUrl/$Owner?tab=packages" -ForegroundColor Cyan - -# ── Release 附件:OTA 更新源(应用内 update.rs 只查 /releases)────────────── -# 规范化版本号:tag 推断出的 "v1.0.2" 与 Cargo 的 "1.0.2" 统一为无 v 数字段 -$VerNum = $Version -replace "^v", "" -$Triple = "x86_64-pc-windows-msvc" -$Api = "$GiteaUrl/api/v1/repos/$Owner/$Repo" - -# 取(或创建)与版本同名 tag 的 Release -$rel = $null -try { - $rel = Invoke-RestMethod -Headers @{ Authorization = "token $Token" } -Uri "$Api/releases/tags/v$VerNum" -} catch { } -if (-not $rel) { - try { - $rel = Invoke-RestMethod -Method Post ` - -Headers @{ Authorization = "token $Token" } ` - -ContentType "application/json" ` - -Body (@{ tag_name = "v$VerNum"; name = "v$VerNum"; draft = $false; prerelease = $false } | ConvertTo-Json) ` - -Uri "$Api/releases" - Write-Host "→ 已创建 Release v$VerNum" - } - catch { - # OTA 只认 Release 附件:这里失败等于本次发布无效,必须让调用方看到失败 - Die "Release 创建/查询失败:$($_.Exception.Message)" - } -} -$releaseId = $rel.id -Write-Host "→ Release v$VerNum (id=$releaseId),上传附件…" - -function Upload-ReleaseAsset($uri, $filePath) { - # Windows PowerShell 5.1 没有 -Form,用 .NET HttpClient 组 multipart(字段名必须为 attachment) - Add-Type -AssemblyName System.Net.Http - $client = New-Object System.Net.Http.HttpClient - $client.DefaultRequestHeaders.Authorization = - New-Object System.Net.Http.Headers.AuthenticationHeaderValue("token", $Token) - $content = New-Object System.Net.Http.MultipartFormDataContent - $fs = [System.IO.File]::OpenRead($filePath) - try { - $fileContent = New-Object System.Net.Http.StreamContent($fs) - $fileContent.Headers.ContentType = - [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse("application/octet-stream") - $content.Add($fileContent, "attachment", (Split-Path -Leaf $filePath)) - $resp = $client.PostAsync($uri, $content).GetAwaiter().GetResult() - return [int]$resp.StatusCode - } - finally { - $fs.Dispose() - $client.Dispose() - } -} - -foreach ($f in $Files) { - $name = Split-Path -Leaf $f - # 已符合 OTA 命名约定的原样使用;否则按约定规范重命名后上传 - if ($name -match "^mqttx-v.*-mqttx(\.exe)?$") { $asset = $name } - elseif ($name.EndsWith(".exe")) { $asset = "mqttx-v$VerNum-$Triple-mqttx.exe" } - else { $asset = "mqttx-v$VerNum-$Triple-mqttx" } - - # ── 7z 压缩附件(OTA 主通道:体积小、下载快)── - # 内部文件名 = $asset(应用解压后按此名暂存安装) - $SevenZip = @( - "$env:ProgramFiles\7-Zip\7z.exe", - "${env:ProgramFiles(x86)}\7-Zip\7z.exe", - "D:\Program Files\7-Zip\7z.exe", - "C:\Program Files (x86)\7-Zip\7z.exe" - ) | Where-Object { Test-Path $_ } | Select-Object -First 1 - if (-not $SevenZip) { - $SevenZip = (Get-Command 7z -ErrorAction SilentlyContinue).Source - } - if (-not $SevenZip) { - # 官方安装器写入注册表(Path 指向安装目录) - try { - $reg = Get-ItemProperty "HKLM:\SOFTWARE\7-Zip" -ErrorAction Stop - if ($reg.Path) { $cand = Join-Path $reg.Path "7z.exe"; if (Test-Path $cand) { $SevenZip = $cand } } - } catch { } - } - if ($SevenZip) { - $packDir = Join-Path $env:TEMP ("mqttx-pack-" + [guid]::NewGuid().ToString("N")) - New-Item -ItemType Directory -Path $packDir | Out-Null - Copy-Item $f (Join-Path $packDir $asset) - $archive = Join-Path $env:TEMP "$asset.7z" - & $SevenZip a -y -mx=9 $archive (Join-Path $packDir $asset) | Out-Null - if ($LASTEXITCODE -eq 0 -and (Test-Path $archive)) { - $asum = (Get-FileHash -Algorithm SHA256 $archive).Hash.ToLower() - "$asum $asset.7z" | Set-Content -NoNewline -Encoding ascii "$archive.sha256" - foreach ($a in @("$asset.7z", "$asset.7z.sha256")) { - $code = Upload-ReleaseAsset "$Api/releases/$releaseId/assets?name=$a" (Join-Path $env:TEMP $a) - if ($code -eq 201) { - Write-Host " ✓ Release 附件 $a" -ForegroundColor Green - } - else { - Write-Host " ! Release 附件 $a 上传失败 (HTTP $code)" -ForegroundColor Yellow - } - } - } - else { - Write-Host " ! 7z 打包失败(exit=$LASTEXITCODE),跳过压缩附件" -ForegroundColor Yellow - } - Remove-Item $packDir, $archive, "$archive.sha256" -Recurse -Force -ErrorAction SilentlyContinue - } - else { - Write-Host " ! 未找到 7-Zip,跳过 7z 附件(仅上传裸二进制)" -ForegroundColor Yellow - } - - # ── 裸二进制附件:v1.0.3 及更早客户端不认识 7z,需要它升级 ── - $tmp = Join-Path $env:TEMP $asset - Copy-Item $f $tmp -Force - $sum = (Get-FileHash -Algorithm SHA256 $f).Hash.ToLower() - "$sum $asset" | Set-Content -NoNewline -Encoding ascii "$tmp.sha256" - foreach ($a in @($asset, "$asset.sha256")) { - $code = Upload-ReleaseAsset "$Api/releases/$releaseId/assets?name=$a" (Join-Path $env:TEMP $a) - if ($code -eq 201) { - Write-Host " ✓ Release 附件 $a" -ForegroundColor Green - } - else { - Write-Host " ! Release 附件 $a 上传失败 (HTTP $code)" -ForegroundColor Yellow - } - } - Remove-Item $tmp, "$tmp.sha256" -ErrorAction SilentlyContinue -} -Write-Host "完成。Release 页面: $GiteaUrl/$Owner/$Repo/releases/tag/v$VerNum" -ForegroundColor Cyan diff --git a/scripts/publish-gitea.sh b/scripts/publish-gitea.sh deleted file mode 100644 index ca2958e..0000000 --- a/scripts/publish-gitea.sh +++ /dev/null @@ -1,258 +0,0 @@ -#!/usr/bin/env bash -# 发布构建产物到 Gitea: -# 1) Generic Package Registry(API 包,GET /api/packages/...) -# 2) Release 附件 —— OTA 更新源只认 Release 附件 -# (命名约定 mqttx-v--mqttx[.exe],含 .sha256 侧车) -# -# 用法: -# GITEA_URL=https://gitea.example.com GITEA_TOKEN=xxx \ -# ./scripts/publish-gitea.sh [文件...] [版本] -# -# 环境变量: -# GITEA_URL 实例地址,如 https://gitea.example.com(必填) -# GITEA_TOKEN 访问令牌,需 package + repo 写权限(必填) -# GITEA_OWNER 软件包所属用户/组织(默认:由 git remote 推断,失败则报错) -# GITEA_REPO 仓库名(默认:由 git remote 推断,失败为 mqttx) -# GITEA_PKG 软件包名(默认 mqttx) -# -# 版本推断顺序:参数 > git describe --tags > gpui-app/Cargo.toml。 -set -euo pipefail - -die() { echo "错误: $*" >&2; exit 1; } - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" - -# ── 地址:优先环境变量,否则从 git remote origin 推断 ──────────────────────── -if [ -z "${GITEA_URL:-}" ]; then - remote_url="$(git -C "$ROOT" remote get-url origin 2>/dev/null || true)" - case "$remote_url" in - *://*) host="${remote_url#*://}"; host="${host%%/*}" ;; - *:*) host="${remote_url%%:*}" ;; - *) host="" ;; - esac - host="${host##*@}" # 去掉 git@user@ 前缀 - [ -n "$host" ] && GITEA_URL="https://$host" -fi -[ -n "${GITEA_URL:-}" ] || die "缺少 GITEA_URL(或配置 git remote origin)" -GITEA_URL="${GITEA_URL%/}" - -# ── 令牌:优先环境变量,否则取 Git Credential Manager 中同一主机的凭证 ──────── -if [ -z "${GITEA_TOKEN:-}" ]; then - GITEA_TOKEN="$(printf 'protocol=https\nhost=%s\n\n' "${GITEA_URL#*://}" \ - | GCM_INTERACTIVE=Never GIT_TERMINAL_PROMPT=0 git credential fill 2>/dev/null \ - | sed -n 's/^password=//p')" - [ -n "${GITEA_TOKEN:-}" ] || die "缺少 GITEA_TOKEN,且无法从 git credential store 自动获取" -fi -PKG="${GITEA_PKG:-mqttx}" - -# ── owner/repo:优先环境变量,其次从 git remote 推断 ───────────────────────── -remote_owner_repo() { - local url path - url="$(git -C "$ROOT" remote get-url origin 2>/dev/null)" || return 1 - case "$url" in - *://*) path="${url#*://}"; path="${path#*/}" ;; # https://host/owner/repo.git - *:*) path="${url#*:}" ;; # git@host:owner/repo.git - *) return 1 ;; - esac - path="${path%.git}" - printf '%s\n' "${path%%/*}" "${path#*/}" -} -OWNER="${GITEA_OWNER:-$(remote_owner_repo | head -1 || true)}" -REPO="${GITEA_REPO:-$(remote_owner_repo | tail -1 || true)}" -[ -n "${OWNER:-}" ] || die "无法推断 GITEA_OWNER,请显式设置" -[ -n "${REPO:-}" ] || REPO="$PKG" - -# ── 版本:参数 > 当前提交的精确 tag > Cargo.toml ───────────────────────────── -# 严禁用 'git describe --tags --always' 兜底:它会产出 v1.0.0-4-gabc1234 这类非 -# semver 描述串,据此创建的 Release 会被客户端 release_part() 截成 1.0.0 并判定 -# "不比当前版本新" —— 更新永远发不出去,而且该 Release 会占据 /releases/latest, -# 把真正的更新挡在后面。 -FILES=() -for arg in "$@"; do - if [ -f "$arg" ]; then FILES+=("$arg"); else VERSION="$arg"; fi -done -CARGO_VER="$(grep -m1 '^version' "$ROOT/gpui-app/Cargo.toml" | cut -d'"' -f2)" -if [ -z "${VERSION:-}" ]; then - VERSION="$(git -C "$ROOT" describe --tags --exact-match 2>/dev/null || true)" - [ -n "$VERSION" ] && echo "→ 版本取自当前提交的 tag: $VERSION" -fi -if [ -z "${VERSION:-}" ]; then - VERSION="$CARGO_VER" - [ -n "$VERSION" ] && echo "→ HEAD 无 tag,版本回退为 gpui-app/Cargo.toml: $VERSION" >&2 -fi -[ -n "${VERSION:-}" ] || die "无法确定版本号:请显式传入(如 v1.0.1)或先给当前提交打 tag" -# Gitea 版本段不允许 "/" -VERSION="${VERSION//\//-}" -# semver 校验:非 semver 版本会让 OTA 静默失效(见上方注释),宁可直接拒绝发布 -if ! printf '%s' "$VERSION" | grep -Eq '^[vV]?[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$'; then - die "版本号 '$VERSION' 不是合法 semver(应形如 v1.0.1 或 1.0.1-rc.1)" -fi -# 与编译期版本比对:不一致时客户端装完新版本仍自报旧版本号,会反复提示同一更新 -if [ -n "$CARGO_VER" ] && [ "${VERSION#v}" != "$CARGO_VER" ] && [ "${VERSION#V}" != "$CARGO_VER" ]; then - echo " ! 警告:版本 '$VERSION' 与 gpui-app/Cargo.toml ($CARGO_VER) 不一致" >&2 -fi - -# ── 默认产物 ──────────────────────────────────────────────────────────────── -if [ "${#FILES[@]}" -eq 0 ]; then - if [ -x "$ROOT/target/release/mqttx.exe" ]; then - FILES=("$ROOT/target/release/mqttx.exe") - elif [ -x "$ROOT/target/release/mqttx" ]; then - FILES=("$ROOT/target/release/mqttx") - else - die "未找到产物,请先 cargo build --release,或显式传入文件" - fi -fi - -echo "→ Gitea: $GITEA_URL" -echo "→ 软件包: $OWNER/$PKG@$VERSION" -echo "→ 仓库: $OWNER/$REPO" -echo "→ 产物: ${FILES[*]}" - -sha256() { - if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1 - else shasum -a 256 "$1" | cut -d' ' -f1; fi -} - -# OTA 产物命名约定的平台三元组(与 update.rs::platform_triple 一致) -triple_of() { - case "$(uname -s)/$(uname -m)" in - MINGW*/*x86_64*|MSYS*/*x86_64*|CYGWIN*/*x86_64*) echo "x86_64-pc-windows-msvc" ;; - Linux/x86_64|Linux/amd64) echo "x86_64-unknown-linux-gnu" ;; - Darwin/x86_64) echo "x86_64-apple-darwin" ;; - Darwin/arm64) echo "aarch64-apple-darwin" ;; - Linux/aarch64|Linux/arm64) echo "aarch64-unknown-linux-gnu" ;; - *) echo "" ;; - esac -} -TRIPLE="${GITEA_TRIPLE:-$(triple_of)}" - -for f in "${FILES[@]}"; do - [ -f "$f" ] || die "文件不存在: $f" - name="$(basename "$f")" - echo "→ 上传 $name ($(du -h "$f" | cut -f1))" - - code="$(curl -sS -o /tmp/gitea-pkg-resp.txt -w '%{http_code}' \ - -X PUT \ - -H "Authorization: token ${GITEA_TOKEN}" \ - --upload-file "$f" \ - "${GITEA_URL}/api/packages/${OWNER}/generic/${PKG}/${VERSION}/${name}")" - - case "$code" in - 201|200|409) # 409 = 同名版本已存在,Gitea 拒绝覆盖 - if [ "$code" = "409" ]; then - echo " ! 已存在同名版本(409),跳过:$name" - else - echo " ✓ 已发布 $name (HTTP $code)" - # 附带 sha256 校验文件,便于下游验证 - sum="$(sha256 "$f")" - echo "$sum $name" > "${f}.sha256" - code2="$(curl -sS -o /dev/null -w '%{http_code}' \ - -X PUT \ - -H "Authorization: token ${GITEA_TOKEN}" \ - -H "Content-Type: text/plain" \ - --data-binary "@${f}.sha256" \ - "${GITEA_URL}/api/packages/${OWNER}/generic/${PKG}/${VERSION}/${name}.sha256")" - [ "$code2" = "201" ] || echo " ! sha256 文件上传失败 (HTTP $code2)" - fi - ;; - *) - echo " ✗ 上传失败 (HTTP $code)" >&2 - cat /tmp/gitea-pkg-resp.txt >&2 || true - exit 1 - ;; - esac -done - -echo "完成。软件包页面: ${GITEA_URL}/${OWNER}?tab=packages" - -# ── Release 附件:OTA 更新源(update.rs 只查 /releases)────────────────────── -API="${GITEA_URL}/api/v1/repos/${OWNER}/${REPO}" -STAGE="$(mktemp -d)" -trap 'rm -rf "$STAGE"' EXIT - -# 规范化版本号:tag 推断出的 "v1.0.2" 与 Cargo 的 "1.0.2" 统一为无 v 数字段 -VER_NUM="${VERSION#v}" -[ -n "$TRIPLE" ] || TRIPLE="unknown" - -# 取(或创建)与版本同名 tag 的 Release;找不到时按需创建 -code="$(curl -sS -o "$STAGE/rel.json" -w '%{http_code}' \ - -H "Authorization: token ${GITEA_TOKEN}" \ - "${API}/releases/tags/v${VER_NUM}")" -if [ "$code" = "404" ]; then - echo "→ Release v${VER_NUM} 不存在,创建中…" - code="$(curl -sS -o "$STAGE/rel.json" -w '%{http_code}' \ - -X POST \ - -H "Authorization: token ${GITEA_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "{\"tag_name\":\"v${VER_NUM}\",\"name\":\"v${VER_NUM}\",\"draft\":false,\"prerelease\":false}" \ - "${API}/releases")" - if [ "$code" != "201" ]; then - echo " ! Release 创建失败 (HTTP $code),跳过附件上传" >&2 - cat "$STAGE/rel.json" >&2 || true - # OTA 只认 Release 附件:这里失败等于本次发布无效,必须让调用方看到失败, - # 而不是把 job 变绿、让"发布成功"与"用户能收到更新"脱钩。 - exit 1 - fi -elif [ "$code" != "200" ]; then - echo " ! 查询 Release 失败 (HTTP $code),跳过附件上传" >&2 - exit 1 -fi -# 顶层 "id" 是首个出现的 id 字段 -RELEASE_ID="$(sed -n 's/.*"id":[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$STAGE/rel.json" | head -1)" -[ -n "$RELEASE_ID" ] || { echo " ! 无法解析 Release id,跳过附件上传" >&2; exit 1; } -echo "→ Release v${VER_NUM} (id=$RELEASE_ID),上传附件…" - -for f in "${FILES[@]}"; do - name="$(basename "$f")" - # 已符合 OTA 命名约定的原样使用;否则按约定规范重命名后上传 - case "$name" in - mqttx-v*-mqttx|mqttx-v*-mqttx.exe) asset="$name" ;; - *.exe) asset="mqttx-v${VER_NUM}-${TRIPLE}-mqttx.exe" ;; - *) asset="mqttx-v${VER_NUM}-${TRIPLE}-mqttx" ;; - esac - - # ── 7z 压缩附件(OTA 主通道):内部文件名 = $asset(应用解压后按此名安装)── - SEVENZIP="$(command -v 7z 7za 2>/dev/null | head -1)" - [ -z "$SEVENZIP" ] && [ -x "/c/Program Files/7-Zip/7z.exe" ] && SEVENZIP="/c/Program Files/7-Zip/7z.exe" - if [ -n "$SEVENZIP" ]; then - "$SEVENZIP" a -y -mx=9 "$STAGE/$asset.7z" "$f" >/dev/null - # 归档内的文件名需为 $asset:7z 存的是源文件名,先复制改名再压 - rm -f "$STAGE/$asset.7z" - cp -f "$f" "$STAGE/$asset" - "$SEVENZIP" a -y -mx=9 "$STAGE/$asset.7z" "$STAGE/$asset" >/dev/null - if [ -f "$STAGE/$asset.7z" ]; then - printf '%s %s\n' "$(sha256 "$STAGE/$asset.7z")" "$asset.7z" > "$STAGE/$asset.7z.sha256" - for a in "$asset.7z" "$asset.7z.sha256"; do - code="$(curl -sS -o "$STAGE/up.json" -w '%{http_code}' \ - -X POST \ - -H "Authorization: token ${GITEA_TOKEN}" \ - -F "attachment=@${STAGE}/${a}" \ - "${API}/releases/${RELEASE_ID}/assets?name=${a}")" - case "$code" in - 201) echo " ✓ Release 附件 $a" ;; - *) echo " ! Release 附件 $a 上传失败 (HTTP $code)" >&2; cat "$STAGE/up.json" >&2 || true ;; - esac - done - else - echo " ! 7z 打包失败,跳过压缩附件" >&2 - fi - else - echo " ! 未找到 7z/7za,跳过 7z 附件(仅上传裸二进制)" >&2 - fi - - # ── 裸二进制附件:v1.0.3 及更早客户端不认识 7z,需要它升级 ── - cp -f "$f" "$STAGE/$asset" - printf '%s %s\n' "$(sha256 "$f")" "$asset" > "$STAGE/$asset.sha256" - for a in "$asset" "$asset.sha256"; do - code="$(curl -sS -o "$STAGE/up.json" -w '%{http_code}' \ - -X POST \ - -H "Authorization: token ${GITEA_TOKEN}" \ - -F "attachment=@${STAGE}/${a}" \ - "${API}/releases/${RELEASE_ID}/assets?name=${a}")" - case "$code" in - 201) echo " ✓ Release 附件 $a" ;; - *) echo " ! Release 附件 $a 上传失败 (HTTP $code)" >&2; cat "$STAGE/up.json" >&2 || true ;; - esac - done -done -echo "完成。Release 页面: ${GITEA_URL}/${OWNER}/${REPO}/releases/tag/v${VER_NUM}"