diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 870464a..2c094a6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,148 +1,193 @@ -name: Release - -on: - pull_request: - branches: [main] - types: [closed] - -permissions: - contents: write - -jobs: - release: - if: github.event.pull_request.merged == true && github.event.pull_request.head.ref == 'dev' - runs-on: windows-latest - - steps: - - uses: actions/checkout@v4 - - - name: Get version - id: version - shell: pwsh - run: | - $version = ([xml](Get-Content src/PlanViewer.App/PlanViewer.App.csproj)).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 8.0 - if: steps.check.outputs.EXISTS == 'false' - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8.0.x - - - name: Build and test - if: steps.check.outputs.EXISTS == 'false' - 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) - if: steps.check.outputs.EXISTS == 'false' - 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 - - - name: Create Velopack release (Windows) - if: steps.check.outputs.EXISTS == 'false' - shell: pwsh - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ steps.version.outputs.VERSION }} - run: | - dotnet tool install -g vpk - 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 - vpk pack -u PerformanceStudio -v $env:VERSION -p publish/win-x64 -e PlanViewer.App.exe -o releases/velopack --channel win - - - name: Package and upload - if: steps.check.outputs.EXISTS == 'false' - shell: pwsh - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ steps.version.outputs.VERSION }} - run: | - New-Item -ItemType Directory -Force -Path releases - - # Package Windows and Linux as flat zips - foreach ($rid in @('win-x64', 'linux-x64')) { - if (Test-Path 'README.md') { Copy-Item 'README.md' "publish/$rid/" } - if (Test-Path 'LICENSE') { Copy-Item 'LICENSE' "publish/$rid/" } - Compress-Archive -Path "publish/$rid/*" -DestinationPath "releases/PerformanceStudio-$rid.zip" -Force - } - - # 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 '(CFBundleVersion\s*)[^<]*()', "`${1}$env:VERSION`${2}" - $plist = $plist -replace '(CFBundleShortVersionString\s*)[^<]*()', "`${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/" } - - Compress-Archive -Path "$wrapperDir/*" -DestinationPath "releases/PerformanceStudio-$rid.zip" -Force - } - - # 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 +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@v4 + + - name: Get version + id: version + shell: pwsh + run: | + $version = ([xml](Get-Content src/PlanViewer.App/PlanViewer.App.csproj)).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 8.0 + if: steps.check.outputs.EXISTS == 'false' + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Build and test + if: steps.check.outputs.EXISTS == 'false' + 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) + if: steps.check.outputs.EXISTS == 'false' + 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 + + # ── SignPath code signing (Windows only, skipped if secret not configured) ── + - name: Check if signing is configured + if: steps.check.outputs.EXISTS == 'false' + id: signing + shell: bash + run: | + if [ -n "${{ secrets.SIGNPATH_API_TOKEN }}" ]; then + echo "ENABLED=true" >> $GITHUB_OUTPUT + else + echo "ENABLED=false" >> $GITHUB_OUTPUT + echo "::warning::SIGNPATH_API_TOKEN not configured — releasing unsigned binaries" + fi + + - name: Upload Windows build for signing + if: steps.check.outputs.EXISTS == 'false' && steps.signing.outputs.ENABLED == 'true' + id: upload-unsigned + uses: actions/upload-artifact@v4 + with: + name: App-unsigned + path: publish/win-x64/ + + - name: Sign Windows build + if: steps.check.outputs.EXISTS == 'false' && steps.signing.outputs.ENABLED == 'true' + uses: signpath/github-action-submit-signing-request@v1 + with: + api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' + organization-id: '7969f8b6-d946-4a74-9bac-a55856d8b8e0' + project-slug: 'PerformanceStudio' + signing-policy-slug: 'test-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 + if: steps.check.outputs.EXISTS == 'false' && steps.signing.outputs.ENABLED == 'true' + 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) + if: steps.check.outputs.EXISTS == 'false' + shell: pwsh + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.version.outputs.VERSION }} + run: | + dotnet tool install -g vpk + 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 + if: steps.check.outputs.EXISTS == 'false' + shell: pwsh + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.version.outputs.VERSION }} + run: | + New-Item -ItemType Directory -Force -Path releases + + # Package Windows (signed) and Linux as flat zips + foreach ($rid in @('win-x64', 'linux-x64')) { + if (Test-Path 'README.md') { Copy-Item 'README.md' "publish/$rid/" } + if (Test-Path 'LICENSE') { Copy-Item 'LICENSE' "publish/$rid/" } + Compress-Archive -Path "publish/$rid/*" -DestinationPath "releases/PerformanceStudio-$rid.zip" -Force + } + + # 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 '(CFBundleVersion\s*)[^<]*()', "`${1}$env:VERSION`${2}" + $plist = $plist -replace '(CFBundleShortVersionString\s*)[^<]*()', "`${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/" } + + Compress-Archive -Path "$wrapperDir/*" -DestinationPath "releases/PerformanceStudio-$rid.zip" -Force + } + + # 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 diff --git a/README.md b/README.md index e5a3b71..19a5174 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,21 @@ # Performance Studio +

+ GitHub Stars + GitHub Forks + License: MIT + Latest Release + Open Issues + Last Commit + CI +

+

+ Follow @ErikDarlingData on X + YouTube Subscribe + LinkedIn Connect + Blog +

+ A cross-platform SQL Server execution plan analyzer with built-in MCP server for AI-assisted analysis. Parses `.sqlplan` XML, identifies performance problems, suggests missing indexes, and provides actionable warnings — from the command line or a desktop GUI. Built for developers and DBAs who want fast, automated plan analysis without clicking through SSMS. diff --git a/src/PlanViewer.App/Controls/PlanViewerControl.axaml b/src/PlanViewer.App/Controls/PlanViewerControl.axaml index db9d492..1eec781 100644 --- a/src/PlanViewer.App/Controls/PlanViewerControl.axaml +++ b/src/PlanViewer.App/Controls/PlanViewerControl.axaml @@ -14,6 +14,20 @@ BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1"> + + + + + + + + - - + - + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + - - +