From 6ff05f72160af2952abea2ef4416555464f85f36 Mon Sep 17 00:00:00 2001 From: JoeAllen <2494136784@qq.com> Date: Tue, 29 Sep 2026 10:33:42 +0800 Subject: [PATCH] =?UTF-8?q?ci:=20=E5=8F=91=E5=B8=83=E8=84=9A=E6=9C=AC?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=88=9B=E5=BB=BA=20Release=20=E5=B9=B6?= =?UTF-8?q?=E6=8C=89=20OTA=20=E5=91=BD=E5=90=8D=E4=B8=8A=E4=BC=A0=E9=99=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 应用内 OTA 只认 Gitea Release 附件(update.rs 查 /releases),此前脚本 只上传 Generic 包,Release 不存在时更新检查永远失败。现在: - 自动查询/创建与版本同名 tag 的 Release,附件命名规范为 mqttx-v--mqttx[.exe](不符约定时重命名后上传), 附带 .sha256 侧车,供 OTA sha256 校验 - 版本号统一剥离 v 前缀(git describe 与 Cargo.toml 两种来源一致) - 平台三元组按运行环境推断(与 update.rs::platform_triple 一致), 可用 GITEA_TRIPLE 覆盖 - owner/repo 均支持从 git remote 推断,可用 GITEA_REPO 参数覆盖 - ps1 附件上传走 .NET HttpClient multipart(PS 5.1 无 -Form) --- scripts/publish-gitea.ps1 | 100 +++++++++++++++++++++++++++++++++++--- scripts/publish-gitea.sh | 91 +++++++++++++++++++++++++++++++--- 2 files changed, 177 insertions(+), 14 deletions(-) diff --git a/scripts/publish-gitea.ps1 b/scripts/publish-gitea.ps1 index a51e3a9..572ef78 100644 --- a/scripts/publish-gitea.ps1 +++ b/scripts/publish-gitea.ps1 @@ -1,13 +1,15 @@ <# .SYNOPSIS - 发布构建产物到 Gitea Package Registry(Generic 包)。 + 发布构建产物到 Gitea:Generic Package + Release 附件(OTA 更新源)。 .DESCRIPTION - 用 Gitea Generic Package API 上传文件: - PUT {GITEA_URL}/api/packages/{owner}/generic/{pkg}/{version}/{filename} + 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,脚本按"已存在"处理。 - 上传成功后附加 .sha256 校验文件。 + 同名包版本重复上传会得到 409,脚本按"已存在"处理。 .EXAMPLE $env:GITEA_URL = "https://gitea.example.com" @@ -22,6 +24,8 @@ 软件包版本。缺省时依次尝试 git describe --tags、gpui-app/Cargo.toml。 .PARAMETER Owner 覆盖 GITEA_OWNER(缺省时从 git remote origin 推断)。 +.PARAMETER Repo + 覆盖 GITEA_REPO(缺省时从 git remote origin 推断,失败为 mqttx)。 .PARAMETER PackageName 覆盖 GITEA_PKG(默认 mqttx)。 #> @@ -30,6 +34,7 @@ param( [string[]]$Files = @(), [string]$Version = "", [string]$Owner = "", + [string]$Repo = "", [string]$PackageName = "" ) @@ -52,9 +57,10 @@ $GiteaUrl = $GiteaUrl.TrimEnd("/") $Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) $Pkg = if ($PackageName) { $PackageName } elseif ($env:GITEA_PKG) { $env:GITEA_PKG } else { "mqttx" } -# ── owner:参数 > GITEA_OWNER > git remote origin 推断 ────────────────────── +# ── owner/repo:参数 > 环境变量 > git remote origin 推断 ───────────────────── if (-not $Owner) { $Owner = $env:GITEA_OWNER } -if (-not $Owner) { +if (-not $Repo) { $Repo = $env:GITEA_REPO } +if (-not $Owner -or -not $Repo) { $remote = git -C $Root remote get-url origin 2>$null if ($remote) { # 兼容 git@host:owner/repo.git 与 https://host/owner/repo.git @@ -68,10 +74,14 @@ if (-not $Owner) { } $path = $path -replace "\.git$", "" $seg = @($path -split "/" | Where-Object { $_ }) - if ($seg.Count -ge 2) { $Owner = $seg[0] } + 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 } # ── 版本:参数 > git describe > Cargo.toml ────────────────────────────────── if (-not $Version) { @@ -95,6 +105,7 @@ if (-not $Files -or $Files.Count -eq 0) { Write-Host "→ Gitea: $GiteaUrl" Write-Host "→ 软件包: $Owner/$Pkg@$Version" +Write-Host "→ 仓库: $Owner/$Repo" Write-Host "→ 产物: $($Files -join ', ')" foreach ($f in $Files) { @@ -141,3 +152,76 @@ foreach ($f in $Files) { } 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 { + Write-Host " ! Release 创建/查询失败:$($_.Exception.Message);跳过附件上传" -ForegroundColor Yellow + return + } +} +$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" } + $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 index 17ae21b..c30b3ab 100644 --- a/scripts/publish-gitea.sh +++ b/scripts/publish-gitea.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash -# 发布构建产物到 Gitea Package Registry(Generic 包)。 +# 发布构建产物到 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 \ @@ -7,8 +10,9 @@ # # 环境变量: # GITEA_URL 实例地址,如 https://gitea.example.com(必填) -# GITEA_TOKEN 访问令牌,需 package 写权限(必填) +# GITEA_TOKEN 访问令牌,需 package + repo 写权限(必填) # GITEA_OWNER 软件包所属用户/组织(默认:由 git remote 推断,失败则报错) +# GITEA_REPO 仓库名(默认:由 git remote 推断,失败为 mqttx) # GITEA_PKG 软件包名(默认 mqttx) # # 版本推断顺序:参数 > git describe --tags > gpui-app/Cargo.toml。 @@ -23,8 +27,8 @@ GITEA_URL="${GITEA_URL%/}" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PKG="${GITEA_PKG:-mqttx}" -# ── owner:优先环境变量,其次从 git remote 推断 ────────────────────────────── -owner_of_remote() { +# ── 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 @@ -33,10 +37,12 @@ owner_of_remote() { *) return 1 ;; esac path="${path%.git}" - printf '%s\n' "${path%%/*}" + printf '%s\n' "${path%%/*}" "${path#*/}" } -OWNER="${GITEA_OWNER:-$(owner_of_remote || true)}" +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" # ── 版本:参数 > git describe > Cargo.toml ─────────────────────────────────── FILES=() @@ -66,6 +72,7 @@ fi echo "→ Gitea: $GITEA_URL" echo "→ 软件包: $OWNER/$PKG@$VERSION" +echo "→ 仓库: $OWNER/$REPO" echo "→ 产物: ${FILES[*]}" sha256() { @@ -73,6 +80,19 @@ sha256() { 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")" @@ -111,3 +131,62 @@ for f in "${FILES[@]}"; do 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 + exit 0 + fi +elif [ "$code" != "200" ]; then + echo " ! 查询 Release 失败 (HTTP $code),跳过附件上传" >&2 + exit 0 +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 0; } +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 + 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}"