diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 643cb9a5..f3903fbe 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -2,11 +2,11 @@ ## 文件总览 -| 文件 | 触发时机 | 职责 | -|---|---|---| -| `pre-publish-branch-ci-check.yml` | push / PR → dev · release · bugfix | 构建 + 单元测试 | -| `branch-protection.yml` | PR → main | 校验源分支合法性 | -| `auto-version-release.yml` | PR 合并到 main | 版本递增 · 打包 · 冒烟测试 · 发布 | +| 文件 | 触发时机 | 职责 | +| ----------------------------------- | ------------------------------------- | ------------------------------------ | +| `pre-publish-branch-ci-check.yml` | push / PR → dev · release · bugfix | 构建 + 单元测试 | +| `branch-protection.yml` | PR → main | 校验源分支合法性 | +| `auto-version-release.yml` | PR 合并到 main | 版本递增 · 打包 · 冒烟测试 · 发布 | --- @@ -35,6 +35,7 @@ 对预发布分支进行持续集成,确保代码在合并到 main 之前质量达标。 **执行内容:** + 1. 构建整个解决方案(Release 配置) 2. 并行运行 4 项单元测试:Core · UI · ML · Shader 3. 汇总测试结果,写入 commit status `ci-check/pre-publish-tests` @@ -50,18 +51,27 @@ 只允许 `dev`、`release`、`bugfix` 三个分支向 main 发起 PR,其他分支一律拦截。 -| 源分支 | 版本变化 | 说明 | -|---|---|---| -| `release` | Minor +1 | 0.4.x.x → 0.5.0.0 | -| `dev` | Build +1 | 0.5.2.x → 0.5.3.0 | -| `bugfix` | Revision +1 | 0.5.3.0 → 0.5.3.1 | +| 源分支 | 版本变化 | 说明 | +| ----------- | ----------- | ------------------ | +| `release` | Minor +1 | 0.4.x.x → 0.5.0.0 | +| `dev` | Build +1 | 0.5.2.x → 0.5.3.0 | +| `bugfix` | Revision +1 | 0.5.3.0 → 0.5.3.1 | --- ## 工作流三:自动发布 > **文件:** `auto-version-release.yml` -> **触发:** PR 成功合并到 `main` +> **触发:** PR 成功合并到 `main`(标题不含 `[skip ci]`) + +### 更新类型判定 + +工作流根据 PR 标签自动区分两种更新类型: + +| 类型 | 判定条件 | 版本号变化 | +| ------------------------ | --------------------------- | ------------------------------------------------------------ | +| **常规安装式更新** | 无 `update-plugin-*` 标签 | 按分支递增(feature→minor, daily→build, bugfix→revision) | +| **重启式更新** | 含 `update-plugin-*` 标签 | 版本号不变,打包插件 | ### 执行流程 @@ -75,15 +85,23 @@ build package │ InnoSetup 编译安装包,上传为 Artifact(保留 30 天) ▼ -smoke_test ← 失败则终止并回滚 - │ 静默安装 → 启动主程序 → 等待 UI 被拉起(最多 20s) +smoke_test + │ 静默安装 → 启动主程序 → 等待 UI 被拉起(最多 10s) │ 若未自动拉起:兜底手动启动 UI │ 再次尝试启动 UI → 断言只有 1 个 UI 进程(单例验证) + │ 验证 UI 无法在无主进程时独立运行(MessageBox 守卫) + ▼ + ┌─────────────────────────────────────────────────────────────┐ + │ 以下阶段根据更新类型和测试结果有条件执行(见下表) │ + └─────────────────────────────────────────────────────────────┘ + ▼ +package_restart_update (仅重启式更新) + │ 打包插件 zip + manifest,上传为 Artifact ▼ -bump +bump (仅常规安装式更新) │ 更新 csproj / InnoSetup / README badge 版本号并提交到 main ▼ -sync +sync (仅常规安装式更新) │ 将版本文件同步到 release / dev / bugfix 分支 ▼ summary @@ -93,11 +111,34 @@ release(草稿) 创建 Draft GitHub Release - Tag 和标题:v{版本号} - 正文:本次 PR 的描述 - - 附件:安装包 exe + - 附件:安装包 exe(+ 插件包,如有) - 状态:草稿,不公开,等待人工验证后发布 ``` -> 任意步骤失败时,`rollback` job 自动触发,对 main 及三个预发布分支执行 `git revert`。 +### 各阶段执行条件 + +| 阶段 | 常规安装式更新 | 重启式更新 | +| -------------------------- | ------------------------ | ------------------------- | +| `prepare` | ✅ 始终执行 | ✅ 始终执行 | +| `build` | ✅ 始终执行 | ✅ 始终执行 | +| `package` | ✅ 始终执行 | ✅ 始终执行 | +| `smoke_test` | ✅ 始终执行 | ✅ 始终执行 | +| `package_restart_update` | ❌ 始终跳过 | ✅ smoke_test 通过时执行 | +| `bump` | ✅ smoke_test 通过时执行 | ❌ 始终跳过 | +| `sync` | ✅ bump 成功时执行 | ❌ 始终跳过 | +| `summary` | ✅ 全部成功时执行 | ✅ 全部成功时执行 | +| `release` | ✅ summary 成功时执行 | ✅ summary 成功时执行 | +| `rollback` | ✅ sync 阶段失败时触发 | ❌ 始终跳过(无版本同步) | + +### 失败处理 + +| 失败阶段 | 常规安装式更新 | 重启式更新 | +| -------------------------- | --------------------------------------------------------------- | -------------------------------- | +| `build` / `package` | 流程终止,无后续操作 | 流程终止,无后续操作 | +| `smoke_test` | 流程终止,不触发版本同步和 draft | 流程终止,不触发插件打包和 draft | +| `bump` | 流程终止,不触发 sync | — | +| `sync` | **触发 rollback**,回滚 main + 三个预发布分支到 CI 前 SHA | — | +| `package_restart_update` | — | 流程终止,不触发 draft | --- diff --git a/.github/workflows/auto-version-release.yml b/.github/workflows/auto-version-release.yml index 5d50124c..1c5d9c45 100644 --- a/.github/workflows/auto-version-release.yml +++ b/.github/workflows/auto-version-release.yml @@ -22,7 +22,7 @@ jobs: # Job 1: 准备阶段 - 记录 SHA、确定分支、计算新版本号 # ───────────────────────────────────────────────────────────── prepare: - name: Prepare - Calculate Version & Record SHAs + name: Prepare - Version & Metadata runs-on: windows-latest if: | github.event.pull_request.merged == true && @@ -40,6 +40,11 @@ jobs: release_sha: ${{ steps.shas.outputs.release_sha }} pr_title: ${{ steps.pr_info.outputs.pr_title }} pr_body: ${{ steps.pr_info.outputs.pr_body }} + has_plugin_update: ${{ steps.version.outputs.has_plugin_update }} + update_plugins: ${{ steps.version.outputs.update_plugins }} + build_number: ${{ steps.version.outputs.build_number }} + release_tag: ${{ steps.version.outputs.release_tag }} + previous_tag: ${{ steps.prev_tag.outputs.previous_tag }} steps: - name: Checkout with full history @@ -53,19 +58,20 @@ jobs: shell: powershell run: | $mainSha = git rev-parse HEAD - Write-Output "main_sha=$mainSha" >> $env:GITHUB_OUTPUT + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "main_sha=$mainSha`n", $utf8NoBom) Write-Host "main SHA: $mainSha" $devSha = git rev-parse origin/dev - Write-Output "dev_sha=$devSha" >> $env:GITHUB_OUTPUT + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "dev_sha=$devSha`n", $utf8NoBom) Write-Host "dev SHA: $devSha" $bugfixSha = git rev-parse origin/bugfix - Write-Output "bugfix_sha=$bugfixSha" >> $env:GITHUB_OUTPUT + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "bugfix_sha=$bugfixSha`n", $utf8NoBom) Write-Host "bugfix SHA: $bugfixSha" $releaseSha = git rev-parse origin/release - Write-Output "release_sha=$releaseSha" >> $env:GITHUB_OUTPUT + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "release_sha=$releaseSha`n", $utf8NoBom) Write-Host "release SHA: $releaseSha" - name: Determine source branch and version increment @@ -90,12 +96,30 @@ jobs: Write-Host "Unknown source branch: $sourceBranch - defaulting to patch" } - Write-Output "source_branch=$sourceBranch" >> $env:GITHUB_OUTPUT - Write-Output "increment_type=$incrementType" >> $env:GITHUB_OUTPUT + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "source_branch=$sourceBranch`n", $utf8NoBom) + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "increment_type=$incrementType`n", $utf8NoBom) - - name: Calculate new version + - name: Get previous tag + id: prev_tag + shell: powershell + run: | + # Use for-each-ref sorted by creatordate — not limited to ancestor chain + $prevTag = git for-each-ref --sort=-creatordate --format="%(refname:short)" refs/tags/ | Select-Object -First 1 + if (-not $prevTag) { + Write-Host "No previous tag found, this is the first release" + $prevTag = "" + } + Write-Host "Previous tag: $prevTag" + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "previous_tag=$prevTag`n", $utf8NoBom) + + - name: Prepare release metadata id: version shell: powershell + env: + PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }} + PREV_TAG: ${{ steps.prev_tag.outputs.previous_tag }} run: | $csprojPath = "src/VirtualPaper/VirtualPaper.csproj" $content = Get-Content $csprojPath -Raw @@ -112,28 +136,99 @@ jobs: $revision = [int]$versionMatch.Groups[4].Value $currentVersion = "$major.$minor.$build.$revision" + $currentVersionText = "${major}_${minor}_${build}_${revision}" + + # ── Detect plugin update labels ────────────────────────────── + $labels = "$env:PR_LABELS" -split ',' | ForEach-Object { $_.Trim() } + $isPluginUpdate = $false + $plugins = @() - $incrementType = "${{ steps.branch_info.outputs.increment_type }}" - if ($incrementType -eq "feature") { - $minor = $minor + 1 # 第2位 +1 - $build = 0 # 第3位 清零 - $revision = 0 # 第4位 清零 - } elseif ($incrementType -eq "minor") { - $build = $build + 1 # 第3位 +1 - $revision = 0 # 第4位 清零 + if ($labels -contains "update-plugins") { + $isPluginUpdate = $true + $plugins = @("UI", "PlayerWeb", "ScrSaver", "ML", "Shaders") } else { - $revision = $revision + 1 # 第4位 +1 + if ($labels -contains "update-plugin-ui") { $isPluginUpdate = $true; $plugins += "UI" } + if ($labels -contains "update-plugin-playerweb") { $isPluginUpdate = $true; $plugins += "PlayerWeb" } + if ($labels -contains "update-plugin-scrsaver") { $isPluginUpdate = $true; $plugins += "ScrSaver" } + if ($labels -contains "update-plugin-ml") { $isPluginUpdate = $true; $plugins += "ML" } + if ($labels -contains "update-plugin-shaders") { $isPluginUpdate = $true; $plugins += "Shaders" } + } + + # ── Calculate version ──────────────────────────────────────── + if ($isPluginUpdate) { + Write-Host "Plugin update ($($plugins -join ', ')) - version unchanged: $currentVersion" + } else { + Write-Host "Installer-only release - no plugin updates" + $incrementType = "${{ steps.branch_info.outputs.increment_type }}" + if ($incrementType -eq "feature") { + $minor = $minor + 1 # 第2位 +1 + $build = 0 # 第3位 清零 + $revision = 0 # 第4位 清零 + } elseif ($incrementType -eq "minor") { + $build = $build + 1 # 第3位 +1 + $revision = 0 # 第4位 清零 + } else { + $revision = $revision + 1 # 第4位 +1 + } } $newVersion = "$major.$minor.$build.$revision" # 使用下划线分隔,避免各组件 >= 10 时产生歧义(如 0.10.0.0 → 01000 与 0.1.0.0.0 冲突) $newVersionText = "${major}_${minor}_${build}_${revision}" - Write-Host "Version: $currentVersion -> $newVersion" - Write-Output "current_version=$currentVersion" >> $env:GITHUB_OUTPUT - Write-Output "new_version=$newVersion" >> $env:GITHUB_OUTPUT - Write-Output "new_version_text=$newVersionText" >> $env:GITHUB_OUTPUT + # ── Generate build number: YYMM + R + xx (每月重置) ───────── + # 从 previous tag 解码上一次 build number,同月 +1,跨月重置 + $now = Get-Date + $yearMonth = $now.ToString("yyMM") + + $prevBuildNum = "" + $prevTag = "$env:PREV_TAG" + if ($prevTag -match 'plugin([A-Z]+)$') { + $encoded = $Matches[1] + $prevBuildNum = ($encoded.ToCharArray() | ForEach-Object { + if ($_ -ge 'A' -and $_ -le 'J') { [char]([int][char]'0' + ([int]$_ - [int][char]'A')) } else { $_ } + }) -join '' + } + + $buildSeq = 1 + if ($prevBuildNum -match "^(\d{4})R(\d{2})$") { + $prevYM = $Matches[1] + $prevSeq = [int]$Matches[2] + if ($prevYM -eq $yearMonth) { + $buildSeq = $prevSeq + 1 + } + } + + $buildNumber = "${yearMonth}R{0:D2}" -f $buildSeq + Write-Host "Build number: $buildNumber (prev: $prevBuildNum, seq: $buildSeq)" + + # ── Generate release tag ───────────────────────────────────── + # Plugin 更新:tag 后缀用字母编码 build number,保证旧客户端正则兼容 + # 数字 0-9 → A-J,字母原样保留 + # 例:2607R01 → CGAHRAAB + # tag:v0.4.1.0pluginCGAHRAAB → 正则后 0.4.1.0 + # 常规更新:tag 为 v{version} + if ($isPluginUpdate) { + $encoded = ($buildNumber.ToCharArray() | ForEach-Object { + if ($_ -ge '0' -and $_ -le '9') { [char]([int][char]'A' + ([int]$_ - [int][char]'0')) } else { $_ } + }) -join '' + $releaseTag = "v${newVersion}plugin${encoded}" + } else { + $releaseTag = "v${newVersion}" + } + Write-Host "Release tag: $releaseTag" + + # ── Outputs ────────────────────────────────────────────────── + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "current_version=$currentVersion`n", $utf8NoBom) + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "current_version_text=$currentVersionText`n", $utf8NoBom) + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "new_version=$newVersion`n", $utf8NoBom) + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "new_version_text=$newVersionText`n", $utf8NoBom) + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "has_plugin_update=$($isPluginUpdate.ToString().ToLower())`n", $utf8NoBom) + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "update_plugins=$($plugins -join ';')`n", $utf8NoBom) + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "build_number=$buildNumber`n", $utf8NoBom) + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "release_tag=$releaseTag`n", $utf8NoBom) - name: Capture PR title and body id: pr_info @@ -200,6 +295,25 @@ jobs: - name: Rebuild solution run: msbuild src/VirtualPaper.sln /t:Rebuild /p:Configuration=Release /p:Platform="Any CPU" /restore /m + - name: Generate app_build.json + shell: pwsh + run: | + $buildNumber = "${{ needs.prepare.outputs.build_number }}" + $outDir = "src/VirtualPaper/bin/Release/net8.0-windows10.0.19041.0" + $json = @{ + app_build = $buildNumber + plugins = @{ + UI = $buildNumber + PlayerWeb = $buildNumber + ScrSaver = $buildNumber + ML = $buildNumber + Shaders = $buildNumber + } + } | ConvertTo-Json -Depth 3 + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::WriteAllText("$outDir/app_build.json", $json, $utf8NoBom) + Write-Host "[OK] Generated app_build.json with build $buildNumber" + - name: Validate Build Artifacts # 在产物就地校验:主 exe / Plugin exe / 核心 dll 存在且非空。 # 通过后才上传 artifact,确保 package / smoke_test job 拿到的是健康的构建产物。 @@ -523,13 +637,131 @@ jobs: } Write-Host "[OK] Standalone-UI guard test passed" + # ───────────────────────────────────────────────────────────── + # Job 4.5: 制作插件更新包(plugin zips + manifest) + # 仅在冒烟测试通过且 PR 有 plugin-packages 标签时执行 + # ───────────────────────────────────────────────────────────── + package_plugin_update: + name: Create Plugin Packages + runs-on: windows-latest + needs: [prepare, build, smoke_test] + if: needs.prepare.outputs.has_plugin_update == 'true' + + steps: + - name: Download build output + uses: actions/download-artifact@v4 + with: + name: build-release-output + path: src/ + + - name: Create Plugin packages + shell: pwsh + run: | + $buildDir = "src/VirtualPaper/bin/Release/net8.0-windows10.0.19041.0" + $pluginsDir = "$buildDir/Plugins" + $outputDir = "plugin-packages" + New-Item -ItemType Directory -Path $outputDir -Force | Out-Null + + $buildNumber = "${{ needs.prepare.outputs.build_number }}" + $plugins = "${{ needs.prepare.outputs.update_plugins }}" -split ';' | Where-Object { $_ -ne "" } + + # Create each plugin zip containing only the plugin folder + foreach ($plugin in $plugins) { + $pluginDir = "$pluginsDir/$plugin" + if (Test-Path $pluginDir) { + $pluginLower = $plugin.ToLower() + $zipName = "plugin-$pluginLower-$buildNumber.zip" + $zipPath = "$outputDir/$zipName" + # Zip the folder itself (not just contents) + Compress-Archive -Path $pluginDir -DestinationPath $zipPath -Force + Write-Host "[OK] Created $zipName" + } else { + Write-Warning "Plugin directory not found: $pluginDir" + } + } + + # Generate update_manifest.json with zip-level SHA256 + # 使用 System.Text.Json 的 Utf8JsonWriter 替代 ConvertTo-Json, + # 避免 PowerShell 5.1 默认缩进随嵌套深度爆炸的问题 + $ms = [System.IO.MemoryStream]::new() + $writer = [System.Text.Json.Utf8JsonWriter]::new($ms, [System.Text.Json.JsonWriterOptions]@{ + Indented = $true + }) + + $writer.WriteStartObject() + $writer.WriteString("type", "plugins") + $writer.WriteString("app_build", $buildNumber) + $writer.WriteString("min_app_build", $buildNumber) + $writer.WritePropertyName("plugins") + $writer.WriteStartObject() + + foreach ($plugin in $plugins) { + $pluginLower = $plugin.ToLower() + $zipName = "plugin-$pluginLower-$buildNumber.zip" + $zipPath = "$outputDir/$zipName" + if (Test-Path $zipPath) { + $hash = (Get-FileHash -Path $zipPath -Algorithm SHA256).Hash.ToLower() + $writer.WritePropertyName($plugin) + $writer.WriteStartObject() + $writer.WriteString("build", $buildNumber) + $writer.WriteString("asset", $zipName) + $writer.WriteString("sha256", $hash) + $writer.WriteEndObject() + } + } + + $writer.WriteEndObject() + $writer.WriteEndObject() + $writer.Flush() + + $manifestJson = [System.Text.Encoding]::UTF8.GetString($ms.ToArray()) + $writer.Dispose() + $ms.Dispose() + + # Use UTF-8 without BOM to match client-side hash computation + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::WriteAllText("$outputDir/plugin-manifest.json", $manifestJson, $utf8NoBom) + Write-Host "[OK] Generated plugin-manifest.json" + Write-Host $manifestJson + + # Generate PLUGIN_MANIFEST_SHA256.txt for manifest verification + $manifestHash = (Get-FileHash -Path "$outputDir/plugin-manifest.json" -Algorithm SHA256).Hash.ToLower() + [System.IO.File]::WriteAllText("$outputDir/PLUGIN_MANIFEST_SHA256.txt", $manifestHash, $utf8NoBom) + Write-Host "[OK] Generated PLUGIN_MANIFEST_SHA256.txt: $manifestHash" + + # Generate app_build.json for installation root + $appBuildJson = @{ + app_build = $buildNumber + plugins = @{ + UI = $buildNumber + PlayerWeb = $buildNumber + ScrSaver = $buildNumber + ML = $buildNumber + Shaders = $buildNumber + } + } | ConvertTo-Json -Depth 3 + [System.IO.File]::WriteAllText("$outputDir/app_build.json", $appBuildJson, $utf8NoBom) + Write-Host "[OK] Generated app_build.json" + Write-Host $appBuildJson + + - name: Upload plugin update packages + uses: actions/upload-artifact@v4 + with: + name: VirtualPaper-Plugins-v${{ needs.prepare.outputs.new_version }} + path: plugin-packages/ + retention-days: 30 + # ───────────────────────────────────────────────────────────── # Job 5: 将版本号递增变更提交到 main(冒烟测试通过后才提交) # ───────────────────────────────────────────────────────────── bump: name: Bump Version to main runs-on: windows-latest - needs: [prepare, package, smoke_test] + needs: [prepare, package, smoke_test, package_plugin_update] + if: | + always() && + needs.prepare.outputs.has_plugin_update != 'true' && + needs.smoke_test.result == 'success' steps: - name: Checkout main with full history @@ -589,6 +821,9 @@ jobs: name: Sync Version to Pre-publish Branches runs-on: windows-latest needs: [prepare, bump] + if: | + always() && + needs.bump.result == 'success' steps: - name: Checkout with full history @@ -632,12 +867,16 @@ jobs: Write-Host "[OK]" -ForegroundColor Green -NoNewline; Write-Host " $branch fully synced with main" } else { - # 策略 2(fallback):merge 无法自动解决 → 放弃,直接 reset --hard 到 main - # 使用 reset --hard 直接移动分支指针,不产生额外 commit, - # 历史看起来如同这次 merge 从未发生,比 git revert 更干净彻底。 - Write-Host " [WARN]" -ForegroundColor Yellow -NoNewline; Write-Host " merge has unresolvable conflicts, force-resetting to origin/main" + # 策略 2(fallback):merge 无法自动解决 → 备份后 reset --hard 到 main + Write-Host " [WARN]" -ForegroundColor Yellow -NoNewline; Write-Host " merge has unresolvable conflicts, backing up and force-resetting to origin/main" git merge --abort + # 备份当前分支状态到 ${branch}_backup,再 reset + $backupBranch = "${branch}_backup" + git branch -f $backupBranch HEAD + git push --force origin $backupBranch + Write-Host " [BACKUP]" -ForegroundColor Cyan -NoNewline; Write-Host " $branch saved to $backupBranch" + git reset --hard origin/main git push --force-with-lease origin $branch Write-Host "[WARN]" -ForegroundColor Yellow -NoNewline; Write-Host " $branch force-reset to origin/main (unresolvable conflicts overridden)" @@ -650,15 +889,15 @@ jobs: summary: name: Summary runs-on: ubuntu-latest - needs: [prepare, build, package, smoke_test, bump, sync] - if: >- + needs: [prepare, build, package, smoke_test, package_plugin_update, bump, sync] + if: | !cancelled() && needs.prepare.result == 'success' && needs.build.result == 'success' && needs.package.result == 'success' && needs.smoke_test.result == 'success' && - needs.bump.result == 'success' && - needs.sync.result == 'success' + (needs.prepare.outputs.has_plugin_update == 'true' || (needs.bump.result == 'success' && needs.sync.result == 'success')) && + (needs.prepare.outputs.has_plugin_update != 'true' || needs.package_plugin_update.result == 'success') steps: - name: Print summary @@ -675,10 +914,25 @@ jobs: echo "Source : ${{ needs.prepare.outputs.source_branch }} -> main" echo "Type : $TYPE" echo -e "\033[0;36mPackage : ${{ needs.package.outputs.installer_name }}\033[0m" + + if [ "${{ needs.prepare.outputs.has_plugin_update }}" = "true" ]; then + echo -e "\033[0;36mPlugins Update: YES (plugins: ${{ needs.prepare.outputs.update_plugins }}, build: ${{ needs.prepare.outputs.build_number }})\033[0m" + else + echo "Plugins Update: NO (installer only)" + fi + echo "" - echo -e "\033[0;32m[OK]\033[0m Version updated and committed to main" - echo -e "\033[0;32m[OK]\033[0m Version synced to pre-publish branches (release / dev / bugfix)" + if [ "${{ needs.prepare.outputs.has_plugin_update }}" = "true" ]; then + echo -e "\033[0;33m[SKIP]\033[0m Version bump skipped (plugin update - version unchanged)" + echo -e "\033[0;33m[SKIP]\033[0m Branch sync skipped (plugin update)" + else + echo -e "\033[0;32m[OK]\033[0m Version updated and committed to main" + echo -e "\033[0;32m[OK]\033[0m Version synced to pre-publish branches (release / dev / bugfix)" + fi echo -e "\033[0;32m[OK]\033[0m Installer uploaded as Artifact" + if [ "${{ needs.prepare.outputs.has_plugin_update }}" = "true" ]; then + echo -e "\033[0;32m[OK]\033[0m Plugins update packages uploaded as Artifact" + fi echo -e "\033[0;32m[OK]\033[0m Smoke test passed (install + launch + singleton)" echo -e "\033[0;32m[OK]\033[0m Draft GitHub Release will be created: v${{ needs.prepare.outputs.new_version }}" echo -e "\033[0;32m==========================================\033[0m" @@ -689,45 +943,84 @@ jobs: # - 以 PR body 作为 Release 正文 # - Tag / Release 标题均为 "v版本号" # - 将 Installer exe 作为 Release 附件上传 + # - 如有重启式更新包,一并上传 # - 创建为草稿(draft),需人工验证后手动点击发布 # ───────────────────────────────────────────────────────────── release: name: Create Draft GitHub Release runs-on: ubuntu-latest - needs: [prepare, package, summary] - if: needs.summary.result == 'success' + needs: [prepare, build, package, smoke_test, package_plugin_update, bump, sync, summary] + if: | + !cancelled() && + needs.prepare.result == 'success' && + needs.build.result == 'success' && + needs.package.result == 'success' && + needs.smoke_test.result == 'success' && + (needs.prepare.outputs.has_plugin_update == 'true' || (needs.bump.result == 'success' && needs.sync.result == 'success')) && + (needs.prepare.outputs.has_plugin_update != 'true' || needs.package_plugin_update.result == 'success') steps: + - name: Checkout (for tag verification) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Download installer artifact uses: actions/download-artifact@v4 with: name: VirtualPaper-Installer-v${{ needs.prepare.outputs.new_version }} path: dist/ + - name: Download plugin update artifact + if: needs.prepare.outputs.has_plugin_update == 'true' + uses: actions/download-artifact@v4 + with: + name: VirtualPaper-Plugins-v${{ needs.prepare.outputs.new_version }} + path: dist/plugin-packages/ + + - name: Verify tag does not exist + id: tag_check + shell: powershell + run: | + $tag = "${{ needs.prepare.outputs.release_tag }}" + $existing = git tag -l $tag 2>$null + if ($existing) { + Write-Error "Tag '$tag' already exists! Possible concurrent build race. Aborting release." + exit 1 + } + Write-Host "[OK] Tag '$tag' is unique, safe to create" + - name: Create Draft Release & Tag uses: softprops/action-gh-release@v2 with: - tag_name: v${{ needs.prepare.outputs.new_version }} - name: v${{ needs.prepare.outputs.new_version }} + tag_name: ${{ needs.prepare.outputs.release_tag }} + name: ${{ needs.prepare.outputs.has_plugin_update == 'true' && format('v{0} Plugin Update (Build {1})', needs.prepare.outputs.new_version, needs.prepare.outputs.build_number) || format('v{0} (Build {1})', needs.prepare.outputs.new_version, needs.prepare.outputs.build_number) }} body: ${{ needs.prepare.outputs.pr_body }} + previous_tag: ${{ needs.prepare.outputs.previous_tag }} files: | dist/*.exe dist/SHA256.txt + dist/plugin-packages/*.zip + dist/plugin-packages/plugin-manifest.json + dist/plugin-packages/PLUGIN_MANIFEST_SHA256.txt draft: true # 草稿状态,不对外公开,等待人工验证后发布 make_latest: false # 草稿阶段不标记为最新,发布时再生效 token: ${{ secrets.GITHUB_TOKEN }} # ───────────────────────────────────────────────────────────── - # Job 9: 失败回滚(任意 job 失败时执行) + # Job 9: 失败回滚(bump 或 sync 执行后失败时) # ───────────────────────────────────────────────────────────── rollback: name: Rollback on Failure runs-on: windows-latest - needs: [prepare, build, package, smoke_test, bump, sync] - # 触发条件:bump 或 sync 已经开始执行(分支内容已被修改),且流程出现失败或取消。 - # build 失败时 bump/sync 均为 skipped(分支未动),不触发,避免无意义的回滚操作。 + needs: [prepare, build, package, smoke_test, package_plugin_update, bump, sync] + # 触发条件: + # 1. 仅针对常规安装式更新(重启式更新不涉及版本同步,无需回滚) + # 2. bump 或 sync 已经开始执行(分支内容已被修改),且流程出现失败或取消 + # 3. bump 依赖 smoke_test 成功,因此 smoke_test 失败时 bump/sync 均为 skipped,不触发回滚 if: | always() && + needs.prepare.outputs.has_plugin_update != 'true' && needs.prepare.result == 'success' && (needs.bump.result != 'skipped' || needs.sync.result != 'skipped') && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) diff --git a/src/StaticImg/StaticImg.csproj b/src/StaticImg/StaticImg.csproj index ee03a769..a0586899 100644 --- a/src/StaticImg/StaticImg.csproj +++ b/src/StaticImg/StaticImg.csproj @@ -3,7 +3,7 @@ net8.0-windows10.0.19041.0 10.0.17763.0 Workloads.Creation.StaticImg - win-x86;win-x64;win-arm64 + false true true enable diff --git a/src/VirtualPaper.AppSettingsPanel/ViewModels/GeneralSettingViewModel.cs b/src/VirtualPaper.AppSettingsPanel/ViewModels/GeneralSettingViewModel.cs index 82d7294b..972bc785 100644 --- a/src/VirtualPaper.AppSettingsPanel/ViewModels/GeneralSettingViewModel.cs +++ b/src/VirtualPaper.AppSettingsPanel/ViewModels/GeneralSettingViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using System.Windows.Input; using Grpc.Core; @@ -13,8 +14,10 @@ using VirtualPaper.Common.Utils.Storage; using VirtualPaper.Common.Utils.ThreadContext; using VirtualPaper.Grpc.Client.Interfaces; +using VirtualPaper.Models.AppUpdate; using VirtualPaper.Models.Cores; using VirtualPaper.Models.Cores.Interfaces; +using VirtualPaper.Models.Events; using VirtualPaper.Models.Mvvm; using VirtualPaper.UIComponent; using VirtualPaper.UIComponent.Utils; @@ -34,10 +37,39 @@ public string AppVersionText { ver += "b"; else if (Constants.ApplicationType.IsMSIX) ver += $" {LanguageUtil.GetI18n(Constants.I18n.Settings_General_Version_MsStore)}"; + + var appBuild = LoadAppBuildInfo().AppBuild; + if (!string.IsNullOrEmpty(appBuild)) + ver += $" (Build {appBuild})"; + return ver; } } + public List PluginVersionTexts { + get { + var buildInfo = LoadAppBuildInfo(); + return buildInfo.Plugins.Select(kv => $"{kv.Key}: {kv.Value}").ToList(); + } + } + + public bool HasPluginVersions => PluginVersionTexts.Count > 0; + + public Microsoft.UI.Xaml.Visibility PluginVersionsVisibility => + HasPluginVersions ? Microsoft.UI.Xaml.Visibility.Visible : Microsoft.UI.Xaml.Visibility.Collapsed; + + private static AppBuildInfo LoadAppBuildInfo() { + var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, Constants.CoreField.AppBuildFile); + if (!File.Exists(path)) return new AppBuildInfo(); + try { + var json = File.ReadAllText(path); + return JsonSerializer.Deserialize(json, AppBuildInfoContext.Default.AppBuildInfo) ?? new AppBuildInfo(); + } + catch { + return new AppBuildInfo(); + } + } + public List SystemBackdrops { get; set; } = []; public List Languages { get; set; } = []; @@ -71,6 +103,12 @@ public bool IsUpdateBtnEnable { set { _isUpdateBtnEnable = value; OnPropertyChanged(); } } + private bool _isInstallBtnEnable = true; + public bool IsInstallBtnEnable { + get => _isInstallBtnEnable; + set { _isInstallBtnEnable = value; OnPropertyChanged(); } + } + private bool _isUpdateRingActive = false; public bool IsUpdateRingActive { get => _isUpdateRingActive; @@ -154,6 +192,18 @@ public bool IsWallpaperDirectoryChangeEnable { set { _isWallpaperDirectoryChangeEnable = value; OnPropertyChanged(); } } + private string? _text_UpdateReady; + public string? Text_UpdateReady { + get { return _text_UpdateReady; } + set { _text_UpdateReady = value; OnPropertyChanged(); } + } + + private ICommand? _installBtnComand; + public ICommand? InstallBtnComand { + get { return _installBtnComand; } + set { _installBtnComand = value; OnPropertyChanged(); } + } + public ICommand? ChangeFileStorageCommand { get; private set; } public ICommand? OpenFileStorageCommand { get; private set; } public ICommand? CheckUpdateCommand { get; private set; } @@ -162,10 +212,12 @@ public bool IsWallpaperDirectoryChangeEnable { public GeneralSettingViewModel( IAppUpdaterClient appUpdater, IUserSettingsClient userSettingsClient, - IWallpaperControlClient wallpaperControlClient) { + IWallpaperControlClient wallpaperControlClient, + ICommandsClient commandsClient) { _appUpdater = appUpdater; _userSettingsClient = userSettingsClient; _wpControlClient = wallpaperControlClient; + _commandsClient = commandsClient; InitText(); InitCollections(); @@ -220,14 +272,15 @@ private void ChangeAutoShartStatu(bool isAutoStart) { } private async Task CheckUpdateAsync() { + IsInstallBtnEnable = false; IsUpdateBtnEnable = false; IsUpdateRingActive = true; InfoBarVisibilityRestore(); await _appUpdater.CheckUpdateAsync(); - IsUpdateBtnEnable = true; IsUpdateRingActive = false; + IsUpdateBtnEnable = true; } private void InfoBarVisibilityRestore() { @@ -236,32 +289,45 @@ private void InfoBarVisibilityRestore() { private void AppUpdater_UpdateChecked(object? sender, AppUpdaterEventArgs e) { CrossThreadInvoker.InvokeOnUIThread(() => { - MenuUpdate(e.UpdateStatus, e.UpdateDate, e.UpdateVersion); + MenuUpdate(e.UpdateStatus, e.Release); }); } - private void MenuUpdate(AppUpdateStatus status, DateTime date, Version version) { - Version = $"v{version}"; -//#if DEBUG -// CurrentVersionState = VersionState.FindNew; -//#else + private void MenuUpdate(AppUpdateStatus status, ReleaseInfo? release) { + //CurrentVersionState = VersionState.FindNew; + switch (status) { case AppUpdateStatus.Uptodate: CurrentVersionState = VersionState.UptoNewest; break; case AppUpdateStatus.Available: - Version = $"v{version}"; + Version = $"v{release?.Version} Build ({release?.AppBuild})"; CurrentVersionState = VersionState.FindNew; break; + case AppUpdateStatus.InstallerReady: + Text_UpdateReady = LanguageUtil.GetI18n(nameof(Constants.I18n.Settings_General_Version_InstallerReady)); + InstallBtnComand = new RelayCommand(async () => { + + }); + CurrentVersionState = VersionState.InstallReady; + IsInstallBtnEnable = true; + break; + case AppUpdateStatus.PluginsReady: + Text_UpdateReady = LanguageUtil.GetI18n(nameof(Constants.I18n.Settings_General_Version_PluginsReady)); + InstallBtnComand = new RelayCommand(async () => { + await _commandsClient.CloseUI(); + }); + CurrentVersionState = VersionState.InstallReady; + IsInstallBtnEnable = true; + break; case AppUpdateStatus.Invalid or AppUpdateStatus.Error: CurrentVersionState = VersionState.UpdateErr; break; default: break; } -//#endif Version_LastCheckDate = LanguageUtil.GetI18n(Constants.I18n.Settings_General_Version_LastCheckDate); - Version_LastCheckDate += status == AppUpdateStatus.Notchecked ? "" : $" {date}"; + Version_LastCheckDate += status == AppUpdateStatus.Notchecked ? "" : $" {release?.CheckedTime}"; } private async Task StartDownloadAsync() { @@ -335,7 +401,7 @@ internal async Task WallpaperDirectoryChangeAsync(string destRootFolderPath) { GlobalMessageUtil.ShowException(ex); ArcLog.GetLogger().Error(ex.Message); if (destFolderPath != string.Empty) { - FileUtil.EmptyDirectory(destFolderPath); + FileUtil.RemoveDirectory(destFolderPath); } } finally { @@ -376,9 +442,11 @@ internal static async IAsyncEnumerable GetWpBasicDataByInstallFolders WpLibData libData = new(); foreach (string file in files) { if (Path.GetFileName(file) == Constants.Field.WpBasicDataFileName) { - libData.BasicData = await JsonSaver.LoadAsync(file, WpBasicDataContext.Default); + var basicData = await JsonSaver.LoadAsync(file, WpBasicDataContext.Default); + if (basicData == null) continue; - if (libData.BasicData.IsAvailable()) { + libData.BasicData = basicData; + if (libData.BasicData != null && libData.BasicData.IsAvailable()) { libData.Idx = idx++; yield return libData; break; @@ -414,6 +482,7 @@ protected virtual void Dispose(bool disposing) { private readonly IAppUpdaterClient _appUpdater; private readonly IUserSettingsClient _userSettingsClient; private readonly IWallpaperControlClient _wpControlClient; + private readonly ICommandsClient _commandsClient; } public enum VersionState { @@ -424,6 +493,7 @@ public enum VersionState { DownloadFailed, // 下载失败 VerifyFailed, // 校验失败 Downloaded, // 下载完成 - UpdateErr // 网络或更新错误 + UpdateErr, // 网络或更新错误 + InstallReady } } diff --git a/src/VirtualPaper.AppSettingsPanel/Views/GeneralSetting.xaml b/src/VirtualPaper.AppSettingsPanel/Views/GeneralSetting.xaml index be98836b..e1c6a127 100644 --- a/src/VirtualPaper.AppSettingsPanel/Views/GeneralSetting.xaml +++ b/src/VirtualPaper.AppSettingsPanel/Views/GeneralSetting.xaml @@ -18,59 +18,87 @@ Text="{markup:I18n Key=Settings_General_Text_Version}" FontWeight="Bold"/> - - - - - - + + + + + + + - - - - - - - - + + + + + + + + - - - - + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/VirtualPaper/Views/PluginUpdateWindow.xaml.cs b/src/VirtualPaper/Views/PluginUpdateWindow.xaml.cs new file mode 100644 index 00000000..e616994b --- /dev/null +++ b/src/VirtualPaper/Views/PluginUpdateWindow.xaml.cs @@ -0,0 +1,44 @@ +using System.Windows; +using System.Windows.Input; +using VirtualPaper.Common; +using VirtualPaper.Cores.AppUpdate; +using VirtualPaper.lang; + +namespace VirtualPaper.Views { + public partial class PluginUpdateWindow : Window { + public PluginUpdateWindow() { + InitializeComponent(); + } + + private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { + if (e.ChangedButton == MouseButton.Left) + DragMove(); + } + + private void MinimizeButton_Click(object sender, RoutedEventArgs e) { + WindowState = WindowState.Minimized; + } + + public void ReportProgress(PluginsUpdateProgress progress) { + Dispatcher.Invoke(() => { + UpdateProgressBar.Value = progress.Percent; + StatusText.Text = progress.Message; + + if (progress.Stage == PluginsUpdateStage.Completed) { + TitleText.Text = LanguageManager.Instance[nameof(Constants.I18n.PluginUpdate_Title_Completed)]; + } + else if (progress.Stage == PluginsUpdateStage.Failed) { + TitleText.Text = LanguageManager.Instance[nameof(Constants.I18n.PluginUpdate_Title_Failed)]; + } + }); + } + + public void ShowError(string message) { + Dispatcher.Invoke(() => { + TitleText.Text = LanguageManager.Instance[nameof(Constants.I18n.PluginUpdate_Title_Failed)]; + StatusText.Text = message; + UpdateProgressBar.Foreground = System.Windows.Media.Brushes.Red; + }); + } + } +} diff --git a/src/VirtualPaper/Views/SplashWindow.xaml b/src/VirtualPaper/Views/SplashWindow.xaml index 5a9547e9..ac6ce15e 100644 --- a/src/VirtualPaper/Views/SplashWindow.xaml +++ b/src/VirtualPaper/Views/SplashWindow.xaml @@ -21,7 +21,7 @@ - + diff --git a/src/VirtualPaper/VirtualPaper.csproj b/src/VirtualPaper/VirtualPaper.csproj index d75cdc8c..aa78613a 100644 --- a/src/VirtualPaper/VirtualPaper.csproj +++ b/src/VirtualPaper/VirtualPaper.csproj @@ -11,9 +11,12 @@ 0.5.2.0 virtualpaper.ico true + win-x64 + false + @@ -51,7 +54,7 @@ <_PluginFiles Include="$(MSBuildProjectDirectory)\Plugins\**\*" Exclude="$(MSBuildProjectDirectory)\Plugins\**\*.pdb; $(MSBuildProjectDirectory)\Plugins\**\*.xml" /> - + diff --git a/src/WebBackdrop/Core/Utils/WebProjectSession.cs b/src/WebBackdrop/Core/Utils/WebProjectSession.cs new file mode 100644 index 00000000..49f20dcc --- /dev/null +++ b/src/WebBackdrop/Core/Utils/WebProjectSession.cs @@ -0,0 +1,33 @@ +using System; +using VirtualPaper.Common.Utils.UndoRedo.Events; + +namespace Workloads.Creation.WebBackdrop.Core.Utils { + public partial class WebProjectSession : IDisposable { + public event EventHandler? SessionDisposed; + public event EventHandler? IsSavedChanged; + + public string SessionId { get; } = Guid.NewGuid().ToString(); + + private void UnReUtil_IsSavedChanged(object? sender, IsSavedChangedEventArgs e) { + IsSavedChanged?.Invoke(this, e); + } + + #region dispose + private bool _isDisposed; + protected virtual void Dispose(bool disposing) { + if (!_isDisposed) { + if (disposing) { + //UnReUtil?.Dispose(); + SessionDisposed?.Invoke(this, EventArgs.Empty); + } + _isDisposed = true; + } + } + + public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } + #endregion + } +} diff --git a/src/WebBackdrop/MainPage.xaml b/src/WebBackdrop/MainPage.xaml new file mode 100644 index 00000000..2237c597 --- /dev/null +++ b/src/WebBackdrop/MainPage.xaml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/src/WebBackdrop/MainPage.xaml.cs b/src/WebBackdrop/MainPage.xaml.cs new file mode 100644 index 00000000..1fa9ef90 --- /dev/null +++ b/src/WebBackdrop/MainPage.xaml.cs @@ -0,0 +1,112 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using System.Threading.Tasks; +using VirtualPaper.Common.Logging; +using VirtualPaper.Common.Utils.Storage; +using VirtualPaper.Common.Utils.UndoRedo.Events; +using VirtualPaper.UIComponent; +using VirtualPaper.UIComponent.Templates; +using VirtualPaper.UIComponent.Utils; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Workloads.Creation.WebBackdrop.Core.Utils; +using Workloads.Utils.DraftUtils.Interfaces; +using Workloads.Utils.DraftUtils.Models; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace Workloads.Creation.WebBackdrop { + /// + /// An empty page that can be used on its own or navigated to within a Frame. + /// + public sealed partial class MainPage : ArcPage { + public override Type ArcType => typeof(MainPage); + + public MainPage() { + InitializeComponent(); + } + } + //public sealed partial class MainPage : ArcPage, IRuntime { + // public event EventHandler? IsSavedChanged; + // public string FileName => Session.DesignFileUtil.FileName; + // public string FileNameWithoutEx => Session.DesignFileUtil.FileNameWithoutEx; + // public string Id => Session.SessionId; + // public override Type ArcType => typeof(MainPage); + // protected override bool IsMultiInstance => true; + // public WebProjectSession Session { get; private set; } + // public bool IsSavedFromInit => Session.DesignFileUtil.IsSaveFromInit; + + // public MainPage() { + // InitializeComponent(); + // } + + // #region workSpace events + // public async Task SaveAsync() { + // try { + // //var res = await inkCanvas.SaveAsync(); + // //return res; + // throw new NotImplementedException(); + // } + // catch (Exception ex) { + // ArcLog.GetLogger().Error(ex); + // GlobalMessageUtil.ShowException(ex); + // } + // return false; + // } + + // public async Task SaveAsAsync() { + // try { + // var format = Session.DesignFileUtil.ExportFormatDefult; + // var path = await ExportAsync(format); + // if (path != null) { + // Session.DesignFileUtil.SetFilePath(path); + // //await inkCanvas.UpdateRecentUsedAsync(path); + // throw new NotImplementedException(); + // } + + // return path; + // } + // catch (Exception ex) { + // ArcLog.GetLogger().Error(ex); + // GlobalMessageUtil.ShowException(ex); + // } + // return null; + // } + + // public async Task UndoAsync() { + // try { + // await Session.UnReUtil.UndoAsync(); + // } + // catch (Exception ex) { + // ArcLog.GetLogger().Error(ex); + // GlobalMessageUtil.ShowException(ex); + // } + // } + + // public async Task RedoAsync() { + // try { + // await Session.UnReUtil.RedoAsync(); + // } + // catch (Exception ex) { + // ArcLog.GetLogger().Error(ex); + // GlobalMessageUtil.ShowException(ex); + // } + // } + + // public async Task ExportAsync(ExportImageFormat format) { + // throw new NotImplementedException(); + // } + // #endregion + //} +} diff --git a/src/WebBackdrop/WebBackdrop.csproj b/src/WebBackdrop/WebBackdrop.csproj new file mode 100644 index 00000000..a7179529 --- /dev/null +++ b/src/WebBackdrop/WebBackdrop.csproj @@ -0,0 +1,27 @@ + + + net8.0-windows10.0.19041.0 + 10.0.17763.0 + Workloads.Creation.WebBackdrop + false + true + true + enable + + + + + + + + + + + + + + MSBuild:Compile + + + + \ No newline at end of file diff --git a/src/Workloads.Utils/Workloads.Utils.csproj b/src/Workloads.Utils/Workloads.Utils.csproj index 53c3ab39..6f333e14 100644 --- a/src/Workloads.Utils/Workloads.Utils.csproj +++ b/src/Workloads.Utils/Workloads.Utils.csproj @@ -3,7 +3,7 @@ net8.0-windows10.0.19041.0 10.0.19041.0 Workloads.Utils - win-x86;win-x64;win-arm64 + false true false true