ci: 发布脚本自动创建 Release 并按 OTA 命名上传附件
应用内 OTA 只认 Gitea Release 附件(update.rs 查 /releases),此前脚本 只上传 Generic 包,Release 不存在时更新检查永远失败。现在: - 自动查询/创建与版本同名 tag 的 Release,附件命名规范为 mqttx-v<ver>-<triple>-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)
This commit is contained in:
@@ -1,13 +1,15 @@
|
|||||||
<#
|
<#
|
||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
发布构建产物到 Gitea Package Registry(Generic 包)。
|
发布构建产物到 Gitea:Generic Package + Release 附件(OTA 更新源)。
|
||||||
|
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
用 Gitea Generic Package API 上传文件:
|
1) Generic Package API 上传:
|
||||||
PUT {GITEA_URL}/api/packages/{owner}/generic/{pkg}/{version}/{filename}
|
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<ver>-<triple>-mqttx[.exe],含 .sha256 侧车。
|
||||||
认证:Authorization: token <GITEA_TOKEN>
|
认证:Authorization: token <GITEA_TOKEN>
|
||||||
同名版本重复上传会得到 409,脚本按"已存在"处理。
|
同名包版本重复上传会得到 409,脚本按"已存在"处理。
|
||||||
上传成功后附加 .sha256 校验文件。
|
|
||||||
|
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
$env:GITEA_URL = "https://gitea.example.com"
|
$env:GITEA_URL = "https://gitea.example.com"
|
||||||
@@ -22,6 +24,8 @@
|
|||||||
软件包版本。缺省时依次尝试 git describe --tags、gpui-app/Cargo.toml。
|
软件包版本。缺省时依次尝试 git describe --tags、gpui-app/Cargo.toml。
|
||||||
.PARAMETER Owner
|
.PARAMETER Owner
|
||||||
覆盖 GITEA_OWNER(缺省时从 git remote origin 推断)。
|
覆盖 GITEA_OWNER(缺省时从 git remote origin 推断)。
|
||||||
|
.PARAMETER Repo
|
||||||
|
覆盖 GITEA_REPO(缺省时从 git remote origin 推断,失败为 mqttx)。
|
||||||
.PARAMETER PackageName
|
.PARAMETER PackageName
|
||||||
覆盖 GITEA_PKG(默认 mqttx)。
|
覆盖 GITEA_PKG(默认 mqttx)。
|
||||||
#>
|
#>
|
||||||
@@ -30,6 +34,7 @@ param(
|
|||||||
[string[]]$Files = @(),
|
[string[]]$Files = @(),
|
||||||
[string]$Version = "",
|
[string]$Version = "",
|
||||||
[string]$Owner = "",
|
[string]$Owner = "",
|
||||||
|
[string]$Repo = "",
|
||||||
[string]$PackageName = ""
|
[string]$PackageName = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -52,9 +57,10 @@ $GiteaUrl = $GiteaUrl.TrimEnd("/")
|
|||||||
$Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
$Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
|
||||||
$Pkg = if ($PackageName) { $PackageName } elseif ($env:GITEA_PKG) { $env:GITEA_PKG } else { "mqttx" }
|
$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) { $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
|
$remote = git -C $Root remote get-url origin 2>$null
|
||||||
if ($remote) {
|
if ($remote) {
|
||||||
# 兼容 git@host:owner/repo.git 与 https://host/owner/repo.git
|
# 兼容 git@host:owner/repo.git 与 https://host/owner/repo.git
|
||||||
@@ -68,10 +74,14 @@ if (-not $Owner) {
|
|||||||
}
|
}
|
||||||
$path = $path -replace "\.git$", ""
|
$path = $path -replace "\.git$", ""
|
||||||
$seg = @($path -split "/" | Where-Object { $_ })
|
$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 $Owner) { Die "无法推断 owner,请用 -Owner 或 GITEA_OWNER 指定" }
|
||||||
|
if (-not $Repo) { $Repo = $Pkg }
|
||||||
|
|
||||||
# ── 版本:参数 > git describe > Cargo.toml ──────────────────────────────────
|
# ── 版本:参数 > git describe > Cargo.toml ──────────────────────────────────
|
||||||
if (-not $Version) {
|
if (-not $Version) {
|
||||||
@@ -95,6 +105,7 @@ if (-not $Files -or $Files.Count -eq 0) {
|
|||||||
|
|
||||||
Write-Host "→ Gitea: $GiteaUrl"
|
Write-Host "→ Gitea: $GiteaUrl"
|
||||||
Write-Host "→ 软件包: $Owner/$Pkg@$Version"
|
Write-Host "→ 软件包: $Owner/$Pkg@$Version"
|
||||||
|
Write-Host "→ 仓库: $Owner/$Repo"
|
||||||
Write-Host "→ 产物: $($Files -join ', ')"
|
Write-Host "→ 产物: $($Files -join ', ')"
|
||||||
|
|
||||||
foreach ($f in $Files) {
|
foreach ($f in $Files) {
|
||||||
@@ -141,3 +152,76 @@ foreach ($f in $Files) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "完成。软件包页面: $GiteaUrl/$Owner?tab=packages" -ForegroundColor Cyan
|
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
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# 发布构建产物到 Gitea Package Registry(Generic 包)。
|
# 发布构建产物到 Gitea:
|
||||||
|
# 1) Generic Package Registry(API 包,GET /api/packages/...)
|
||||||
|
# 2) Release 附件 —— OTA 更新源只认 Release 附件
|
||||||
|
# (命名约定 mqttx-v<ver>-<triple>-mqttx[.exe],含 .sha256 侧车)
|
||||||
#
|
#
|
||||||
# 用法:
|
# 用法:
|
||||||
# GITEA_URL=https://gitea.example.com GITEA_TOKEN=xxx \
|
# GITEA_URL=https://gitea.example.com GITEA_TOKEN=xxx \
|
||||||
@@ -7,8 +10,9 @@
|
|||||||
#
|
#
|
||||||
# 环境变量:
|
# 环境变量:
|
||||||
# GITEA_URL 实例地址,如 https://gitea.example.com(必填)
|
# GITEA_URL 实例地址,如 https://gitea.example.com(必填)
|
||||||
# GITEA_TOKEN 访问令牌,需 package 写权限(必填)
|
# GITEA_TOKEN 访问令牌,需 package + repo 写权限(必填)
|
||||||
# GITEA_OWNER 软件包所属用户/组织(默认:由 git remote 推断,失败则报错)
|
# GITEA_OWNER 软件包所属用户/组织(默认:由 git remote 推断,失败则报错)
|
||||||
|
# GITEA_REPO 仓库名(默认:由 git remote 推断,失败为 mqttx)
|
||||||
# GITEA_PKG 软件包名(默认 mqttx)
|
# GITEA_PKG 软件包名(默认 mqttx)
|
||||||
#
|
#
|
||||||
# 版本推断顺序:参数 > git describe --tags > gpui-app/Cargo.toml。
|
# 版本推断顺序:参数 > git describe --tags > gpui-app/Cargo.toml。
|
||||||
@@ -23,8 +27,8 @@ GITEA_URL="${GITEA_URL%/}"
|
|||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
PKG="${GITEA_PKG:-mqttx}"
|
PKG="${GITEA_PKG:-mqttx}"
|
||||||
|
|
||||||
# ── owner:优先环境变量,其次从 git remote 推断 ──────────────────────────────
|
# ── owner/repo:优先环境变量,其次从 git remote 推断 ─────────────────────────
|
||||||
owner_of_remote() {
|
remote_owner_repo() {
|
||||||
local url path
|
local url path
|
||||||
url="$(git -C "$ROOT" remote get-url origin 2>/dev/null)" || return 1
|
url="$(git -C "$ROOT" remote get-url origin 2>/dev/null)" || return 1
|
||||||
case "$url" in
|
case "$url" in
|
||||||
@@ -33,10 +37,12 @@ owner_of_remote() {
|
|||||||
*) return 1 ;;
|
*) return 1 ;;
|
||||||
esac
|
esac
|
||||||
path="${path%.git}"
|
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 "${OWNER:-}" ] || die "无法推断 GITEA_OWNER,请显式设置"
|
||||||
|
[ -n "${REPO:-}" ] || REPO="$PKG"
|
||||||
|
|
||||||
# ── 版本:参数 > git describe > Cargo.toml ───────────────────────────────────
|
# ── 版本:参数 > git describe > Cargo.toml ───────────────────────────────────
|
||||||
FILES=()
|
FILES=()
|
||||||
@@ -66,6 +72,7 @@ fi
|
|||||||
|
|
||||||
echo "→ Gitea: $GITEA_URL"
|
echo "→ Gitea: $GITEA_URL"
|
||||||
echo "→ 软件包: $OWNER/$PKG@$VERSION"
|
echo "→ 软件包: $OWNER/$PKG@$VERSION"
|
||||||
|
echo "→ 仓库: $OWNER/$REPO"
|
||||||
echo "→ 产物: ${FILES[*]}"
|
echo "→ 产物: ${FILES[*]}"
|
||||||
|
|
||||||
sha256() {
|
sha256() {
|
||||||
@@ -73,6 +80,19 @@ sha256() {
|
|||||||
else shasum -a 256 "$1" | cut -d' ' -f1; fi
|
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
|
for f in "${FILES[@]}"; do
|
||||||
[ -f "$f" ] || die "文件不存在: $f"
|
[ -f "$f" ] || die "文件不存在: $f"
|
||||||
name="$(basename "$f")"
|
name="$(basename "$f")"
|
||||||
@@ -111,3 +131,62 @@ for f in "${FILES[@]}"; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
echo "完成。软件包页面: ${GITEA_URL}/${OWNER}?tab=packages"
|
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}"
|
||||||
|
|||||||
Reference in New Issue
Block a user