From d9e350b2c6c341df113a074c825538f8401f8a48 Mon Sep 17 00:00:00 2001 From: newbe36524 Date: Fri, 27 Mar 2026 11:58:49 +0800 Subject: [PATCH 1/8] ci(workflows): add dev release workflow and azure version parameter Add publish-dev workflow for automated dev releases and extend sync-azure-storage with version prefix parameter: Changes: - add publish-dev.yml workflow for building and publishing dev artifacts - add release_version input parameter to sync-azure-storage.yml - add release_version workflow_dispatch input to sync-azure-storage - pass RELEASE_VERSION to Azure build environment Co-Authored-By: Hagicode --- .github/workflows/publish-dev.yml | 562 +++++++++++++++++++++++ .github/workflows/sync-azure-storage.yml | 10 + 2 files changed, 572 insertions(+) create mode 100644 .github/workflows/publish-dev.yml diff --git a/.github/workflows/publish-dev.yml b/.github/workflows/publish-dev.yml new file mode 100644 index 0000000..f1e04a6 --- /dev/null +++ b/.github/workflows/publish-dev.yml @@ -0,0 +1,562 @@ +name: Publish Dev Build + +on: + push: + branches: + - publish + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: publish-dev-${{ github.ref }} + cancel-in-progress: true + +env: + NODE_VERSION: "22" + PUBLISH_BASE_VERSION: "0.0.1" + RELEASE_CHANNEL: "dev" + +jobs: + prepare-release: + name: Prepare Dev Release + runs-on: ubuntu-latest + outputs: + version: ${{ steps.meta.outputs.version }} + release_tag: ${{ steps.meta.outputs.release_tag }} + release_name: ${{ steps.meta.outputs.release_name }} + channel: ${{ steps.meta.outputs.channel }} + + steps: + - name: Compute dev release metadata + id: meta + shell: bash + run: | + set -euo pipefail + VERSION="${PUBLISH_BASE_VERSION}-dev.${GITHUB_RUN_NUMBER}" + RELEASE_TAG="V${VERSION}" + RELEASE_NAME="Publish Dev ${VERSION}" + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "release_tag=${RELEASE_TAG}" >> "$GITHUB_OUTPUT" + echo "release_name=${RELEASE_NAME}" >> "$GITHUB_OUTPUT" + echo "channel=${RELEASE_CHANNEL}" >> "$GITHUB_OUTPUT" + + { + echo '## Publish dev release prepared' + echo + echo "- Version: ${VERSION}" + echo "- Release tag: ${RELEASE_TAG}" + echo "- Release channel: ${RELEASE_CHANNEL}" + echo "- Commit: ${GITHUB_SHA}" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Create or update prerelease + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_TAG: ${{ steps.meta.outputs.release_tag }} + RELEASE_NAME: ${{ steps.meta.outputs.release_name }} + run: | + set -euo pipefail + NOTES_FILE="$(mktemp)" + cat > "$NOTES_FILE" </dev/null 2>&1; then + echo "Updating existing prerelease ${RELEASE_TAG}" + gh release edit "$RELEASE_TAG" \ + --title "$RELEASE_NAME" \ + --prerelease + else + echo "Creating prerelease ${RELEASE_TAG}" + gh release create "$RELEASE_TAG" \ + --target "$GITHUB_SHA" \ + --title "$RELEASE_NAME" \ + --prerelease \ + --latest=false \ + --notes-file "$NOTES_FILE" + fi + + build-windows: + name: Build Windows Dev Artifacts + runs-on: windows-latest + needs: prepare-release + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + cache-dependency-path: package-lock.json + + - name: Cache pinned runtime downloads + uses: actions/cache@v4 + with: + path: build/embedded-runtime/downloads + key: ${{ runner.os }}-embedded-runtime-${{ hashFiles('resources/embedded-runtime/runtime-manifest.json') }} + + - name: Sync package version + shell: bash + run: | + set -euo pipefail + VERSION="${{ needs.prepare-release.outputs.version }}" + echo "Syncing npm/package.json version to ${VERSION}" + npm version "${VERSION}" --no-git-tag-version + + - name: Install dependencies + run: npm ci + + - name: Build for Windows + shell: bash + run: node scripts/ci-build.js --platform win + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HAGICODE_EMBEDDED_DOTNET_PLATFORM: win-x64 + + - name: Collect Windows artifacts + id: windows_artifacts + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $pkgDir = Join-Path $env:GITHUB_WORKSPACE 'pkg' + if (-not (Test-Path $pkgDir)) { + throw "pkg directory not found: $pkgDir" + } + + $allFiles = Get-ChildItem -Path $pkgDir -File + $nsis = @($allFiles | Where-Object { $_.Extension -ieq '.exe' -and $_.Name -like '*Setup*' } | Sort-Object FullName -Unique) + $portable = @($allFiles | Where-Object { $_.Extension -ieq '.exe' -and $_.Name -notlike '*Setup*' } | Sort-Object FullName -Unique) + $appx = @($allFiles | Where-Object { $_.Extension -ieq '.appx' } | Sort-Object FullName -Unique) + $unpackedRoots = @(Get-ChildItem -Path $pkgDir -Directory | Where-Object { $_.Name -eq 'win-unpacked' } | Sort-Object FullName -Unique) + + if ($nsis.Count -eq 0 -and $portable.Count -eq 0 -and $appx.Count -eq 0) { + throw 'No Windows artifacts were found in pkg/ for release upload.' + } + + if ($unpackedRoots.Count -eq 0) { + throw 'No win-unpacked directory was found in pkg/ for ZIP assembly.' + } + + $zipPayloadRoot = Join-Path $pkgDir 'windows-zip-payload' + $zipBaseName = if ($portable.Count -gt 0) { + [System.IO.Path]::GetFileNameWithoutExtension($portable[0].Name) + } else { + 'hagicode-windows-unpacked' + } + + function Write-MultilineOutput([string]$Name, [object[]]$Files) { + Add-Content -Path $env:GITHUB_OUTPUT -Value "$Name<> "$GITHUB_STEP_SUMMARY" + + - name: Upload NSIS installer to prerelease + uses: softprops/action-gh-release@v2 + if: success() && steps.windows_artifacts.outputs.nsis_files != '' + with: + tag_name: ${{ needs.prepare-release.outputs.release_tag }} + name: ${{ needs.prepare-release.outputs.release_name }} + draft: 'false' + prerelease: 'true' + overwrite_files: 'true' + files: ${{ steps.windows_artifacts.outputs.nsis_files }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload AppX package to prerelease + uses: softprops/action-gh-release@v2 + if: success() && steps.windows_artifacts.outputs.appx_files != '' + with: + tag_name: ${{ needs.prepare-release.outputs.release_tag }} + name: ${{ needs.prepare-release.outputs.release_name }} + draft: 'false' + prerelease: 'true' + overwrite_files: 'true' + files: ${{ steps.windows_artifacts.outputs.appx_files }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload portable to prerelease + uses: softprops/action-gh-release@v2 + if: success() && steps.windows_artifacts.outputs.portable_files != '' + with: + tag_name: ${{ needs.prepare-release.outputs.release_tag }} + name: ${{ needs.prepare-release.outputs.release_name }} + draft: 'false' + prerelease: 'true' + overwrite_files: 'true' + files: ${{ steps.windows_artifacts.outputs.portable_files }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload Windows ZIP to prerelease + uses: softprops/action-gh-release@v2 + if: success() && steps.windows_zip.outputs.zip_files != '' + with: + tag_name: ${{ needs.prepare-release.outputs.release_tag }} + name: ${{ needs.prepare-release.outputs.release_name }} + draft: 'false' + prerelease: 'true' + overwrite_files: 'true' + files: ${{ steps.windows_zip.outputs.zip_files }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + build-linux: + name: Build Linux Dev Artifacts + runs-on: ubuntu-latest + needs: prepare-release + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + cache-dependency-path: package-lock.json + + - name: Cache pinned runtime downloads + uses: actions/cache@v4 + with: + path: build/embedded-runtime/downloads + key: ${{ runner.os }}-embedded-runtime-${{ hashFiles('resources/embedded-runtime/runtime-manifest.json') }} + + - name: Sync package version + shell: bash + run: | + set -euo pipefail + VERSION="${{ needs.prepare-release.outputs.version }}" + echo "Syncing npm/package.json version to ${VERSION}" + npm version "${VERSION}" --no-git-tag-version + + - name: Install dependencies + run: npm ci + + - name: Build for Linux + run: node scripts/ci-build.js --platform linux + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HAGICODE_EMBEDDED_DOTNET_PLATFORM: linux-x64 + + - name: Summarize Linux artifacts + if: success() + shell: bash + run: | + set -euo pipefail + shopt -s nullglob + zip_files=(pkg/*.zip) + zip_count=${#zip_files[@]} + + { + echo '## Linux packaging artifacts' + echo + echo '- AppImage upload pattern: pkg/*.AppImage' + echo '- deb upload pattern: pkg/*.deb' + echo '- tar.gz upload pattern: pkg/*.tar.gz' + if [ "$zip_count" -eq 0 ]; then + echo '- ZIP upload pattern: pkg/*.zip (missing)' + else + echo "- ZIP upload pattern: pkg/*.zip (${zip_count} file(s))" + fi + } >> "$GITHUB_STEP_SUMMARY" + + if [ "$zip_count" -eq 0 ]; then + echo '::error::Linux ZIP artifact was not produced in pkg/.' + exit 1 + fi + + - name: Upload AppImage to prerelease + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.prepare-release.outputs.release_tag }} + name: ${{ needs.prepare-release.outputs.release_name }} + draft: 'false' + prerelease: 'true' + overwrite_files: 'true' + files: pkg/*.AppImage + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload deb to prerelease + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.prepare-release.outputs.release_tag }} + name: ${{ needs.prepare-release.outputs.release_name }} + draft: 'false' + prerelease: 'true' + overwrite_files: 'true' + files: pkg/*.deb + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload tar.gz to prerelease + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.prepare-release.outputs.release_tag }} + name: ${{ needs.prepare-release.outputs.release_name }} + draft: 'false' + prerelease: 'true' + overwrite_files: 'true' + files: pkg/*.tar.gz + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload Linux ZIP to prerelease + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.prepare-release.outputs.release_tag }} + name: ${{ needs.prepare-release.outputs.release_name }} + draft: 'false' + prerelease: 'true' + overwrite_files: 'true' + files: pkg/*.zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + build-macos: + name: Build macOS Dev Artifacts + runs-on: macos-latest + needs: prepare-release + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + cache-dependency-path: package-lock.json + + - name: Sync package version + shell: bash + run: | + set -euo pipefail + VERSION="${{ needs.prepare-release.outputs.version }}" + echo "Syncing npm/package.json version to ${VERSION}" + npm version "${VERSION}" --no-git-tag-version + + - name: Install dependencies + run: npm ci + + - name: Build for macOS + run: node scripts/ci-build.js --platform mac + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload DMG to prerelease + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.prepare-release.outputs.release_tag }} + name: ${{ needs.prepare-release.outputs.release_name }} + draft: 'false' + prerelease: 'true' + overwrite_files: 'true' + files: pkg/*.dmg + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload ZIP to prerelease + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.prepare-release.outputs.release_tag }} + name: ${{ needs.prepare-release.outputs.release_name }} + draft: 'false' + prerelease: 'true' + overwrite_files: 'true' + files: pkg/*.zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + build-summary: + name: Publish Dev Summary + needs: + - prepare-release + - build-windows + - build-linux + - build-macos + if: always() + runs-on: ubuntu-latest + outputs: + release_status: ${{ steps.status.outputs.overall }} + channel: ${{ steps.status.outputs.channel }} + + steps: + - name: Determine workflow status + id: status + shell: bash + run: | + set -euo pipefail + windows_status="${{ needs.build-windows.result }}" + linux_status="${{ needs.build-linux.result }}" + macos_status="${{ needs.build-macos.result }}" + + echo "Windows: ${windows_status}" + echo "Linux: ${linux_status}" + echo "macOS: ${macos_status}" + + if [ "$windows_status" != 'success' ] || [ "$linux_status" != 'success' ] || [ "$macos_status" != 'success' ]; then + echo 'overall=failed' >> "$GITHUB_OUTPUT" + else + echo 'overall=success' >> "$GITHUB_OUTPUT" + fi + + echo 'channel=dev' >> "$GITHUB_OUTPUT" + + - name: Publish workflow summary + if: always() + shell: bash + run: | + if [ "${{ steps.status.outputs.overall }}" = "success" ]; then + azure_sync_eligible=true + else + azure_sync_eligible=false + fi + + { + echo '## Publish dev orchestration summary' + echo + echo '- Version: ${{ needs.prepare-release.outputs.version }}' + echo '- Release tag: ${{ needs.prepare-release.outputs.release_tag }}' + echo '- Windows status: ${{ needs.build-windows.result }}' + echo '- Linux status: ${{ needs.build-linux.result }}' + echo '- macOS status: ${{ needs.build-macos.result }}' + echo '- Overall status: ${{ steps.status.outputs.overall }}' + echo "- Azure sync eligible: ${azure_sync_eligible}" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Fail workflow on platform failure + if: steps.status.outputs.overall != 'success' + shell: bash + run: | + echo '::error::One or more publish dev platform builds failed. Azure sync is blocked.' + exit 1 + + sync-azure: + name: Sync Dev Release to Azure Storage + needs: + - prepare-release + - build-summary + if: ${{ always() && needs.build-summary.outputs.release_status == 'success' }} + uses: ./.github/workflows/sync-azure-storage.yml + with: + release_tag: ${{ needs.prepare-release.outputs.release_tag }} + release_version: ${{ needs.prepare-release.outputs.version }} + release_channel: ${{ needs.prepare-release.outputs.channel }} + secrets: inherit diff --git a/.github/workflows/sync-azure-storage.yml b/.github/workflows/sync-azure-storage.yml index 0ddab3d..05a9afb 100644 --- a/.github/workflows/sync-azure-storage.yml +++ b/.github/workflows/sync-azure-storage.yml @@ -12,6 +12,11 @@ on: description: 'Release tag to sync' required: true type: string + release_version: + description: 'Version prefix to publish into Azure (defaults to release_tag)' + required: false + type: string + default: '' release_channel: description: 'Release channel (stable, beta, dev)' required: false @@ -26,6 +31,10 @@ on: description: 'Release tag to sync (empty for latest release)' required: false type: string + release_version: + description: 'Version prefix to publish into Azure (defaults to release_tag)' + required: false + type: string release_channel: description: 'Release channel (stable, beta, dev)' required: false @@ -81,6 +90,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} AZURE_BLOB_SAS_URL: ${{ secrets.AZURE_BLOB_SAS_URL }} RELEASE_TAG: ${{ steps.release_info.outputs.tag }} + RELEASE_VERSION: ${{ inputs.release_version }} RELEASE_CHANNEL: ${{ inputs.release_channel }} FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }} GITHUB_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} From 2625c32a002a923cf516d314dbca690797ff87d5 Mon Sep 17 00:00:00 2001 From: newbe36524 Date: Fri, 27 Mar 2026 11:58:58 +0800 Subject: [PATCH 2/8] feat(nukeBuild): add configurable Azure version prefix Add ReleaseVersion parameter to support custom version prefix for Azure publishing: Changes: - add ReleaseVersion parameter to Build.cs - use ReleaseVersion in Build.AzureStorage.cs for Azure version prefix - default to versionTag when ReleaseVersion is not specified - add logging for effective Azure publish version Co-Authored-By: Hagicode --- nukeBuild/Build.AzureStorage.cs | 10 ++++++++-- nukeBuild/Build.cs | 3 +++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/nukeBuild/Build.AzureStorage.cs b/nukeBuild/Build.AzureStorage.cs index 2a67e8e..dfbf9a8 100644 --- a/nukeBuild/Build.AzureStorage.cs +++ b/nukeBuild/Build.AzureStorage.cs @@ -80,7 +80,12 @@ private async Task ExecutePublishToAzureBlob() Log.Information("使用指定的 ReleaseTag: {Tag}", versionTag); } - BuildConfig.Version = versionTag; + var effectiveVersion = !string.IsNullOrWhiteSpace(ReleaseVersion) + ? ReleaseVersion + : versionTag; + + BuildConfig.Version = effectiveVersion; + Log.Information("Azure publish version prefix: {Version}", effectiveVersion); if (!UploadArtifacts && !UploadIndex) { @@ -140,7 +145,7 @@ private async Task ExecutePublishToAzureBlob() { SasUrl = AzureBlobSasUrl, UploadRetries = AzureUploadRetries, - VersionPrefix = versionTag, + VersionPrefix = effectiveVersion, }; var localIndexPath = (RootDirectory / "artifacts" / "azure-index.json").ToString(); @@ -213,6 +218,7 @@ private async Task ExecutePublishToAzureBlob() Log.Information("=== 同步完成 ==="); Log.Information(" Release Tag: {Tag}", versionTag); + Log.Information(" Azure Version Prefix: {Version}", effectiveVersion); Log.Information(" 下载资源: {DownloadCount}", downloadedFiles.Count); Log.Information(" 产物上传: {ArtifactsStatus}", UploadArtifacts ? "已执行" : "已跳过"); Log.Information(" Index 上传: {IndexStatus}", UploadIndex ? "已执行" : "已跳过"); diff --git a/nukeBuild/Build.cs b/nukeBuild/Build.cs index c039ef7..7e796ad 100644 --- a/nukeBuild/Build.cs +++ b/nukeBuild/Build.cs @@ -55,6 +55,9 @@ partial class Build : NukeBuild [Parameter("Release tag to sync (e.g., v1.0.0)")] readonly string ReleaseTag = ""; + [Parameter("Version prefix to publish (defaults to release tag)")] + readonly string ReleaseVersion = ""; + [Parameter("Release channel (stable, beta, dev)")] readonly string ReleaseChannel = "beta"; From 55390a865de3f0197ee3933f6567a2d81dcde920 Mon Sep 17 00:00:00 2001 From: newbe36524 Date: Fri, 27 Mar 2026 14:23:54 +0800 Subject: [PATCH 3/8] ci(workflows): fix publish-dev workflow checkout and repo context Fix missing checkout step and add repository context for release operations: Changes: - add Checkout code step before Compute dev release metadata - add GH_REPO environment variable for gh CLI repository context Co-Authored-By: Hagicode --- .github/workflows/publish-dev.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/publish-dev.yml b/.github/workflows/publish-dev.yml index f1e04a6..112cb3e 100644 --- a/.github/workflows/publish-dev.yml +++ b/.github/workflows/publish-dev.yml @@ -29,6 +29,9 @@ jobs: channel: ${{ steps.meta.outputs.channel }} steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Compute dev release metadata id: meta shell: bash @@ -56,6 +59,7 @@ jobs: shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} RELEASE_TAG: ${{ steps.meta.outputs.release_tag }} RELEASE_NAME: ${{ steps.meta.outputs.release_name }} run: | From 9e1a9e24263a596aefb54efa0159ca8627218a52 Mon Sep 17 00:00:00 2001 From: newbe36524 Date: Fri, 27 Mar 2026 15:26:50 +0800 Subject: [PATCH 4/8] ci(workflows): fix publish-dev version prefix format Add 'v' prefix to release_version parameter to match version normalization. Changes: - add v prefix to release_version in publish-dev workflow Co-Authored-By: Hagicode --- .github/workflows/publish-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-dev.yml b/.github/workflows/publish-dev.yml index 112cb3e..63ff5f6 100644 --- a/.github/workflows/publish-dev.yml +++ b/.github/workflows/publish-dev.yml @@ -561,6 +561,6 @@ jobs: uses: ./.github/workflows/sync-azure-storage.yml with: release_tag: ${{ needs.prepare-release.outputs.release_tag }} - release_version: ${{ needs.prepare-release.outputs.version }} + release_version: v${{ needs.prepare-release.outputs.version }} release_channel: ${{ needs.prepare-release.outputs.channel }} secrets: inherit From 3573733068fc78be4ed68ab3d85f918cf6b5f9ae Mon Sep 17 00:00:00 2001 From: newbe36524 Date: Fri, 27 Mar 2026 15:26:53 +0800 Subject: [PATCH 5/8] feat(nukeBuild): add Azure storage configuration constants and public base URL Add configuration constants for GitHub release repository and public CDN base URL. Changes: - add GitHubReleaseRepositoryName and DesktopPublicBaseUrl constants to BuildConfig - add PublicBaseUrl property to AzureBlobPublishOptions - add AzurePublicBaseUrl parameter to Build Co-Authored-By: Hagicode --- nukeBuild/AzureStorageConfiguration.cs | 6 ++++++ nukeBuild/Build.cs | 3 +++ nukeBuild/BuildConfig.cs | 3 +++ 3 files changed, 12 insertions(+) diff --git a/nukeBuild/AzureStorageConfiguration.cs b/nukeBuild/AzureStorageConfiguration.cs index 4dcddac..f6edc63 100644 --- a/nukeBuild/AzureStorageConfiguration.cs +++ b/nukeBuild/AzureStorageConfiguration.cs @@ -47,6 +47,12 @@ public class AzureBlobPublishOptions /// public string VersionPrefix { get; set; } = string.Empty; + /// + /// Public base URL used in generated download links and web seeds. + /// Falls back to the SAS container URL when empty. + /// + public string PublicBaseUrl { get; set; } = string.Empty; + /// /// Container name (parsed from SAS URL) /// diff --git a/nukeBuild/Build.cs b/nukeBuild/Build.cs index 7e796ad..ee2c4dc 100644 --- a/nukeBuild/Build.cs +++ b/nukeBuild/Build.cs @@ -27,6 +27,9 @@ partial class Build : NukeBuild [Parameter("Azure Blob SAS URL for authentication and upload")] [Secret] readonly string AzureBlobSasUrl = ""; + [Parameter("Public base URL for published desktop artifacts")] + readonly string AzurePublicBaseUrl = BuildConfig.DesktopPublicBaseUrl; + [Parameter("Skip Azure Blob publish")] readonly bool SkipAzureBlobPublish = false; [Parameter("Generate Azure index.json")] readonly bool AzureGenerateIndex = true; diff --git a/nukeBuild/BuildConfig.cs b/nukeBuild/BuildConfig.cs index 5bd1bc7..ebe3abf 100644 --- a/nukeBuild/BuildConfig.cs +++ b/nukeBuild/BuildConfig.cs @@ -5,6 +5,9 @@ /// internal static class BuildConfig { + internal const string GitHubReleaseRepositoryName = "hagicode-desktop"; + internal const string DesktopPublicBaseUrl = "https://desktop.dl.hagicode.com"; + /// /// The release packaged directory path /// From 5b2063f6cd140cdebe6591f5946ca7f300d762f8 Mon Sep 17 00:00:00 2001 From: newbe36524 Date: Fri, 27 Mar 2026 15:26:55 +0800 Subject: [PATCH 6/8] feat(nukeBuild): add version normalization and source archive filtering Add utilities for normalizing version prefixes and filtering GitHub-generated source archives. Changes: - add NormalizePublishedVersionPrefix method to ensure v prefix - add ResolvePublicBaseUrl utility method for public URL resolution - add IsGitHubGeneratedSourceArchive for detecting GitHub source archives - filter out GitHub-generated .zip and .tar.gz source archives from downloads and index Co-Authored-By: Hagicode --- nukeBuild/Build.AzureStorage.cs | 36 ++++++++++++++++++++--- nukeBuild/Utils/AzureBlobPathUtilities.cs | 33 +++++++++++++++++++-- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/nukeBuild/Build.AzureStorage.cs b/nukeBuild/Build.AzureStorage.cs index dfbf9a8..d0c25bf 100644 --- a/nukeBuild/Build.AzureStorage.cs +++ b/nukeBuild/Build.AzureStorage.cs @@ -1,6 +1,7 @@ using Adapters; using AzureStorage; using System.Diagnostics; +using Utils; public partial class Build { @@ -80,9 +81,10 @@ private async Task ExecutePublishToAzureBlob() Log.Information("使用指定的 ReleaseTag: {Tag}", versionTag); } - var effectiveVersion = !string.IsNullOrWhiteSpace(ReleaseVersion) - ? ReleaseVersion - : versionTag; + var effectiveVersion = NormalizePublishedVersionPrefix( + !string.IsNullOrWhiteSpace(ReleaseVersion) + ? ReleaseVersion + : versionTag); BuildConfig.Version = effectiveVersion; Log.Information("Azure publish version prefix: {Version}", effectiveVersion); @@ -120,10 +122,23 @@ private async Task ExecutePublishToAzureBlob() Log.Information("使用 gh CLI 下载 release 资产..."); await DownloadReleaseAssetsUsingGhAsync(versionTag, downloadDirectory); - downloadedFiles = Directory.GetFiles(downloadDirectory) + var allDownloadedFiles = Directory.GetFiles(downloadDirectory) .Where((path) => !File.GetAttributes(path).HasFlag(FileAttributes.Directory)) .ToList(); + downloadedFiles = allDownloadedFiles + .Where((path) => !AzureBlobPathUtilities.IsGitHubGeneratedSourceArchive( + Path.GetFileName(path), + BuildConfig.GitHubReleaseRepositoryName, + effectiveVersion)) + .ToList(); + + var filteredAssetCount = allDownloadedFiles.Count - downloadedFiles.Count; + if (filteredAssetCount > 0) + { + Log.Information("已过滤 {Count} 个 GitHub 自动生成源码包资产", filteredAssetCount); + } + if (downloadedFiles.Count > 0) { Log.Information("成功下载 {Count} 个资源", downloadedFiles.Count); @@ -146,6 +161,7 @@ private async Task ExecutePublishToAzureBlob() SasUrl = AzureBlobSasUrl, UploadRetries = AzureUploadRetries, VersionPrefix = effectiveVersion, + PublicBaseUrl = AzurePublicBaseUrl, }; var localIndexPath = (RootDirectory / "artifacts" / "azure-index.json").ToString(); @@ -292,6 +308,18 @@ private static string ResolveFailureStageCode(ArtifactPublishFailureStage? stage }; } + private static string NormalizePublishedVersionPrefix(string versionOrTag) + { + var normalized = versionOrTag.Trim(); + if (string.IsNullOrWhiteSpace(normalized)) + { + return normalized; + } + + normalized = normalized.TrimStart('v', 'V'); + return $"v{normalized}"; + } + private async Task DownloadReleaseAssetsUsingGhAsync(string tag, AbsolutePath downloadDirectory) { Directory.CreateDirectory(downloadDirectory); diff --git a/nukeBuild/Utils/AzureBlobPathUtilities.cs b/nukeBuild/Utils/AzureBlobPathUtilities.cs index 815d50c..de928c8 100644 --- a/nukeBuild/Utils/AzureBlobPathUtilities.cs +++ b/nukeBuild/Utils/AzureBlobPathUtilities.cs @@ -2,7 +2,7 @@ namespace Utils; public static class AzureBlobPathUtilities { - public static string NormalizeVersionPrefix(string? versionPrefix) + public static string NormalizeVersionPrefix(string versionPrefix) { if (string.IsNullOrWhiteSpace(versionPrefix)) { @@ -12,7 +12,7 @@ public static string NormalizeVersionPrefix(string? versionPrefix) return versionPrefix.Trim().Trim('/').Replace('\\', '/'); } - public static string BuildBlobPath(string? versionPrefix, string fileName) + public static string BuildBlobPath(string versionPrefix, string fileName) { var normalizedPrefix = NormalizeVersionPrefix(versionPrefix); var normalizedFileName = fileName.Replace('\\', '/'); @@ -27,6 +27,16 @@ public static string BuildContainerBaseUrl(string sasUrl) return $"{uri.GetLeftPart(UriPartial.Path).TrimEnd('/')}/"; } + public static string ResolvePublicBaseUrl(string sasUrl, string publicBaseUrl = "") + { + if (!string.IsNullOrWhiteSpace(publicBaseUrl)) + { + return $"{publicBaseUrl.Trim().TrimEnd('/')}/"; + } + + return BuildContainerBaseUrl(sasUrl); + } + public static string BuildBlobUrl(string containerBaseUrl, string blobPath) { var baseUri = new Uri(containerBaseUrl.EndsWith('/') ? containerBaseUrl : $"{containerBaseUrl}/"); @@ -38,4 +48,23 @@ public static string ExtractVersion(string blobName) var slashIndex = blobName.IndexOf('/'); return slashIndex > 0 ? blobName[..slashIndex] : "latest"; } + + public static bool IsGitHubGeneratedSourceArchive(string fileName, string repositoryName, string releaseVersionOrTag) + { + if (string.IsNullOrWhiteSpace(fileName) || + string.IsNullOrWhiteSpace(repositoryName) || + string.IsNullOrWhiteSpace(releaseVersionOrTag)) + { + return false; + } + + var normalizedVersion = releaseVersionOrTag.Trim().TrimStart('v', 'V'); + if (string.IsNullOrWhiteSpace(normalizedVersion)) + { + return false; + } + + return fileName.Equals($"{repositoryName}-{normalizedVersion}.zip", StringComparison.OrdinalIgnoreCase) + || fileName.Equals($"{repositoryName}-{normalizedVersion}.tar.gz", StringComparison.OrdinalIgnoreCase); + } } From 7df67ba13ab1a6fc9896b0409410333e53df7061 Mon Sep 17 00:00:00 2001 From: newbe36524 Date: Fri, 27 Mar 2026 15:26:58 +0800 Subject: [PATCH 7/8] feat(nukeBuild): update Azure adapters to use public base URL and filter source archives Update Azure blob adapter and release orchestrator to use resolved public base URL. Changes: - update AzureBlobAdapter.BuildIndexResult to use ResolvePublicBaseUrl - filter GitHub-generated source archives in BuildIndexResult - update AzureReleasePublishOrchestrator to use ResolvePublicBaseUrl Co-Authored-By: Hagicode --- nukeBuild/Adapters/AzureBlobAdapter.cs | 11 ++++++++--- nukeBuild/Adapters/AzureReleasePublishOrchestrator.cs | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/nukeBuild/Adapters/AzureBlobAdapter.cs b/nukeBuild/Adapters/AzureBlobAdapter.cs index f1aae73..3b39f28 100644 --- a/nukeBuild/Adapters/AzureBlobAdapter.cs +++ b/nukeBuild/Adapters/AzureBlobAdapter.cs @@ -327,7 +327,7 @@ public async Task GenerateIndexFromBlobsWithMeta } var blobs = await ListBlobsAsync(options); - var result = BuildIndexResult(blobs, options.SasUrl, publishedArtifacts); + var result = BuildIndexResult(blobs, options.SasUrl, publishedArtifacts, options.PublicBaseUrl); result.IndexJson = SerializeJson(result.Document!, minify); await File.WriteAllTextAsync(outputPath, result.IndexJson); @@ -348,9 +348,10 @@ public async Task GenerateIndexFromBlobsWithMeta public AzureBlobIndexGenerationResult BuildIndexResult( IReadOnlyCollection blobs, string sasUrl, - IReadOnlyCollection? publishedArtifacts = null) + IReadOnlyCollection publishedArtifacts = null, + string publicBaseUrl = "") { - var containerBaseUrl = AzureBlobPathUtilities.BuildContainerBaseUrl(sasUrl); + var containerBaseUrl = AzureBlobPathUtilities.ResolvePublicBaseUrl(sasUrl, publicBaseUrl); var metadataByPath = (publishedArtifacts ?? Array.Empty()) .ToDictionary((artifact) => artifact.Path, StringComparer.OrdinalIgnoreCase); @@ -369,6 +370,10 @@ public AzureBlobIndexGenerationResult BuildIndexResult( var blobsByName = versionGroup.ToDictionary((blob) => blob.Name, StringComparer.OrdinalIgnoreCase); var artifactBlobs = versionGroup .Where((blob) => !blob.Name.EndsWith(".torrent", StringComparison.OrdinalIgnoreCase)) + .Where((blob) => !AzureBlobPathUtilities.IsGitHubGeneratedSourceArchive( + Path.GetFileName(blob.Name), + BuildConfig.GitHubReleaseRepositoryName, + versionGroup.Key)) .OrderBy((blob) => blob.Name, StringComparer.OrdinalIgnoreCase) .ToList(); diff --git a/nukeBuild/Adapters/AzureReleasePublishOrchestrator.cs b/nukeBuild/Adapters/AzureReleasePublishOrchestrator.cs index 60e85e7..4c6aefa 100644 --- a/nukeBuild/Adapters/AzureReleasePublishOrchestrator.cs +++ b/nukeBuild/Adapters/AzureReleasePublishOrchestrator.cs @@ -23,7 +23,7 @@ public async Task PublishAsync( bool minifyIndexJson) { var summary = new ReleasePublishSummary(); - var containerBaseUrl = Utils.AzureBlobPathUtilities.BuildContainerBaseUrl(options.SasUrl); + var containerBaseUrl = Utils.AzureBlobPathUtilities.ResolvePublicBaseUrl(options.SasUrl, options.PublicBaseUrl); var metadataResult = await _metadataBuilder.BuildAsync(downloadedFiles, options.VersionPrefix, containerBaseUrl); summary.EligibleAssetCount = metadataResult.EligibleArtifactCount; From fb361034b0c520c03cd2fb9b69ff6c90bae9c1c9 Mon Sep 17 00:00:00 2001 From: newbe36524 Date: Fri, 27 Mar 2026 15:27:01 +0800 Subject: [PATCH 8/8] test(nukeBuild): update tests for version prefix and public base URL Update test expectations to match new version normalization with v prefix and public CDN URL. Changes: - update AzureBlobAdapterTests to use v-prefixed versions - update AzureBlobAdapterTests to use desktop.dl.hagicode.com public URL - add test data for GitHub-generated source archives filtering - update ArtifactHybridMetadataBuilderTests for v-prefixed versions and public URL Co-Authored-By: Hagicode --- .../ArtifactHybridMetadataBuilderTests.cs | 8 ++--- nukeBuild.Tests/AzureBlobAdapterTests.cs | 32 +++++++++++-------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/nukeBuild.Tests/ArtifactHybridMetadataBuilderTests.cs b/nukeBuild.Tests/ArtifactHybridMetadataBuilderTests.cs index 2b25934..601e092 100644 --- a/nukeBuild.Tests/ArtifactHybridMetadataBuilderTests.cs +++ b/nukeBuild.Tests/ArtifactHybridMetadataBuilderTests.cs @@ -16,15 +16,15 @@ public async Task BuildAsync_GeneratesHybridMetadataForEligibleArtifact() await File.WriteAllBytesAsync(artifactPath, Enumerable.Repeat((byte)0x2A, 4096).ToArray()); var builder = new ArtifactHybridMetadataBuilder(new TorrentSidecarGenerator(pieceLengthBytes: 64), thresholdBytes: 1); - var result = await builder.BuildAsync(new[] { artifactPath }, "1.2.3", "https://example.blob.core.windows.net/releases/"); + var result = await builder.BuildAsync(new[] { artifactPath }, "v1.2.3", "https://desktop.dl.hagicode.com/"); var artifact = Assert.Single(result.Artifacts); Assert.True(artifact.MeetsThreshold); Assert.True(artifact.HybridEligible); Assert.False(artifact.LegacyHttpFallback); - Assert.Equal("1.2.3/hagicode-1.2.3-win-x64-nort.zip", artifact.Path); - Assert.Equal("https://example.blob.core.windows.net/releases/1.2.3/hagicode-1.2.3-win-x64-nort.zip", artifact.DirectUrl); - Assert.Equal("https://example.blob.core.windows.net/releases/1.2.3/hagicode-1.2.3-win-x64-nort.zip.torrent", artifact.TorrentUrl); + Assert.Equal("v1.2.3/hagicode-1.2.3-win-x64-nort.zip", artifact.Path); + Assert.Equal("https://desktop.dl.hagicode.com/v1.2.3/hagicode-1.2.3-win-x64-nort.zip", artifact.DirectUrl); + Assert.Equal("https://desktop.dl.hagicode.com/v1.2.3/hagicode-1.2.3-win-x64-nort.zip.torrent", artifact.TorrentUrl); Assert.Contains(artifact.DirectUrl, artifact.WebSeeds); Assert.False(string.IsNullOrWhiteSpace(artifact.InfoHash)); Assert.False(string.IsNullOrWhiteSpace(artifact.Sha256)); diff --git a/nukeBuild.Tests/AzureBlobAdapterTests.cs b/nukeBuild.Tests/AzureBlobAdapterTests.cs index 9b9505c..c7ed638 100644 --- a/nukeBuild.Tests/AzureBlobAdapterTests.cs +++ b/nukeBuild.Tests/AzureBlobAdapterTests.cs @@ -14,21 +14,21 @@ public void BuildIndexResult_WritesAssetsAndFilesAndPreservesFallbacks() { AbsolutePath root = Path.GetTempPath(); var adapter = new AzureBlobAdapter(root); - var directUrl = "https://example.blob.core.windows.net/releases/1.0.0/hagicode-1.0.0-win-x64-nort.zip"; + var directUrl = "https://desktop.dl.hagicode.com/v1.0.0/hagicode-1.0.0-win-x64-nort.zip"; var publishedArtifacts = new[] { new PublishedArtifactMetadata { Name = "hagicode-1.0.0-win-x64-nort.zip", LocalFilePath = "/tmp/hagicode-1.0.0-win-x64-nort.zip", - Path = "1.0.0/hagicode-1.0.0-win-x64-nort.zip", + Path = "v1.0.0/hagicode-1.0.0-win-x64-nort.zip", Size = 2048, LastModified = DateTime.UnixEpoch, DirectUrl = directUrl, MeetsThreshold = true, HybridEligible = true, LegacyHttpFallback = false, - TorrentPath = "1.0.0/hagicode-1.0.0-win-x64-nort.zip.torrent", + TorrentPath = "v1.0.0/hagicode-1.0.0-win-x64-nort.zip.torrent", TorrentUrl = $"{directUrl}.torrent", InfoHash = "abc123", Sha256 = "deadbeef", @@ -37,14 +37,20 @@ public void BuildIndexResult_WritesAssetsAndFilesAndPreservesFallbacks() }; var blobs = new List { - new() { Name = "1.0.0/hagicode-1.0.0-win-x64-nort.zip", Size = 2048, LastModified = DateTime.UnixEpoch }, - new() { Name = "1.0.0/hagicode-1.0.0-win-x64-nort.zip.torrent", Size = 512, LastModified = DateTime.UnixEpoch }, - new() { Name = "0.9.0/legacy.zip", Size = 128, LastModified = DateTime.UnixEpoch }, - new() { Name = "0.8.0/missing.zip", Size = 4096, LastModified = DateTime.UnixEpoch }, - new() { Name = "0.8.0/missing.zip.torrent", Size = 256, LastModified = DateTime.UnixEpoch }, + new() { Name = "v1.0.0/hagicode-1.0.0-win-x64-nort.zip", Size = 2048, LastModified = DateTime.UnixEpoch }, + new() { Name = "v1.0.0/hagicode-1.0.0-win-x64-nort.zip.torrent", Size = 512, LastModified = DateTime.UnixEpoch }, + new() { Name = "v1.0.0/hagicode-desktop-1.0.0.zip", Size = 64, LastModified = DateTime.UnixEpoch }, + new() { Name = "v1.0.0/hagicode-desktop-1.0.0.tar.gz", Size = 64, LastModified = DateTime.UnixEpoch }, + new() { Name = "v0.9.0/legacy.zip", Size = 128, LastModified = DateTime.UnixEpoch }, + new() { Name = "v0.8.0/missing.zip", Size = 4096, LastModified = DateTime.UnixEpoch }, + new() { Name = "v0.8.0/missing.zip.torrent", Size = 256, LastModified = DateTime.UnixEpoch }, }; - var result = adapter.BuildIndexResult(blobs, "https://example.blob.core.windows.net/releases?sig=test", publishedArtifacts); + var result = adapter.BuildIndexResult( + blobs, + "https://example.blob.core.windows.net/releases?sig=test", + publishedArtifacts, + "https://desktop.dl.hagicode.com"); var json = JsonSerializer.Serialize(result.Document, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, @@ -58,16 +64,16 @@ public void BuildIndexResult_WritesAssetsAndFilesAndPreservesFallbacks() Assert.Equal(2, result.HttpOnlyFallbackCount); Assert.Contains(result.Diagnostics, (diagnostic) => diagnostic.Code == "historical-http-only"); - var latestVersion = versions.EnumerateArray().First((entry) => entry.GetProperty("version").GetString() == "1.0.0"); + var latestVersion = versions.EnumerateArray().First((entry) => entry.GetProperty("version").GetString() == "v1.0.0"); var latestAsset = latestVersion.GetProperty("assets")[0]; Assert.Equal("hagicode-1.0.0-win-x64-nort.zip", latestAsset.GetProperty("name").GetString()); Assert.Equal($"{directUrl}.torrent", latestAsset.GetProperty("torrentUrl").GetString()); Assert.Equal("deadbeef", latestAsset.GetProperty("sha256").GetString()); - Assert.Equal("1.0.0/hagicode-1.0.0-win-x64-nort.zip", latestVersion.GetProperty("files")[0].GetString()); + Assert.Equal("v1.0.0/hagicode-1.0.0-win-x64-nort.zip", latestVersion.GetProperty("files")[0].GetString()); - var legacyVersion = versions.EnumerateArray().First((entry) => entry.GetProperty("version").GetString() == "0.9.0"); + var legacyVersion = versions.EnumerateArray().First((entry) => entry.GetProperty("version").GetString() == "v0.9.0"); var legacyAsset = legacyVersion.GetProperty("assets")[0]; Assert.False(legacyAsset.TryGetProperty("torrentUrl", out _)); - Assert.Equal("https://example.blob.core.windows.net/releases/0.9.0/legacy.zip", legacyAsset.GetProperty("directUrl").GetString()); + Assert.Equal("https://desktop.dl.hagicode.com/v0.9.0/legacy.zip", legacyAsset.GetProperty("directUrl").GetString()); } }