Desktop Release · v4.2.0 #28
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Desktop Release | |
| run-name: Desktop Release · ${{ github.ref_name }} | |
| on: | |
| push: | |
| tags: ['v*'] | |
| concurrency: | |
| group: desktop-release-${{ github.ref }} | |
| cancel-in-progress: false | |
| env: | |
| DOTNET_VERSION: '10.0.x' | |
| VELOPACK_VERSION: '1.2.0' | |
| DOTNET_CLI_TELEMETRY_OPTOUT: '1' | |
| DOTNET_NOLOGO: 'true' | |
| jobs: | |
| windows: | |
| name: Windows · ${{ matrix.rid }} | |
| runs-on: windows-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| strategy: | |
| matrix: | |
| include: | |
| - rid: win-x64 | |
| - rid: win-arm64 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| - name: Set up .NET | |
| uses: actions/setup-dotnet@v6 | |
| with: | |
| dotnet-version: ${{ env.DOTNET_VERSION }} | |
| - name: Resolve package version | |
| id: version | |
| shell: bash | |
| run: | | |
| TAG="${GITHUB_REF#refs/tags/v}" | |
| echo "version=$TAG" >> "$GITHUB_OUTPUT" | |
| - name: Build Windows app | |
| run: | | |
| dotnet publish collection/desktop/Heartbeat.Desktop.Windows/Heartbeat.Desktop.Windows.csproj ` | |
| -c Release ` | |
| -r ${{ matrix.rid }} ` | |
| --self-contained ` | |
| -p:Version=${{ steps.version.outputs.version }} ` | |
| -o publish | |
| - name: Verify Windows publish delivery contract | |
| shell: bash | |
| run: | | |
| # System 是唯一随 Desktop 发布的 BuiltIn Collector。可选 Collector 都是独立发布单元, | |
| # 宿主产物里不允许出现除 System 以外的任何 Collector Package(ADR-048/ADR-049)。 | |
| test -f publish/CollectorPackages/System/collector-manifest.json | |
| test "$(ls publish/CollectorPackages)" = "System" | |
| - name: Install Velopack CLI | |
| run: dotnet tool install -g vpk --version ${{ env.VELOPACK_VERSION }} | |
| - name: Fetch Windows delta baseline | |
| id: delta_baseline | |
| continue-on-error: true | |
| run: | | |
| vpk download github ` | |
| --repoUrl https://github.com/${{ github.repository }} ` | |
| --token ${{ secrets.GITHUB_TOKEN }} ` | |
| --channel ${{ matrix.rid }} ` | |
| --outputDir releases | |
| - name: Package Windows release | |
| run: | | |
| vpk pack ` | |
| --packId Heartbeat ` | |
| --packVersion ${{ steps.version.outputs.version }} ` | |
| --packDir publish ` | |
| --mainExe Heartbeat.Desktop.Windows.exe ` | |
| --runtime ${{ matrix.rid }} ` | |
| --channel ${{ matrix.rid }} ` | |
| --outputDir releases | |
| - name: Verify Windows release | |
| shell: bash | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| CHANNEL="${{ matrix.rid }}" | |
| test -f "releases/assets.$CHANNEL.json" | |
| test -f "releases/releases.$CHANNEL.json" | |
| test -f "releases/RELEASES-$CHANNEL" | |
| test -f "releases/Heartbeat-$VERSION-$CHANNEL-full.nupkg" | |
| if [ "${{ steps.delta_baseline.outcome }}" = "success" ]; then | |
| test -f "releases/Heartbeat-$VERSION-$CHANNEL-delta.nupkg" | |
| else | |
| echo "::warning::Delta baseline unavailable for $CHANNEL; publishing a full-only release." | |
| fi | |
| test -f "releases/Heartbeat-$CHANNEL-Portable.zip" | |
| test -f "releases/Heartbeat-$CHANNEL-Setup.exe" | |
| - name: Verify packaged Windows artifact and smoke the host | |
| shell: pwsh | |
| run: | | |
| $channel = '${{ matrix.rid }}' | |
| $verifyDir = Join-Path $env:RUNNER_TEMP 'heartbeat-artifact-verification' | |
| if (Test-Path $verifyDir) { Remove-Item $verifyDir -Recurse -Force } | |
| New-Item -ItemType Directory -Path $verifyDir | Out-Null | |
| Expand-Archive -Path "releases/Heartbeat-$channel-Portable.zip" -DestinationPath $verifyDir | |
| # System 是唯一随 Desktop 发布的 BuiltIn Collector;除它以外不允许有任何 Collector Package | |
| # 出现在打包产物里(ADR-048/ADR-049)。 | |
| $manifest = Get-ChildItem $verifyDir -Recurse -File -Filter collector-manifest.json | | |
| Where-Object { $_.FullName -match 'CollectorPackages[\\/]System' } | |
| if (-not $manifest) { throw 'System Collector package is missing from the packaged artifact' } | |
| foreach ($root in Get-ChildItem $verifyDir -Recurse -Directory -Filter CollectorPackages) { | |
| $unexpected = Get-ChildItem $root.FullName -Directory | Where-Object { $_.Name -ne 'System' } | |
| if ($unexpected) { | |
| throw "Desktop artifact must ship no Collector Package other than System: $($unexpected.Name -join ', ')" | |
| } | |
| } | |
| $executable = Get-ChildItem $verifyDir -Recurse -File -Filter Heartbeat.Desktop.Windows.exe | | |
| Select-Object -First 1 | |
| if (-not $executable) { throw 'packaged executable not found' } | |
| if ($channel -ne 'win-x64') { | |
| Write-Host "Skipping startup smoke: $channel binaries cannot run on this runner." | |
| exit 0 | |
| } | |
| # 打包产物只带 System,必须能走完 Host.StartAsync 并干净停止。 | |
| $report = Join-Path $env:RUNNER_TEMP 'startup-smoke.json' | |
| $process = Start-Process -FilePath $executable.FullName ` | |
| -ArgumentList "--verify-startup=$report" -PassThru -Wait | |
| if ($process.ExitCode -ne 0) { | |
| throw "packaged host failed to start (exit $($process.ExitCode))" | |
| } | |
| Get-Content $report | |
| - name: Upload Windows release artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: desktop-${{ matrix.rid }} | |
| path: | | |
| releases/assets.${{ matrix.rid }}.json | |
| releases/releases.${{ matrix.rid }}.json | |
| releases/RELEASES-${{ matrix.rid }} | |
| releases/Heartbeat-${{ steps.version.outputs.version }}-${{ matrix.rid }}-*.nupkg | |
| releases/Heartbeat-${{ matrix.rid }}-Portable.zip | |
| releases/Heartbeat-${{ matrix.rid }}-Setup.exe | |
| if-no-files-found: error | |
| compression-level: 0 | |
| retention-days: 1 | |
| macos: | |
| name: macOS · osx-arm64 | |
| runs-on: macos-15 | |
| timeout-minutes: 45 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| - name: Set up .NET | |
| uses: actions/setup-dotnet@v6 | |
| with: | |
| dotnet-version: ${{ env.DOTNET_VERSION }} | |
| - name: Resolve package version | |
| id: version | |
| shell: bash | |
| run: | | |
| TAG="${GITHUB_REF#refs/tags/v}" | |
| echo "version=$TAG" >> "$GITHUB_OUTPUT" | |
| - name: Run macOS desktop tests | |
| run: dotnet test collection/desktop/Heartbeat.Desktop.Mac.Tests/Heartbeat.Desktop.Mac.Tests.csproj -c Release | |
| - name: Build macOS app | |
| run: | | |
| dotnet publish collection/desktop/Heartbeat.Desktop.Mac/Heartbeat.Desktop.Mac.csproj \ | |
| -c Release \ | |
| -r osx-arm64 \ | |
| --self-contained \ | |
| -p:Version=${{ steps.version.outputs.version }} \ | |
| -o publish-macos | |
| - name: Verify macOS publish delivery contract | |
| run: | | |
| # System 是唯一随 Desktop 发布的 BuiltIn Collector。可选 Collector 都是独立发布单元, | |
| # 宿主产物里不允许出现除 System 以外的任何 Collector Package(ADR-048/ADR-049)。 | |
| test -f publish-macos/CollectorPackages/System/collector-manifest.json | |
| test "$(ls publish-macos/CollectorPackages)" = "System" | |
| - name: Install Velopack CLI | |
| run: dotnet tool install -g vpk --version ${{ env.VELOPACK_VERSION }} | |
| - name: Fetch macOS delta baseline | |
| id: delta_baseline | |
| continue-on-error: true | |
| run: | | |
| vpk download github \ | |
| --repoUrl https://github.com/${{ github.repository }} \ | |
| --token ${{ secrets.GITHUB_TOKEN }} \ | |
| --channel osx-arm64-stable \ | |
| --outputDir releases-macos | |
| - name: Package unsigned macOS release | |
| run: | | |
| vpk pack \ | |
| --packId Heartbeat \ | |
| --packVersion ${{ steps.version.outputs.version }} \ | |
| --packDir publish-macos \ | |
| --mainExe Heartbeat.Desktop.Mac \ | |
| --packTitle Heartbeat \ | |
| --runtime osx-arm64 \ | |
| --channel osx-arm64-stable \ | |
| --bundleId com.shenxianovo.heartbeat \ | |
| --icon "$GITHUB_WORKSPACE/collection/desktop/Heartbeat.Desktop.Mac/heartbeat.icns" \ | |
| --instWelcome "$GITHUB_WORKSPACE/collection/desktop/Heartbeat.Desktop.Mac/macos/INSTALL.txt" \ | |
| --outputDir releases-macos | |
| - name: Constrain Setup to per-user installation | |
| shell: bash | |
| run: | | |
| SETUP_PATH="releases-macos/Heartbeat-osx-arm64-stable-Setup.pkg" | |
| SETUP_WORK="$RUNNER_TEMP/heartbeat-per-user-setup" | |
| mkdir -p "$SETUP_WORK" | |
| pkgutil --expand "$SETUP_PATH" "$SETUP_WORK/expanded" | |
| sed -i '' 's/enable_localSystem="true"/enable_localSystem="false"/' "$SETUP_WORK/expanded/Distribution" | |
| grep -q 'enable_currentUserHome="true"' "$SETUP_WORK/expanded/Distribution" | |
| grep -q 'enable_localSystem="false"' "$SETUP_WORK/expanded/Distribution" | |
| pkgutil --flatten "$SETUP_WORK/expanded" "$SETUP_WORK/per-user.pkg" | |
| mv "$SETUP_WORK/per-user.pkg" "$SETUP_PATH" | |
| - name: Verify unsigned macOS release | |
| shell: bash | |
| run: | | |
| VERSION="${{ steps.version.outputs.version }}" | |
| test -f releases-macos/Heartbeat-osx-arm64-stable-Setup.pkg | |
| test -f releases-macos/Heartbeat-osx-arm64-stable-Portable.zip | |
| test -f releases-macos/releases.osx-arm64-stable.json | |
| test -f releases-macos/RELEASES-osx-arm64-stable | |
| test -f "releases-macos/Heartbeat-$VERSION-osx-arm64-stable-full.nupkg" | |
| if [ "${{ steps.delta_baseline.outcome }}" = "success" ]; then | |
| test -f "releases-macos/Heartbeat-$VERSION-osx-arm64-stable-delta.nupkg" | |
| else | |
| echo "::warning::Delta baseline unavailable for osx-arm64-stable; publishing a full-only release." | |
| fi | |
| VERIFY_DIR="$RUNNER_TEMP/heartbeat-release-verification" | |
| mkdir -p "$VERIFY_DIR/app" | |
| ditto -x -k releases-macos/Heartbeat-osx-arm64-stable-Portable.zip "$VERIFY_DIR/app" | |
| APP_PATH="$VERIFY_DIR/app/Heartbeat.app" | |
| APP_EXECUTABLE="$APP_PATH/Contents/MacOS/Heartbeat.Desktop.Mac" | |
| UPDATE_EXECUTABLE="$APP_PATH/Contents/MacOS/UpdateMac" | |
| test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP_PATH/Contents/Info.plist")" = "com.shenxianovo.heartbeat" | |
| file "$APP_EXECUTABLE" | grep -q 'Mach-O 64-bit executable arm64' | |
| test -x "$UPDATE_EXECUTABLE" | |
| test -f "$APP_PATH/Contents/Resources/sq.version" | |
| codesign -d --verbose=4 "$APP_PATH" 2> "$VERIFY_DIR/signing.txt" || true | |
| ! grep -q 'Authority=Developer ID Application' "$VERIFY_DIR/signing.txt" | |
| pkgutil --check-signature releases-macos/Heartbeat-osx-arm64-stable-Setup.pkg > "$VERIFY_DIR/installer-signing.txt" 2>&1 || true | |
| ! grep -q 'Developer ID Installer' "$VERIFY_DIR/installer-signing.txt" | |
| pkgutil --expand releases-macos/Heartbeat-osx-arm64-stable-Setup.pkg "$VERIFY_DIR/pkg" | |
| grep -q 'enable_currentUserHome="true"' "$VERIFY_DIR/pkg/Distribution" | |
| grep -q 'enable_localSystem="false"' "$VERIFY_DIR/pkg/Distribution" | |
| grep -q 'id="com.shenxianovo.heartbeat"' "$VERIFY_DIR/pkg/Distribution" | |
| grep -q 'Heartbeat-.*-osx-arm64-stable-full.nupkg' releases-macos/releases.osx-arm64-stable.json | |
| # System 随包发布;除 System 以外不允许有任何 Collector Package(ADR-048/ADR-049)。 | |
| test -f "$APP_PATH/Contents/MacOS/CollectorPackages/System/collector-manifest.json" | |
| test "$(ls "$APP_PATH/Contents/MacOS/CollectorPackages")" = "System" | |
| - name: Smoke the packaged macOS host | |
| shell: bash | |
| run: | | |
| APP_EXECUTABLE="$RUNNER_TEMP/heartbeat-release-verification/app/Heartbeat.app/Contents/MacOS/Heartbeat.Desktop.Mac" | |
| REPORT="$RUNNER_TEMP/startup-smoke.json" | |
| "$APP_EXECUTABLE" "--verify-startup=$REPORT" | |
| cat "$REPORT" | |
| - name: Upload macOS release artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: desktop-osx-arm64 | |
| path: | | |
| releases-macos/assets.osx-arm64-stable.json | |
| releases-macos/releases.osx-arm64-stable.json | |
| releases-macos/RELEASES-osx-arm64-stable | |
| releases-macos/Heartbeat-${{ steps.version.outputs.version }}-osx-arm64-stable-*.nupkg | |
| releases-macos/Heartbeat-osx-arm64-stable-Portable.zip | |
| releases-macos/Heartbeat-osx-arm64-stable-Setup.pkg | |
| if-no-files-found: error | |
| compression-level: 0 | |
| retention-days: 1 | |
| github-release: | |
| name: GitHub Release | |
| needs: [windows, macos] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Download desktop release artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: desktop-* | |
| path: all-releases | |
| merge-multiple: true | |
| - name: Build release notes | |
| shell: bash | |
| run: | | |
| PREV_TAG=$(git describe --tags --abbrev=0 "${GITHUB_REF_NAME}^" 2>/dev/null || echo "") | |
| if [ -n "$PREV_TAG" ]; then | |
| RANGE="${PREV_TAG}..${GITHUB_REF_NAME}" | |
| else | |
| RANGE="${GITHUB_REF_NAME}" | |
| fi | |
| NOTES_PATH="$RUNNER_TEMP/release-notes.md" | |
| TAG_REF="refs/heartbeat-release-tags/$GITHUB_REF_NAME" | |
| git fetch --force origin "refs/tags/$GITHUB_REF_NAME:$TAG_REF" | |
| if [ "$(git cat-file -t "$TAG_REF")" = "tag" ]; then | |
| git for-each-ref "$TAG_REF" --format='%(contents)' > "$NOTES_PATH" | |
| else | |
| printf 'Heartbeat %s\n' "$GITHUB_REF_NAME" > "$NOTES_PATH" | |
| fi | |
| COMMIT_COUNT=$(git rev-list --count "$RANGE") | |
| { | |
| echo "" | |
| echo "<details>" | |
| echo "<summary>提交明细(${COMMIT_COUNT} commits)</summary>" | |
| echo "" | |
| git log --pretty=format:"- %s" "$RANGE" | |
| echo "" | |
| echo "</details>" | |
| } >> "$NOTES_PATH" | |
| - name: Publish GitHub Release | |
| uses: softprops/action-gh-release@v3 | |
| with: | |
| files: all-releases/* | |
| tag_name: ${{ github.ref_name }} | |
| body_path: ${{ runner.temp }}/release-notes.md |