Skip to content

Commit ba7e5dc

Browse files
Merge pull request #173 from erikdarlingdata/dev
Release v1.4.0
2 parents e887984 + fbb182b commit ba7e5dc

27 files changed

Lines changed: 4720 additions & 402 deletions

.github/workflows/release.yml

Lines changed: 193 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,148 +1,193 @@
1-
name: Release
2-
3-
on:
4-
pull_request:
5-
branches: [main]
6-
types: [closed]
7-
8-
permissions:
9-
contents: write
10-
11-
jobs:
12-
release:
13-
if: github.event.pull_request.merged == true && github.event.pull_request.head.ref == 'dev'
14-
runs-on: windows-latest
15-
16-
steps:
17-
- uses: actions/checkout@v4
18-
19-
- name: Get version
20-
id: version
21-
shell: pwsh
22-
run: |
23-
$version = ([xml](Get-Content src/PlanViewer.App/PlanViewer.App.csproj)).Project.PropertyGroup.Version | Where-Object { $_ }
24-
echo "VERSION=$version" >> $env:GITHUB_OUTPUT
25-
26-
- name: Check if release already exists
27-
id: check
28-
shell: bash
29-
env:
30-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31-
run: |
32-
if gh release view "v${{ steps.version.outputs.VERSION }}" > /dev/null 2>&1; then
33-
echo "EXISTS=true" >> $GITHUB_OUTPUT
34-
else
35-
echo "EXISTS=false" >> $GITHUB_OUTPUT
36-
fi
37-
38-
- name: Create release
39-
if: steps.check.outputs.EXISTS == 'false'
40-
shell: bash
41-
env:
42-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43-
run: |
44-
gh release create "v${{ steps.version.outputs.VERSION }}" --title "v${{ steps.version.outputs.VERSION }}" --generate-notes --target main
45-
46-
- name: Setup .NET 8.0
47-
if: steps.check.outputs.EXISTS == 'false'
48-
uses: actions/setup-dotnet@v4
49-
with:
50-
dotnet-version: 8.0.x
51-
52-
- name: Build and test
53-
if: steps.check.outputs.EXISTS == 'false'
54-
run: |
55-
dotnet restore
56-
dotnet build -c Release
57-
dotnet test tests/PlanViewer.Core.Tests/PlanViewer.Core.Tests.csproj -c Release --no-build --verbosity normal
58-
59-
- name: Publish App (all platforms)
60-
if: steps.check.outputs.EXISTS == 'false'
61-
run: |
62-
dotnet publish src/PlanViewer.App/PlanViewer.App.csproj -c Release -r win-x64 --self-contained -o publish/win-x64
63-
dotnet publish src/PlanViewer.App/PlanViewer.App.csproj -c Release -r linux-x64 --self-contained -o publish/linux-x64
64-
dotnet publish src/PlanViewer.App/PlanViewer.App.csproj -c Release -r osx-x64 --self-contained -o publish/osx-x64
65-
dotnet publish src/PlanViewer.App/PlanViewer.App.csproj -c Release -r osx-arm64 --self-contained -o publish/osx-arm64
66-
67-
- name: Create Velopack release (Windows)
68-
if: steps.check.outputs.EXISTS == 'false'
69-
shell: pwsh
70-
env:
71-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
72-
VERSION: ${{ steps.version.outputs.VERSION }}
73-
run: |
74-
dotnet tool install -g vpk
75-
New-Item -ItemType Directory -Force -Path releases/velopack
76-
77-
# Download previous release for delta generation
78-
vpk download github --repoUrl https://github.com/${{ github.repository }} --channel win -o releases/velopack --token $env:GH_TOKEN
79-
80-
# Pack Windows release
81-
vpk pack -u PerformanceStudio -v $env:VERSION -p publish/win-x64 -e PlanViewer.App.exe -o releases/velopack --channel win
82-
83-
- name: Package and upload
84-
if: steps.check.outputs.EXISTS == 'false'
85-
shell: pwsh
86-
env:
87-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
88-
VERSION: ${{ steps.version.outputs.VERSION }}
89-
run: |
90-
New-Item -ItemType Directory -Force -Path releases
91-
92-
# Package Windows and Linux as flat zips
93-
foreach ($rid in @('win-x64', 'linux-x64')) {
94-
if (Test-Path 'README.md') { Copy-Item 'README.md' "publish/$rid/" }
95-
if (Test-Path 'LICENSE') { Copy-Item 'LICENSE' "publish/$rid/" }
96-
Compress-Archive -Path "publish/$rid/*" -DestinationPath "releases/PerformanceStudio-$rid.zip" -Force
97-
}
98-
99-
# Package macOS as proper .app bundles
100-
foreach ($rid in @('osx-x64', 'osx-arm64')) {
101-
$appName = "PerformanceStudio.app"
102-
$bundleDir = "publish/$rid-bundle/$appName"
103-
104-
# Create .app bundle structure
105-
New-Item -ItemType Directory -Force -Path "$bundleDir/Contents/MacOS"
106-
New-Item -ItemType Directory -Force -Path "$bundleDir/Contents/Resources"
107-
108-
# Copy all published files into Contents/MacOS
109-
Copy-Item -Path "publish/$rid/*" -Destination "$bundleDir/Contents/MacOS/" -Recurse
110-
111-
# Move Info.plist to Contents/ (it was copied to MacOS/ with the publish output)
112-
if (Test-Path "$bundleDir/Contents/MacOS/Info.plist") {
113-
Move-Item -Path "$bundleDir/Contents/MacOS/Info.plist" -Destination "$bundleDir/Contents/Info.plist" -Force
114-
}
115-
116-
# Update version in Info.plist to match csproj
117-
$plist = Get-Content "$bundleDir/Contents/Info.plist" -Raw
118-
$plist = $plist -replace '(<key>CFBundleVersion</key>\s*<string>)[^<]*(</string>)', "`${1}$env:VERSION`${2}"
119-
$plist = $plist -replace '(<key>CFBundleShortVersionString</key>\s*<string>)[^<]*(</string>)', "`${1}$env:VERSION`${2}"
120-
Set-Content -Path "$bundleDir/Contents/Info.plist" -Value $plist -NoNewline
121-
122-
# Move icon to Contents/Resources
123-
if (Test-Path "$bundleDir/Contents/MacOS/EDD.icns") {
124-
Move-Item -Path "$bundleDir/Contents/MacOS/EDD.icns" -Destination "$bundleDir/Contents/Resources/EDD.icns" -Force
125-
}
126-
127-
# Add README and LICENSE alongside the .app bundle
128-
$wrapperDir = "publish/$rid-bundle"
129-
if (Test-Path 'README.md') { Copy-Item 'README.md' "$wrapperDir/" }
130-
if (Test-Path 'LICENSE') { Copy-Item 'LICENSE' "$wrapperDir/" }
131-
132-
Compress-Archive -Path "$wrapperDir/*" -DestinationPath "releases/PerformanceStudio-$rid.zip" -Force
133-
}
134-
135-
# Checksums (zips only, Velopack has its own checksums)
136-
$checksums = Get-ChildItem releases/*.zip | ForEach-Object {
137-
$hash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash.ToLower()
138-
"$hash $($_.Name)"
139-
}
140-
$checksums | Out-File -FilePath releases/SHA256SUMS.txt -Encoding utf8
141-
Write-Host "Checksums:"
142-
$checksums | ForEach-Object { Write-Host $_ }
143-
144-
# Upload zips + checksums
145-
gh release upload "v$env:VERSION" releases/*.zip releases/SHA256SUMS.txt --clobber
146-
147-
# Upload Velopack artifacts
148-
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
1+
name: Release
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
types: [closed]
7+
8+
permissions:
9+
contents: write
10+
id-token: write
11+
actions: read
12+
13+
jobs:
14+
release:
15+
if: github.event.pull_request.merged == true && github.event.pull_request.head.ref == 'dev'
16+
runs-on: windows-latest
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- name: Get version
22+
id: version
23+
shell: pwsh
24+
run: |
25+
$version = ([xml](Get-Content src/PlanViewer.App/PlanViewer.App.csproj)).Project.PropertyGroup.Version | Where-Object { $_ }
26+
echo "VERSION=$version" >> $env:GITHUB_OUTPUT
27+
28+
- name: Check if release already exists
29+
id: check
30+
shell: bash
31+
env:
32+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33+
run: |
34+
if gh release view "v${{ steps.version.outputs.VERSION }}" > /dev/null 2>&1; then
35+
echo "EXISTS=true" >> $GITHUB_OUTPUT
36+
else
37+
echo "EXISTS=false" >> $GITHUB_OUTPUT
38+
fi
39+
40+
- name: Create release
41+
if: steps.check.outputs.EXISTS == 'false'
42+
shell: bash
43+
env:
44+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45+
run: |
46+
gh release create "v${{ steps.version.outputs.VERSION }}" --title "v${{ steps.version.outputs.VERSION }}" --generate-notes --target main
47+
48+
- name: Setup .NET 8.0
49+
if: steps.check.outputs.EXISTS == 'false'
50+
uses: actions/setup-dotnet@v4
51+
with:
52+
dotnet-version: 8.0.x
53+
54+
- name: Build and test
55+
if: steps.check.outputs.EXISTS == 'false'
56+
run: |
57+
dotnet restore
58+
dotnet build -c Release
59+
dotnet test tests/PlanViewer.Core.Tests/PlanViewer.Core.Tests.csproj -c Release --no-build --verbosity normal
60+
61+
- name: Publish App (all platforms)
62+
if: steps.check.outputs.EXISTS == 'false'
63+
run: |
64+
dotnet publish src/PlanViewer.App/PlanViewer.App.csproj -c Release -r win-x64 --self-contained -o publish/win-x64
65+
dotnet publish src/PlanViewer.App/PlanViewer.App.csproj -c Release -r linux-x64 --self-contained -o publish/linux-x64
66+
dotnet publish src/PlanViewer.App/PlanViewer.App.csproj -c Release -r osx-x64 --self-contained -o publish/osx-x64
67+
dotnet publish src/PlanViewer.App/PlanViewer.App.csproj -c Release -r osx-arm64 --self-contained -o publish/osx-arm64
68+
69+
# ── SignPath code signing (Windows only, skipped if secret not configured) ──
70+
- name: Check if signing is configured
71+
if: steps.check.outputs.EXISTS == 'false'
72+
id: signing
73+
shell: bash
74+
run: |
75+
if [ -n "${{ secrets.SIGNPATH_API_TOKEN }}" ]; then
76+
echo "ENABLED=true" >> $GITHUB_OUTPUT
77+
else
78+
echo "ENABLED=false" >> $GITHUB_OUTPUT
79+
echo "::warning::SIGNPATH_API_TOKEN not configured — releasing unsigned binaries"
80+
fi
81+
82+
- name: Upload Windows build for signing
83+
if: steps.check.outputs.EXISTS == 'false' && steps.signing.outputs.ENABLED == 'true'
84+
id: upload-unsigned
85+
uses: actions/upload-artifact@v4
86+
with:
87+
name: App-unsigned
88+
path: publish/win-x64/
89+
90+
- name: Sign Windows build
91+
if: steps.check.outputs.EXISTS == 'false' && steps.signing.outputs.ENABLED == 'true'
92+
uses: signpath/github-action-submit-signing-request@v1
93+
with:
94+
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
95+
organization-id: '7969f8b6-d946-4a74-9bac-a55856d8b8e0'
96+
project-slug: 'PerformanceStudio'
97+
signing-policy-slug: 'test-signing'
98+
artifact-configuration-slug: 'App'
99+
github-artifact-id: '${{ steps.upload-unsigned.outputs.artifact-id }}'
100+
wait-for-completion: true
101+
output-artifact-directory: 'signed/win-x64'
102+
103+
- name: Replace unsigned Windows build with signed
104+
if: steps.check.outputs.EXISTS == 'false' && steps.signing.outputs.ENABLED == 'true'
105+
shell: pwsh
106+
run: |
107+
Remove-Item -Recurse -Force publish/win-x64
108+
Copy-Item -Recurse signed/win-x64 publish/win-x64
109+
110+
# ── Velopack (uses signed Windows binaries) ───────────────────────
111+
- name: Create Velopack release (Windows)
112+
if: steps.check.outputs.EXISTS == 'false'
113+
shell: pwsh
114+
env:
115+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
116+
VERSION: ${{ steps.version.outputs.VERSION }}
117+
run: |
118+
dotnet tool install -g vpk
119+
New-Item -ItemType Directory -Force -Path releases/velopack
120+
121+
# Download previous release for delta generation
122+
vpk download github --repoUrl https://github.com/${{ github.repository }} --channel win -o releases/velopack --token $env:GH_TOKEN
123+
124+
# Pack Windows release (now signed)
125+
vpk pack -u PerformanceStudio -v $env:VERSION -p publish/win-x64 -e PlanViewer.App.exe -o releases/velopack --channel win
126+
127+
# ── Package and upload ────────────────────────────────────────────
128+
- name: Package and upload
129+
if: steps.check.outputs.EXISTS == 'false'
130+
shell: pwsh
131+
env:
132+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
133+
VERSION: ${{ steps.version.outputs.VERSION }}
134+
run: |
135+
New-Item -ItemType Directory -Force -Path releases
136+
137+
# Package Windows (signed) and Linux as flat zips
138+
foreach ($rid in @('win-x64', 'linux-x64')) {
139+
if (Test-Path 'README.md') { Copy-Item 'README.md' "publish/$rid/" }
140+
if (Test-Path 'LICENSE') { Copy-Item 'LICENSE' "publish/$rid/" }
141+
Compress-Archive -Path "publish/$rid/*" -DestinationPath "releases/PerformanceStudio-$rid.zip" -Force
142+
}
143+
144+
# Package macOS as proper .app bundles
145+
foreach ($rid in @('osx-x64', 'osx-arm64')) {
146+
$appName = "PerformanceStudio.app"
147+
$bundleDir = "publish/$rid-bundle/$appName"
148+
149+
# Create .app bundle structure
150+
New-Item -ItemType Directory -Force -Path "$bundleDir/Contents/MacOS"
151+
New-Item -ItemType Directory -Force -Path "$bundleDir/Contents/Resources"
152+
153+
# Copy all published files into Contents/MacOS
154+
Copy-Item -Path "publish/$rid/*" -Destination "$bundleDir/Contents/MacOS/" -Recurse
155+
156+
# Move Info.plist to Contents/ (it was copied to MacOS/ with the publish output)
157+
if (Test-Path "$bundleDir/Contents/MacOS/Info.plist") {
158+
Move-Item -Path "$bundleDir/Contents/MacOS/Info.plist" -Destination "$bundleDir/Contents/Info.plist" -Force
159+
}
160+
161+
# Update version in Info.plist to match csproj
162+
$plist = Get-Content "$bundleDir/Contents/Info.plist" -Raw
163+
$plist = $plist -replace '(<key>CFBundleVersion</key>\s*<string>)[^<]*(</string>)', "`${1}$env:VERSION`${2}"
164+
$plist = $plist -replace '(<key>CFBundleShortVersionString</key>\s*<string>)[^<]*(</string>)', "`${1}$env:VERSION`${2}"
165+
Set-Content -Path "$bundleDir/Contents/Info.plist" -Value $plist -NoNewline
166+
167+
# Move icon to Contents/Resources
168+
if (Test-Path "$bundleDir/Contents/MacOS/EDD.icns") {
169+
Move-Item -Path "$bundleDir/Contents/MacOS/EDD.icns" -Destination "$bundleDir/Contents/Resources/EDD.icns" -Force
170+
}
171+
172+
# Add README and LICENSE alongside the .app bundle
173+
$wrapperDir = "publish/$rid-bundle"
174+
if (Test-Path 'README.md') { Copy-Item 'README.md' "$wrapperDir/" }
175+
if (Test-Path 'LICENSE') { Copy-Item 'LICENSE' "$wrapperDir/" }
176+
177+
Compress-Archive -Path "$wrapperDir/*" -DestinationPath "releases/PerformanceStudio-$rid.zip" -Force
178+
}
179+
180+
# Checksums (zips only, Velopack has its own checksums)
181+
$checksums = Get-ChildItem releases/*.zip | ForEach-Object {
182+
$hash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash.ToLower()
183+
"$hash $($_.Name)"
184+
}
185+
$checksums | Out-File -FilePath releases/SHA256SUMS.txt -Encoding utf8
186+
Write-Host "Checksums:"
187+
$checksums | ForEach-Object { Write-Host $_ }
188+
189+
# Upload zips + checksums
190+
gh release upload "v$env:VERSION" releases/*.zip releases/SHA256SUMS.txt --clobber
191+
192+
# Upload Velopack artifacts
193+
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

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
# Performance Studio
22

3+
<p align="center">
4+
<a href="https://github.com/erikdarlingdata/PerformanceStudio/stargazers"><img src="https://img.shields.io/github/stars/erikdarlingdata/PerformanceStudio?style=for-the-badge&logo=github&color=gold&logoColor=black" alt="GitHub Stars"></a>
5+
<a href="https://github.com/erikdarlingdata/PerformanceStudio/network/members"><img src="https://img.shields.io/github/forks/erikdarlingdata/PerformanceStudio?style=for-the-badge&logo=github" alt="GitHub Forks"></a>
6+
<a href="https://github.com/erikdarlingdata/PerformanceStudio/blob/main/LICENSE"><img src="https://img.shields.io/github/license/erikdarlingdata/PerformanceStudio?style=for-the-badge" alt="License: MIT"></a>
7+
<a href="https://github.com/erikdarlingdata/PerformanceStudio/releases/latest"><img src="https://img.shields.io/github/v/release/erikdarlingdata/PerformanceStudio?style=for-the-badge" alt="Latest Release"></a>
8+
<a href="https://github.com/erikdarlingdata/PerformanceStudio/issues"><img src="https://img.shields.io/github/issues/erikdarlingdata/PerformanceStudio?style=for-the-badge" alt="Open Issues"></a>
9+
<a href="https://github.com/erikdarlingdata/PerformanceStudio/commits/main"><img src="https://img.shields.io/github/last-commit/erikdarlingdata/PerformanceStudio?style=for-the-badge" alt="Last Commit"></a>
10+
<a href="https://github.com/erikdarlingdata/PerformanceStudio/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/erikdarlingdata/PerformanceStudio/ci.yml?style=for-the-badge&label=CI" alt="CI"></a>
11+
</p>
12+
<p align="center">
13+
<a href="https://x.com/erikdarlingdata"><img src="https://img.shields.io/badge/Follow_%40ErikDarlingData-black?style=for-the-badge&logo=x&logoColor=white" alt="Follow @ErikDarlingData on X"></a>
14+
<a href="https://www.youtube.com/@ErikDarlingData"><img src="https://img.shields.io/badge/YouTube-Subscribe-red?style=for-the-badge&logo=youtube&logoColor=white" alt="YouTube Subscribe"></a>
15+
<a href="https://www.linkedin.com/in/erik-darling-data/"><img src="https://img.shields.io/badge/LinkedIn-Connect-0077B5?style=for-the-badge&logo=linkedin&logoColor=white" alt="LinkedIn Connect"></a>
16+
<a href="https://erikdarling.com"><img src="https://img.shields.io/badge/Blog-erikdarling.com-FF6B35?style=for-the-badge&logo=wordpress&logoColor=white" alt="Blog"></a>
17+
</p>
18+
319
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.
420

521
Built for developers and DBAs who want fast, automated plan analysis without clicking through SSMS.

0 commit comments

Comments
 (0)