Skip to content

Desktop Release · v4.1.0 #27

Desktop Release · v4.1.0

Desktop Release · v4.1.0 #27

Workflow file for this run

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