v2.1.0 的 Console Release 在 publish job 红:
`安装包数量不足(3 < 4),架构可能缺失`。
根因是**守卫与实现漂移**,不是产物缺失:build 矩阵是
x64 → msi+nsis(2 个)、arm64 → 仅 nsis(1 个,WiX 的 arm64 MSI 支持面窄,
刻意不出),合计恒为 3;而断言写死「两架构各一套(exe+msi)= 4 个」,
于是从 arm64 去掉 msi 那天起就**每次都红** —— 红门禁等于没有门禁。
改为按架构断言(x64 ≥2 / arm64 ≥1),缺失时精确报出是哪一架构;
矩阵再变也只需改一行期望值。
附带修一个只在 CI 爆的坑:`$dir)`(变量紧贴全角括号)在 set -u 下被 bash
当成变量名的一部分 → unbound variable,本地肉眼完全正常。变量引用统一改
${var} 形式,并用抽取工作流原文的方式在本地跑通正例/反例两个分支。
409 lines
21 KiB
YAML
409 lines
21 KiB
YAML
name: Console Windows
|
||
|
||
# 控制台(apps/console)的 Windows 安装包构建。
|
||
#
|
||
# 为什么单独一个工作流、而不塞进 windows-e2e
|
||
# ----------------------------------------
|
||
# windows-e2e 是「被控端在 Windows 上的行为验证」,跑一遍约 4 分钟、含安装/卸载真机冒烟;
|
||
# 而控制台打包要装 Rust 工具链、编 Tauri(首次冷编 10 分钟级)。两者目标不同、
|
||
# 耗时量级不同,混在一起会让「改一行前端也要等 15 分钟」。
|
||
#
|
||
# ⚠️ 在此之前,控制台的 Windows 侧只被验证到 prepare:node 与自举(见 windows-e2e),
|
||
# **从未真跑过 tauri build**。而 Rust 里那些 `cfg!(windows)` 分支
|
||
# (taskkill 收进程树、PowerShell 计划任务、node.exe 路径)在 macOS 上
|
||
# 只被类型检查、从未被编译成 Windows 目标,更没有产物级验证 —— 故本工作流是它们的第一道真闸。
|
||
#
|
||
# 架构(为什么必须两套)
|
||
# --------------------
|
||
# 被控端发布包曾出过「arm64 包里装的是 x64 node.exe」的事故,后果是
|
||
# **在 ARM Windows 上能 listen、但 TLS 握手不完成**,且 macOS 侧 100% 复现不出。
|
||
# 控制台同理:它内含 node 运行时,且要用它**托起本机被控端**(即 TLS 服务端)。
|
||
# 故 x64 与 arm64 必须各出一套,且都要能装到对应架构的 Windows 上。
|
||
#
|
||
# 这里用 GitHub 的 ARM64 Windows runner(`windows-11-arm`,公共仓库免费)
|
||
# **原生构建**,而不是在 x64 runner 上交叉编译 —— 交叉编译需要 VS 的 ARM64 工具链,
|
||
# 且 `prepare-console.mjs` 的自检要真执行 node.exe(x64 机器跑不了 arm64 二进制),
|
||
# 会逼着引入「跳过自检」这类削弱验证的开关。原生构建则自检照跑、无特殊分支。
|
||
|
||
on:
|
||
push:
|
||
branches: [main]
|
||
paths:
|
||
- 'apps/console/**'
|
||
- 'apps/agent/**'
|
||
- 'packages/**'
|
||
- 'scripts/prepare-console.mjs'
|
||
- 'scripts/lib/bundle-agent.mjs'
|
||
- 'package.json'
|
||
- 'pnpm-lock.yaml'
|
||
- '.github/workflows/console-windows.yml'
|
||
pull_request:
|
||
branches: [main]
|
||
paths:
|
||
- 'apps/console/**'
|
||
- '.github/workflows/console-windows.yml'
|
||
workflow_dispatch:
|
||
# 供 `console-release.yml` 在 tag 推送时调用。
|
||
#
|
||
# 为什么不直接把 `tags` 加进上面的 push:`paths` 与 `tags` 同时存在时,
|
||
# tag 推送会因为「路径过滤」判定不通过而**静默不触发**(这是 GitHub Actions
|
||
# 的经典坑)。拆成「主工作流只跑分支 + 发布工作流只认 tag」最不容易出错。
|
||
workflow_call:
|
||
|
||
concurrency:
|
||
group: ${{ github.workflow }}-${{ github.ref }}
|
||
cancel-in-progress: true
|
||
|
||
jobs:
|
||
build:
|
||
name: 控制台安装包 · ${{ matrix.arch }}
|
||
runs-on: ${{ matrix.runner }}
|
||
timeout-minutes: 60
|
||
|
||
strategy:
|
||
# 一条架构挂了不要拖停另一条 —— 否则「arm64 失败」会掩盖「x64 到底成没成」
|
||
fail-fast: false
|
||
matrix:
|
||
include:
|
||
- arch: x64
|
||
runner: windows-latest
|
||
# x64 给两种安装形态:msi(企业批量部署)+ nsis(普通用户双击)
|
||
bundles: msi,nsis
|
||
pe: '0x8664'
|
||
- arch: arm64
|
||
runner: windows-11-arm
|
||
# arm64 只出 nsis:WiX 的 arm64 MSI 支持面较窄,不值得为它引入不确定性
|
||
bundles: nsis
|
||
pe: '0xaa64'
|
||
|
||
steps:
|
||
- name: 检出代码
|
||
uses: actions/checkout@v5
|
||
|
||
- name: 安装 pnpm
|
||
uses: pnpm/action-setup@v4
|
||
with:
|
||
version: 12
|
||
|
||
- name: 安装 Node.js
|
||
uses: actions/setup-node@v4
|
||
with:
|
||
node-version: 22
|
||
cache: pnpm
|
||
|
||
- name: 安装 Rust 工具链
|
||
uses: dtolnay/rust-toolchain@stable
|
||
|
||
- name: 缓存 Rust 构建产物
|
||
uses: Swatinem/rust-cache@v2
|
||
with:
|
||
# 只缓存 src-tauri(控制台壳),不牵动仓库根的 Rust 工作区
|
||
workspaces: apps/console/src-tauri
|
||
|
||
- name: 安装依赖
|
||
run: pnpm install --frozen-lockfile
|
||
|
||
# ⚠️ 这条断言是必需项,不是「顺手检查」。
|
||
# ARM runner 的镜像里确实存在 x64 模拟的组件(社区实测 python 即为 AMD64),
|
||
# 一旦 Node 也是模拟版,process.arch 会是 x64 → prepare-console 会把 x64 的 node.exe
|
||
# 打进 arm64 安装包 → **装到真机上能启动、TLS 握手不完成**,且本机 CI 全绿。
|
||
# 故先断言「我到底在什么架构上编」,再往下走。
|
||
- name: 断言构建机架构与目标一致
|
||
shell: pwsh
|
||
run: |
|
||
$ErrorActionPreference = 'Stop'
|
||
$arch = node -p "process.arch"
|
||
Write-Host "node process.arch = $arch"
|
||
Write-Host "runner = ${{ matrix.runner }}"
|
||
if ($arch -ne '${{ matrix.arch }}') {
|
||
throw "架构不符:期望 ${{ matrix.arch }},实际 $arch —— 打进包的 node.exe 会是错的架构"
|
||
}
|
||
|
||
- name: 构建 workspace 包(含被控端)
|
||
# agent 的 dist 是 prepare-console 打包被控端的前置产物,顺序不能调
|
||
run: pnpm --filter "./packages/*" --filter "@nodeagent/agent" build
|
||
|
||
# prepare-console 的前置检查要求 sidecar.mjs 已存在;它自己也会调 esbuild 打 agent.mjs
|
||
- name: 构建 sidecar 并准备随包资源(node + agent.mjs)
|
||
working-directory: apps/console
|
||
run: |
|
||
pnpm sidecar:build
|
||
pnpm prepare:node
|
||
|
||
# ⚠️ 交付物级断言(与 pack.mjs 的双架构校验同源思路):
|
||
# 不只看「脚本跑完了」,而是直接读最终要打进包的那个 node.exe 的 PE 头。
|
||
# 中间逻辑再自洽,也可能因为上游给了错架构的二进制而产出错包。
|
||
- name: 断言随包 node.exe 的 PE 架构
|
||
shell: pwsh
|
||
run: |
|
||
$ErrorActionPreference = 'Stop'
|
||
$node = 'apps/console/src-tauri/runtime/node.exe'
|
||
if (-not (Test-Path $node)) { throw "缺少随包 Node 运行时:$node" }
|
||
$bytes = [IO.File]::ReadAllBytes((Resolve-Path $node))
|
||
$peOff = [BitConverter]::ToUInt32($bytes, 0x3C)
|
||
$machine = [BitConverter]::ToUInt16($bytes, $peOff + 4)
|
||
$actual = '0x{0:x4}' -f $machine
|
||
Write-Host "node.exe PE Machine = $actual(体积 $([math]::Round($bytes.Length/1MB,1)) MB)"
|
||
if ($actual -ne '${{ matrix.pe }}') {
|
||
throw "随包 node.exe 架构错误:期望 ${{ matrix.pe }},实际 $actual —— 严禁发布该包"
|
||
}
|
||
|
||
- name: 打包安装包(${{ matrix.bundles }})
|
||
working-directory: apps/console
|
||
# beforeBuildCommand(vite build)由 tauri 自己触发;资源靠 --config 注入,
|
||
# 默认 conf 里绝不出现 runtime 路径(bundle.resources 是编译期契约,声明了不存在的文件 build.rs 会直接失败)
|
||
run: pnpm tauri build --config src-tauri/tauri.bundle.gen.json --bundles ${{ matrix.bundles }}
|
||
|
||
- name: 验证产物自举
|
||
# 真 spawn 产物内的 node 去跑产物内的 sidecar,走一次 JSON-RPC
|
||
# (这在 macOS 上抓出过 strip 破坏签名导致 SIGKILL 的问题,Windows 侧同样值得钉住)
|
||
run: node apps/console/scripts/verify-bundle.mjs --runtime
|
||
|
||
# ⚠️ 断言的是**最终交付物**,不是中间产物。
|
||
#
|
||
# 这一版是本步的第二次实现。首版写成「安装包体积 > 50MB」,理由是
|
||
# 「含 Node 运行时(约 80MB),故不可能是个小文件」—— 这个推理把
|
||
# **未压缩的目录形态**(.app ≈ 92MB)当成了**压缩安装包的体积**。
|
||
# 2026-10-11 首个 run 两个 job 双双死在这一行:实测 msi 34.2MB、
|
||
# nsis 19.5~23.2MB(LZMA 下 node.exe 自身就能压到 1/3~1/4),阈值必然误杀。
|
||
#
|
||
# 更根本的问题是:**体积阈值属于「中间逻辑自洽」式断言** —— 包大
|
||
# 不等于 node.exe 真在里面,包小也不等于没在里面(压缩率会随版本漂)。
|
||
# 故改为**解开包、查内容**,一次钉死三件事:
|
||
# ① 三个随包资源(node.exe / sidecar.mjs / agent.mjs)必须都在 ——
|
||
# 缺任一,装到真机上就是「应用能开、但托不起被控端」;
|
||
# ② node.exe 的 PE Machine 必须等于目标架构 —— 最终产物的架构断言,
|
||
# 直接对治「arm64 包里装的是 x64 node.exe」那类只在真机暴露的事故;
|
||
# ③ node.exe 解压后体积必须 > 40MB —— 挡住「塞了个占位文件」。
|
||
# 附带收益:真装一遍本身就是安装器冒烟(比只读目录更强)。
|
||
- name: 断言安装包内容(随包 node.exe 齐全且架构正确)
|
||
shell: pwsh
|
||
# 静默安装理论上会自行结束;给个上限,免得万一卡住把 job 拖满 60 分钟
|
||
timeout-minutes: 8
|
||
run: |
|
||
$ErrorActionPreference = 'Stop'
|
||
$expect = '${{ matrix.pe }}'
|
||
$bundle = 'apps/console/src-tauri/target/release/bundle'
|
||
|
||
function Get-PeMachine([string]$path) {
|
||
$fs = [System.IO.File]::OpenRead($path)
|
||
try {
|
||
$br = [System.IO.BinaryReader]::new($fs)
|
||
$fs.Position = 0x3C
|
||
$peOff = $br.ReadInt32()
|
||
$fs.Position = $peOff
|
||
if ($br.ReadUInt32() -ne 0x00004550) { throw "不是 PE 文件:$path" }
|
||
return '0x{0:x4}' -f $br.ReadUInt16()
|
||
} finally { $fs.Dispose() }
|
||
}
|
||
|
||
# 在候选目录里找出真正装了 node.exe 的那个(安装位置随安装形态而异)
|
||
function Find-PayloadRoot([string[]]$candidates) {
|
||
foreach ($c in $candidates) {
|
||
if (-not (Test-Path $c)) { continue }
|
||
$hit = Get-ChildItem -Path $c -Recurse -Filter 'node.exe' -ErrorAction SilentlyContinue |
|
||
Select-Object -First 1
|
||
if ($hit) { return $c }
|
||
}
|
||
return $null
|
||
}
|
||
|
||
# 安装器可能「自己先退出、再由子进程继续装」,故给它一段收敛时间
|
||
function Wait-PayloadRoot([string[]]$candidates, [int]$seconds = 30) {
|
||
$deadline = (Get-Date).AddSeconds($seconds)
|
||
do {
|
||
$r = Find-PayloadRoot $candidates
|
||
if ($r) { return $r }
|
||
Start-Sleep -Seconds 1
|
||
} while ((Get-Date) -lt $deadline)
|
||
return $null
|
||
}
|
||
|
||
# 解开后的目录里必须三样齐全,且 node.exe 架构要对
|
||
function Assert-Payload([string]$root, [string]$label) {
|
||
$node = Get-ChildItem -Path $root -Recurse -Filter 'node.exe' -ErrorAction SilentlyContinue |
|
||
Select-Object -First 1
|
||
$side = Get-ChildItem -Path $root -Recurse -Filter 'sidecar.mjs' -ErrorAction SilentlyContinue |
|
||
Select-Object -First 1
|
||
$agent = Get-ChildItem -Path $root -Recurse -Filter 'agent.mjs' -ErrorAction SilentlyContinue |
|
||
Select-Object -First 1
|
||
$listing = (Get-ChildItem -Path $root -Recurse -File -ErrorAction SilentlyContinue |
|
||
Select-Object -First 40 -ExpandProperty FullName) -join "`n"
|
||
if (-not $side) { throw "$label 内未找到 sidecar.mjs`n实际内容:`n$listing" }
|
||
if (-not $agent) {
|
||
throw "$label 内未找到 agent.mjs —— 被控端未随包,装上后启动不了本机被控端`n实际内容:`n$listing"
|
||
}
|
||
if (-not $node) {
|
||
throw "$label 内未找到 node.exe —— 随包 Node 运行时未打入,装上后一启动就找不到 node`n实际内容:`n$listing"
|
||
}
|
||
$mb = [math]::Round($node.Length / 1MB, 1)
|
||
$machine = Get-PeMachine $node.FullName
|
||
Write-Host " node.exe $mb MB PE Machine=$machine"
|
||
Write-Host " sidecar.mjs $([math]::Round($side.Length / 1KB)) KB"
|
||
Write-Host " agent.mjs $([math]::Round($agent.Length / 1KB)) KB"
|
||
if ($mb -lt 40) { throw "$label 内 node.exe 仅 $mb MB —— 体积异常,疑似占位文件" }
|
||
if ($machine -ne $expect) {
|
||
throw "$label 内 node.exe 架构错误:期望 $expect,实际 $machine —— 严禁发布该包"
|
||
}
|
||
}
|
||
|
||
$msis = @(Get-ChildItem -Path "$bundle/msi/*.msi" -ErrorAction SilentlyContinue)
|
||
$nsis = @(Get-ChildItem -Path "$bundle/nsis/*.exe" -ErrorAction SilentlyContinue)
|
||
if (($msis.Count + $nsis.Count) -eq 0) { throw "未产出任何安装包($bundle 下为空)" }
|
||
|
||
$checked = 0
|
||
|
||
foreach ($m in $msis) {
|
||
Write-Host "── MSI:$($m.Name) $([math]::Round($m.Length / 1MB, 1)) MB"
|
||
# 管理安装(/a)= 只解包不注册,正合此处用途
|
||
$dest = Join-Path $PWD 'pkg-extract/msi'
|
||
Remove-Item -Recurse -Force $dest -ErrorAction SilentlyContinue
|
||
New-Item -ItemType Directory -Force -Path $dest | Out-Null
|
||
$log = Join-Path $PWD 'msi-extract.log'
|
||
# ⚠️ 必须用 Start-Process -Wait -PassThru 取退出码,**不能**用 `& msiexec.exe`:
|
||
# msiexec 是 GUI 子系统程序,脚本里 `&` 调它**既不等待、也不设 `$LASTEXITCODE`**
|
||
# (实测:整个步骤 0.8 秒就跑到下一行,`$LASTEXITCODE` 为空 → `$null -ne 0` 判真 → 当场抛错)。
|
||
# 参数拼成**单个字符串**传递,避免 -ArgumentList 数组被 PowerShell 自动加引号而破坏 /D= 语义。
|
||
$argLine = "/a `"$($m.FullName)`" /qn TARGETDIR=`"$dest`" /l*v `"$log`""
|
||
$p = Start-Process -FilePath 'msiexec.exe' -ArgumentList $argLine -Wait -PassThru
|
||
Write-Host " msiexec 退出码 = $($p.ExitCode)"
|
||
# 3010 = 成功但需重启,不是失败
|
||
if ($p.ExitCode -ne 0 -and $p.ExitCode -ne 3010) {
|
||
$tail = (Get-Content $log -Tail 30 -ErrorAction SilentlyContinue) -join "`n"
|
||
throw "msiexec /a 失败(退出码 $($p.ExitCode)):$($m.Name)`n$tail"
|
||
}
|
||
$root = Wait-PayloadRoot @($dest)
|
||
if (-not $root) { throw "MSI 管理安装后未在 $dest 下找到 node.exe" }
|
||
Assert-Payload $root $m.Name
|
||
$checked++
|
||
}
|
||
|
||
foreach ($n in $nsis) {
|
||
Write-Host "── NSIS:$($n.Name) $([math]::Round($n.Length / 1MB, 1)) MB"
|
||
# /S 静默、/D 指定目录:两者都由 NSIS **运行时自身**在 .onInit 之前解析
|
||
# (核心参数,不依赖 Tauri 模板),故对任何 NSIS 安装包都成立。
|
||
# /D= 必须是最后一个参数且**不加引号**,故这里用仓库目录下的相对路径
|
||
# (不含空格),而不是通常带空格的用户目录。
|
||
$dest = Join-Path $PWD 'pkg-extract/nsis'
|
||
Remove-Item -Recurse -Force $dest -ErrorAction SilentlyContinue
|
||
New-Item -ItemType Directory -Force -Path $dest | Out-Null
|
||
# 同 MSI:安装器也是 GUI 子系统程序,必须 Start-Process -Wait 才拿得到退出码。
|
||
# 单个字符串传参 → `/D=` 保持原样不加引号(NSIS 的硬要求)。
|
||
$p = Start-Process -FilePath $n.FullName -ArgumentList "/S /D=$dest" -Wait -PassThru
|
||
Write-Host " 安装器退出码 = $($p.ExitCode)"
|
||
if ($p.ExitCode -ne 0 -and $p.ExitCode -ne 3010) {
|
||
throw "NSIS 静默安装失败(退出码 $($p.ExitCode)):$($n.Name)"
|
||
}
|
||
# /D= 理论上一律生效,但若模板在 .onInit 里改写了 $INSTDIR 就未必;
|
||
# 故把默认安装位置一并列为候选,避免「装成功了却因为找不到目录而误报」。
|
||
$root = Wait-PayloadRoot @(
|
||
$dest,
|
||
(Join-Path $env:LOCALAPPDATA 'nodeagent-console'),
|
||
(Join-Path $env:LOCALAPPDATA 'Programs/nodeagent-console'),
|
||
(Join-Path $env:ProgramFiles 'nodeagent-console')
|
||
)
|
||
if (-not $root) {
|
||
throw "NSIS 静默安装后未找到随包 node.exe —— 已查:$dest 与默认安装位置"
|
||
}
|
||
Write-Host " 安装位置:$root"
|
||
Assert-Payload $root $n.Name
|
||
$checked++
|
||
}
|
||
|
||
Write-Host ""
|
||
Write-Host "✓ 已核对 $checked 个安装包:node.exe / sidecar.mjs / agent.mjs 齐全,node.exe 架构 = $expect"
|
||
|
||
- name: 上传安装包
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: nodeagent-console-windows-${{ matrix.arch }}
|
||
path: |
|
||
apps/console/src-tauri/target/release/bundle/msi/*.msi
|
||
apps/console/src-tauri/target/release/bundle/nsis/*.exe
|
||
# 一条架构没产出就报错,避免「jobs 全绿但产物不全」
|
||
if-no-files-found: error
|
||
# 交付流水线 §4:CI artifact 公开可下载,留存越久暴露面越大,30 → 7
|
||
retention-days: 7
|
||
|
||
# 发布:tag 推送时把两个架构的安装包补挂到 release.mjs 建好的 Release 上。
|
||
#
|
||
# 为什么必须有这一步(2026-10-17 真机教训)
|
||
# --------------------------------------
|
||
# `scripts/release.mjs` 只产出 agent / cli / mcp,控制台的 NSIS/MSI
|
||
# **从未进入过 Release** —— 后果是:用户从 Release 页面下载到的永远是旧控制台,
|
||
# 而当天修的 6 个真机缺陷只存在于 CI artifact(retention 7 天,到期自动删除)。
|
||
# 一句话:**修复发出去了,但没人拿得到。**
|
||
publish:
|
||
name: 补挂安装包到 Release
|
||
# 只有 tag 才发布;分支构建(日常验证)一律跳过
|
||
if: startsWith(github.ref, 'refs/tags/v')
|
||
needs: build
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 20
|
||
permissions:
|
||
contents: write
|
||
steps:
|
||
- name: 下载两个架构的安装包
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
path: dist
|
||
pattern: nodeagent-console-windows-*
|
||
|
||
- name: 列出待发布文件(先看清再上传)
|
||
run: |
|
||
set -euo pipefail
|
||
echo "--- dist 实际内容 ---"
|
||
find dist -type f | sort | head -40
|
||
echo "--- 安装包 ---"
|
||
find dist -type f \( -name '*.exe' -o -name '*.msi' \) -printf '%10s %p\n' | sort -k2
|
||
|
||
# 期望值必须与上面的 build 矩阵一致:
|
||
# x64 → msi + nsis(2 个)
|
||
# arm64 → 仅 nsis(1 个,WiX 的 arm64 MSI 支持面窄,刻意不出)
|
||
# 合计 3 个。
|
||
#
|
||
# ⚠️ 这里曾写死「两架构各一套(exe+msi)= 4 个」,与矩阵漂移后**每次必红** ——
|
||
# 红门禁等于没有门禁(本项目反复踩的同一类坑:断言与实现各改各的)。
|
||
# 故改为**按架构**断言,并在缺失时报出具体是哪个架构,便于一眼定位。
|
||
fail=0
|
||
for spec in "x64:2" "arm64:1"; do
|
||
arch=${spec%%:*}; want=${spec##*:}
|
||
dir="dist/nodeagent-console-windows-$arch"
|
||
# ⚠️ 变量引用一律用 ${var} 形式:`$dir)` 这种「$var + 全角括号」在
|
||
# set -u 下会被 bash 当成变量名的一部分 → "unbound variable",
|
||
# 而写法在本地肉眼完全正常(本地实测复现过)。
|
||
if [ ! -d "${dir}" ]; then
|
||
echo "::error::缺少 ${arch} 的产物目录(${dir})—— 该架构构建失败或 artifact 名称变更"
|
||
fail=1
|
||
continue
|
||
fi
|
||
got=$(find "${dir}" -type f \( -name '*.exe' -o -name '*.msi' \) | wc -l)
|
||
echo " ${arch}: ${got} / ${want}"
|
||
if [ "${got}" -lt "${want}" ]; then
|
||
echo "::error::${arch} 安装包不足(${got} < ${want})—— 产物缺失,或矩阵已变更但本断言未同步"
|
||
fail=1
|
||
fi
|
||
done
|
||
[ "${fail}" -eq 0 ] || exit 1
|
||
echo "✓ 两架构安装包齐全"
|
||
|
||
- name: 挂到对应 Release
|
||
env:
|
||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
TAG: ${{ github.ref_name }}
|
||
run: |
|
||
set -euo pipefail
|
||
# release.mjs 在本地跑、与本工作流并行,故先等 Release 出现(最多 10 分钟)
|
||
for i in $(seq 1 60); do
|
||
if gh release view "$TAG" >/dev/null 2>&1; then echo "✓ Release $TAG 已存在"; break; fi
|
||
echo "等待 Release $TAG 创建… ($i/60)"; sleep 10
|
||
done
|
||
if ! gh release view "$TAG" >/dev/null 2>&1; then
|
||
echo "::warning::Release $TAG 尚未创建,安装包仅留在 CI artifact。请先跑 scripts/release.mjs"
|
||
exit 0
|
||
fi
|
||
find dist -type f \( -name '*.exe' -o -name '*.msi' \) -print0 \
|
||
| xargs -0 gh release upload "$TAG" --clobber
|
||
echo "✓ 已挂上的资产:"
|
||
gh release view "$TAG" --json assets -q '.assets[].name'
|