Release v1.14.1 #65
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: Release | |
| on: | |
| pull_request: | |
| branches: [main] | |
| types: [closed] | |
| permissions: | |
| contents: write | |
| id-token: write | |
| actions: read | |
| jobs: | |
| release: | |
| if: github.event.pull_request.merged == true && github.event.pull_request.head.ref == 'dev' | |
| runs-on: windows-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Get version | |
| id: version | |
| shell: pwsh | |
| run: | | |
| $version = ([xml](Get-Content src/Directory.Build.props)).Project.PropertyGroup.Version | Where-Object { $_ } | |
| echo "VERSION=$version" >> $env:GITHUB_OUTPUT | |
| - name: Check if release already exists | |
| id: check | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| if gh release view "v${{ steps.version.outputs.VERSION }}" > /dev/null 2>&1; then | |
| echo "EXISTS=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "EXISTS=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Create release | |
| if: steps.check.outputs.EXISTS == 'false' | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh release create "v${{ steps.version.outputs.VERSION }}" --title "v${{ steps.version.outputs.VERSION }}" --generate-notes --target main | |
| - name: Setup .NET 10.0 | |
| uses: actions/setup-dotnet@v5 | |
| with: | |
| dotnet-version: 10.0.x | |
| - name: Build and test | |
| run: | | |
| dotnet restore | |
| dotnet build -c Release | |
| dotnet test tests/PlanViewer.Core.Tests/PlanViewer.Core.Tests.csproj -c Release --no-build --verbosity normal | |
| - name: Publish App (all platforms) | |
| run: | | |
| dotnet publish src/PlanViewer.App/PlanViewer.App.csproj -c Release -r win-x64 --self-contained -o publish/win-x64 | |
| dotnet publish src/PlanViewer.App/PlanViewer.App.csproj -c Release -r linux-x64 --self-contained -o publish/linux-x64 | |
| dotnet publish src/PlanViewer.App/PlanViewer.App.csproj -c Release -r osx-x64 --self-contained -o publish/osx-x64 | |
| dotnet publish src/PlanViewer.App/PlanViewer.App.csproj -c Release -r osx-arm64 --self-contained -o publish/osx-arm64 | |
| # ── SSMS extension VSIX (issue 343 — get it into automated builds) ── | |
| # PlanViewer.Ssms is a legacy non-SDK project and is not in PlanViewer.sln, | |
| # so the `dotnet build` above never touches it. It needs full MSBuild plus | |
| # the VSSDK build targets from the Microsoft.VSSDK.BuildTools package. | |
| # The build step never fails the job (it only sets an output on success), | |
| # so a VSIX build failure can never block the cross-platform app release. | |
| - name: Add MSBuild to PATH | |
| uses: microsoft/setup-msbuild@v3 | |
| continue-on-error: true | |
| - name: Build SSMS extension | |
| id: ssms | |
| continue-on-error: true | |
| shell: pwsh | |
| env: | |
| VERSION: ${{ steps.version.outputs.VERSION }} | |
| run: | | |
| $manifest = 'src/PlanViewer.Ssms/source.extension.vsixmanifest' | |
| $manifestVersion = ([xml](Get-Content $manifest)).PackageManifest.Metadata.Identity.Version | |
| if ($manifestVersion -ne $env:VERSION) { | |
| Write-Host "::warning::VSIX manifest version ($manifestVersion) does not match release version ($env:VERSION) - bump source.extension.vsixmanifest and Properties/AssemblyInfo.cs" | |
| } | |
| # Use -restore (not the -t:Restore,Build target list) so the | |
| # package-generated props land in a fresh evaluation before Build. | |
| # Those props redirect VSToolsPath into the Microsoft.VSSDK.BuildTools | |
| # package, which is how the VSSDK targets resolve on a runner that | |
| # lacks the Visual Studio extension-development workload. | |
| msbuild src/PlanViewer.Ssms/PlanViewer.Ssms.csproj -restore -t:Build -p:Configuration=Release -p:DeployExtension=false | |
| dotnet build src/PlanViewer.Ssms.Installer/PlanViewer.Ssms.Installer.csproj -c Release | |
| $vsix = 'src/PlanViewer.Ssms/bin/Release/PlanViewer.Ssms.vsix' | |
| $exe = 'src/PlanViewer.Ssms.Installer/bin/Release/net472/InstallSsmsExtension.exe' | |
| if ((Test-Path $vsix) -and (Test-Path $exe)) { | |
| New-Item -ItemType Directory -Force -Path releases | Out-Null | |
| Copy-Item $vsix 'releases/PlanViewer.Ssms.vsix' | |
| Copy-Item $exe 'releases/InstallSsmsExtension.exe' | |
| "BUILT=true" >> $env:GITHUB_OUTPUT | |
| Write-Host "SSMS extension built: $vsix" | |
| } else { | |
| Write-Host "::warning::SSMS extension did not produce the expected artifacts - release published without the VSIX (issue 343)" | |
| } | |
| - name: Upload SSMS extension to release | |
| if: steps.ssms.outputs.BUILT == 'true' | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ steps.version.outputs.VERSION }} | |
| run: gh release upload "v$env:VERSION" releases/PlanViewer.Ssms.vsix releases/InstallSsmsExtension.exe --clobber | |
| # Publish to the SSMS Gallery (https://ssmsgallery.azurewebsites.net, issue 343). | |
| # The gallery extracts version/description from the vsixmanifest, so the | |
| # upload filename is irrelevant. continue-on-error: a gallery outage must | |
| # never fail the release. bash so `curl` is curl.exe, not the pwsh alias. | |
| - name: Publish SSMS extension to SSMS Gallery | |
| if: steps.ssms.outputs.BUILT == 'true' | |
| continue-on-error: true | |
| shell: bash | |
| run: | | |
| echo "Uploading PlanViewer.Ssms.vsix to the SSMS Gallery..." | |
| curl -sS -f "https://ssmsgallery.azurewebsites.net/api/upload" \ | |
| -F "file=@releases/PlanViewer.Ssms.vsix" | |
| # ── SignPath code signing (Windows). Signing is REQUIRED for a release: | |
| # if the SignPath token is missing the job fails loudly rather than | |
| # silently shipping unsigned binaries. ── | |
| - name: Verify signing is configured | |
| shell: bash | |
| env: | |
| SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }} | |
| run: | | |
| if [ -z "$SIGNPATH_API_TOKEN" ]; then | |
| echo "::error::SIGNPATH_API_TOKEN missing — signing is required for a release; aborting." | |
| exit 1 | |
| fi | |
| echo "SignPath token present — Windows binaries will be signed." | |
| - name: Upload Windows build for signing | |
| id: upload-unsigned | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: App-unsigned | |
| path: publish/win-x64/ | |
| - name: Sign Windows build | |
| uses: signpath/github-action-submit-signing-request@v2 | |
| with: | |
| api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' | |
| organization-id: '7969f8b6-d946-4a74-9bac-a55856d8b8e0' | |
| project-slug: 'PerformanceStudio' | |
| signing-policy-slug: 'release-signing' | |
| artifact-configuration-slug: 'App' | |
| github-artifact-id: '${{ steps.upload-unsigned.outputs.artifact-id }}' | |
| wait-for-completion: true | |
| output-artifact-directory: 'signed/win-x64' | |
| - name: Replace unsigned Windows build with signed | |
| shell: pwsh | |
| run: | | |
| Remove-Item -Recurse -Force publish/win-x64 | |
| Copy-Item -Recurse signed/win-x64 publish/win-x64 | |
| # ── Velopack (uses signed Windows binaries) ─────────────────────── | |
| - name: Create Velopack release (Windows) | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ steps.version.outputs.VERSION }} | |
| run: | | |
| # Pin vpk to match the Velopack PackageReference (Velopack recommends the | |
| # CLI and library versions match for compatible packages + reproducible releases). | |
| dotnet tool install -g vpk --version 1.2.0 | |
| New-Item -ItemType Directory -Force -Path releases/velopack | |
| # Download previous release for delta generation | |
| vpk download github --repoUrl https://github.com/${{ github.repository }} --channel win -o releases/velopack --token $env:GH_TOKEN | |
| # Pack Windows release (now signed) | |
| vpk pack -u PerformanceStudio -v $env:VERSION -p publish/win-x64 -e PlanViewer.App.exe -o releases/velopack --channel win | |
| # ── Package and upload ──────────────────────────────────────────── | |
| - name: Package and upload | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| VERSION: ${{ steps.version.outputs.VERSION }} | |
| run: | | |
| New-Item -ItemType Directory -Force -Path releases | |
| # Windows (signed): flat zip via Compress-Archive. Windows has no Unix | |
| # execute bit, so the signed .exe needs no special handling. | |
| if (Test-Path 'README.md') { Copy-Item 'README.md' 'publish/win-x64/' } | |
| if (Test-Path 'LICENSE') { Copy-Item 'LICENSE' 'publish/win-x64/' } | |
| Compress-Archive -Path 'publish/win-x64/*' -DestinationPath 'releases/PerformanceStudio-win-x64.zip' -Force | |
| # Linux: flat zip via zip_with_exec.py so the apphost keeps its execute | |
| # bit. Compress-Archive (and .NET's ZipArchive) stamp the archive's host | |
| # byte as Windows, so unzip/macOS/Linux drop the Unix mode and the binary | |
| # extracts non-executable -- it then won't launch ("permission denied"). | |
| if (Test-Path 'README.md') { Copy-Item 'README.md' 'publish/linux-x64/' } | |
| if (Test-Path 'LICENSE') { Copy-Item 'LICENSE' 'publish/linux-x64/' } | |
| python .github/scripts/zip_with_exec.py publish/linux-x64 releases/PerformanceStudio-linux-x64.zip --exec PlanViewer.App --exec-optional createdump | |
| # Package macOS as proper .app bundles | |
| foreach ($rid in @('osx-x64', 'osx-arm64')) { | |
| $appName = "PerformanceStudio.app" | |
| $bundleDir = "publish/$rid-bundle/$appName" | |
| # Create .app bundle structure | |
| New-Item -ItemType Directory -Force -Path "$bundleDir/Contents/MacOS" | |
| New-Item -ItemType Directory -Force -Path "$bundleDir/Contents/Resources" | |
| # Copy all published files into Contents/MacOS | |
| Copy-Item -Path "publish/$rid/*" -Destination "$bundleDir/Contents/MacOS/" -Recurse | |
| # Move Info.plist to Contents/ (it was copied to MacOS/ with the publish output) | |
| if (Test-Path "$bundleDir/Contents/MacOS/Info.plist") { | |
| Move-Item -Path "$bundleDir/Contents/MacOS/Info.plist" -Destination "$bundleDir/Contents/Info.plist" -Force | |
| } | |
| # Update version in Info.plist to match csproj | |
| $plist = Get-Content "$bundleDir/Contents/Info.plist" -Raw | |
| $plist = $plist -replace '(<key>CFBundleVersion</key>\s*<string>)[^<]*(</string>)', "`${1}$env:VERSION`${2}" | |
| $plist = $plist -replace '(<key>CFBundleShortVersionString</key>\s*<string>)[^<]*(</string>)', "`${1}$env:VERSION`${2}" | |
| Set-Content -Path "$bundleDir/Contents/Info.plist" -Value $plist -NoNewline | |
| # Move icon to Contents/Resources | |
| if (Test-Path "$bundleDir/Contents/MacOS/EDD.icns") { | |
| Move-Item -Path "$bundleDir/Contents/MacOS/EDD.icns" -Destination "$bundleDir/Contents/Resources/EDD.icns" -Force | |
| } | |
| # Add README and LICENSE alongside the .app bundle | |
| $wrapperDir = "publish/$rid-bundle" | |
| if (Test-Path 'README.md') { Copy-Item 'README.md' "$wrapperDir/" } | |
| if (Test-Path 'LICENSE') { Copy-Item 'LICENSE' "$wrapperDir/" } | |
| # Zip the bundle with zip_with_exec.py so the apphost inside the .app | |
| # keeps its execute bit (Compress-Archive would strip it, leaving a | |
| # bundle macOS refuses to launch). | |
| python .github/scripts/zip_with_exec.py "$wrapperDir" "releases/PerformanceStudio-$rid.zip" --exec "PerformanceStudio.app/Contents/MacOS/PlanViewer.App" --exec-optional "PerformanceStudio.app/Contents/MacOS/createdump" | |
| } | |
| # Checksums (zips only, Velopack has its own checksums) | |
| $checksums = Get-ChildItem releases/*.zip | ForEach-Object { | |
| $hash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash.ToLower() | |
| "$hash $($_.Name)" | |
| } | |
| $checksums | Out-File -FilePath releases/SHA256SUMS.txt -Encoding utf8 | |
| Write-Host "Checksums:" | |
| $checksums | ForEach-Object { Write-Host $_ } | |
| # Upload zips + checksums | |
| gh release upload "v$env:VERSION" releases/*.zip releases/SHA256SUMS.txt --clobber | |
| # Upload Velopack artifacts | |
| vpk upload github --repoUrl https://github.com/${{ github.repository }} --channel win -o releases/velopack --releaseName "v$env:VERSION" --tag "v$env:VERSION" --merge --token $env:GH_TOKEN |