From 320c291480cea746392cdd08e371c03c8ad2b0b6 Mon Sep 17 00:00:00 2001 From: Christian Mourier Date: Fri, 19 Jun 2026 13:45:37 +0200 Subject: [PATCH 1/7] Reconcile ship manifest with actual shipped DLLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop never-built System.Text.Json family from the DLL whitelist and manifest.json (the codebase deliberately avoids System.Text.Json), and add the five System.* DLLs that actually ship to manifest.json files[]. No change to the loose-file payload — the phantom DLLs were never produced. Makes manifest.json accurate for VSIX catalog installs. --- build/SqlPilotDlls.txt | 3 --- src/SqlPilot.Package/manifest.json | 6 +++++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/build/SqlPilotDlls.txt b/build/SqlPilotDlls.txt index d739f87..3cadf05 100644 --- a/build/SqlPilotDlls.txt +++ b/build/SqlPilotDlls.txt @@ -3,11 +3,8 @@ SqlPilot.Core.dll SqlPilot.Smo.dll SqlPilot.UI.dll CommunityToolkit.Mvvm.dll -System.Text.Json.dll -System.Text.Encodings.Web.dll System.Buffers.dll System.Memory.dll System.Numerics.Vectors.dll System.Runtime.CompilerServices.Unsafe.dll System.Threading.Tasks.Extensions.dll -System.ValueTuple.dll diff --git a/src/SqlPilot.Package/manifest.json b/src/SqlPilot.Package/manifest.json index 40dd2ea..3302620 100644 --- a/src/SqlPilot.Package/manifest.json +++ b/src/SqlPilot.Package/manifest.json @@ -12,7 +12,11 @@ {"fileName": "/SqlPilot.UI.dll", "sha256": null}, {"fileName": "/SqlPilot.Package.pkgdef", "sha256": null}, {"fileName": "/CommunityToolkit.Mvvm.dll", "sha256": null}, - {"fileName": "/System.Text.Json.dll", "sha256": null}, + {"fileName": "/System.Buffers.dll", "sha256": null}, + {"fileName": "/System.Memory.dll", "sha256": null}, + {"fileName": "/System.Numerics.Vectors.dll", "sha256": null}, + {"fileName": "/System.Runtime.CompilerServices.Unsafe.dll", "sha256": null}, + {"fileName": "/System.Threading.Tasks.Extensions.dll", "sha256": null}, {"fileName": "/manifest.json", "sha256": null}, {"fileName": "/catalog.json", "sha256": null} ], From 400b2faf9a1f73fb00c9c443c576710ed7304146 Mon Sep 17 00:00:00 2001 From: Christian Mourier Date: Fri, 19 Jun 2026 13:47:31 +0200 Subject: [PATCH 2/7] Build modern .vsix as an additive release artifact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zip the already-assembled SSMS22 layout (built from SqlPilotDlls.txt) plus an OPC [Content_Types].xml into SqlPilot-.vsix and attach it to the release. Built from a separate staging copy so the loose-file payload ZIP is unchanged. No csproj/build change — the VSSDK CreateVsixContainer target does not wire up in this SDK-style project. --- .github/workflows/release.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 712fa25..661b454 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -158,6 +158,32 @@ jobs: Compress-Archive -Path "$packageDir/*" -DestinationPath "$artifacts/SqlPilot-$tag.zip" + # ─── Modern .vsix for gallery distribution (additive — payload ZIP above is untouched) ─── + # A .vsix is an OPC ZIP of the SSMS22 layout + [Content_Types].xml. + # Built from a SEPARATE staging copy so the loose-file SSMS22/ folder + # shipped in the payload ZIP stays byte-for-byte identical. + $vsixStaging = "$artifacts/vsix-staging" + New-Item -ItemType Directory -Path $vsixStaging -Force | Out-Null + Copy-Item "$ssms22Dir/*" $vsixStaging -Recurse -Force + $contentTypes = @' + + + + + + + + +'@ + # Brackets in the filename are PowerShell wildcards — must use -LiteralPath. + Set-Content -LiteralPath "$vsixStaging/[Content_Types].xml" -Value $contentTypes -Encoding utf8 -NoNewline + $vsixPath = "$artifacts/SqlPilot-$tag.vsix" + Remove-Item $vsixPath -Force -ErrorAction SilentlyContinue + Add-Type -AssemblyName System.IO.Compression.FileSystem + [System.IO.Compression.ZipFile]::CreateFromDirectory($vsixStaging, $vsixPath) + Write-Host "Built VSIX: $vsixPath" + Get-ChildItem $vsixStaging | Select-Object Name + # ─── Friendly WPF installer (separate ZIP) ─── # SqlPilotInstaller.exe + its dependency DLLs. End users download # this small ZIP, extract it, and run the .exe — the installer @@ -177,6 +203,7 @@ jobs: with: files: | artifacts/SqlPilot-*.zip + artifacts/SqlPilot-*.vsix artifacts/SqlPilotInstaller-*.zip generate_release_notes: true draft: false From b8fb1eb56bd381128209b5cc8861fbf85a868f9d Mon Sep 17 00:00:00 2001 From: Christian Mourier Date: Fri, 19 Jun 2026 13:51:57 +0200 Subject: [PATCH 3/7] Publish modern .vsix to VSIX Gallery and SSMS Gallery on release Upload SqlPilot-.vsix to both galleries after the GitHub release. Gated to non-prerelease tags. Manage tokens come from repo secrets VSIXGALLERY_TOKEN / SSMSGALLERY_TOKEN. --- .github/workflows/release.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 661b454..f7a0ffd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -208,3 +208,29 @@ jobs: generate_release_notes: true draft: false prerelease: ${{ contains(github.ref, '-') }} + + - name: Publish to extension galleries + # Skip prerelease tags (containing '-') so test tags don't hit the galleries. + if: ${{ !contains(github.ref, '-') }} + shell: pwsh + env: + VSIXGALLERY_TOKEN: ${{ secrets.VSIXGALLERY_TOKEN }} + SSMSGALLERY_TOKEN: ${{ secrets.SSMSGALLERY_TOKEN }} + run: | + $tag = "${{ steps.version.outputs.tag }}" + $vsix = "artifacts/SqlPilot-$tag.vsix" + if (-not (Test-Path $vsix)) { Write-Error "VSIX not found: $vsix"; exit 1 } + $repo = "https://github.com/mourier/sql-pilot" + $issues = "https://github.com/mourier/sql-pilot/issues" + $readme = "https://raw.githubusercontent.com/mourier/sql-pilot/main/README.md" + + # VSIX Gallery (vsixgallery.com) — raw body + X-Manage-Token header. + $vg = "https://www.vsixgallery.com/api/upload?repo=$repo&issuetracker=$issues&readmeUrl=$readme" + Write-Host "Publishing to VSIX Gallery..." + curl.exe --fail --silent --show-error -X POST $vg -H "X-Manage-Token: $env:VSIXGALLERY_TOKEN" --data-binary "@$vsix" + + # SSMS Gallery (ssmsgallery.azurewebsites.net) — multipart form per its devguide. + $sg = "https://ssmsgallery.azurewebsites.net/api/upload?repo=$repo&issuetracker=$issues&readmeUrl=$readme" + Write-Host "Publishing to SSMS Gallery..." + curl.exe --fail --silent --show-error -X POST $sg -H "X-Manage-Token: $env:SSMSGALLERY_TOKEN" -F "file=@$vsix" + Write-Host "Gallery publish complete." From 409c8df90459d14afce613ee0176f73a26b674b5 Mon Sep 17 00:00:00 2001 From: Christian Mourier Date: Fri, 19 Jun 2026 13:54:12 +0200 Subject: [PATCH 4/7] Document .vsix gallery distribution channel Add VSIX Gallery badge + install note (SSMS 22 only; 18/20 stay installer-only), document the additive .vsix release step and the [21.0,23.0) target/bump policy, and record that CreateVsixContainer is inert in this SDK-style project (use the zip-the-SSMS22-folder approach). --- README.md | 7 +++++++ docs/ARCHITECTURE.md | 11 ++++++++++- docs/INSTALL.md | 4 ++++ docs/SSMS_INTEGRATION_NOTES.md | 1 + 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b50790f..e2681e9 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ CI + + VSIX Gallery + SSMS 18, 20, 22 Language: C# Windows x64 and Arm64 @@ -91,6 +94,10 @@ The installer downloads the matching release payload from GitHub itself, verifie > **First run will show "Windows protected your PC"** because we don't pay for an EV code-signing certificate yet. Click **More info** → **Run anyway**. The installer source is in [`src/SqlPilot.Installer/`](src/SqlPilot.Installer) — verify before running if you want. +### SSMS 22 via VSIX Gallery (alternative) + +SSMS 22 users can also install from the [VSIX Gallery](https://www.vsixgallery.com/extension/SqlPilot.8f4a3b2e-1c5d-4e6f-9a0b-7d8c2e3f4a5b/) or [SSMS Gallery](https://ssmsgallery.azurewebsites.net/): download the `.vsix` and run it (or subscribe to the gallery feed for update notifications). **SSMS 18 and 20 must use the installer above** — the `.vsix` targets the modern shell (SSMS 21–22) only. + ### Power user — scripted install For CI pipelines, silent installs, or admins who script everything: diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index fddcb07..96de992 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -229,13 +229,22 @@ C:\Program Files (x86)\Microsoft SQL Server Management Studio {18|20}\Common7\ID ## Release Pipeline -A tag push matching `v*` triggers `.github/workflows/release.yml`, which builds Modern + Legacy + the WPF installer in one `msbuild SqlPilot.sln` step and publishes **two release assets**: +A tag push matching `v*` triggers `.github/workflows/release.yml`, which builds Modern + Legacy + the WPF installer in one `msbuild SqlPilot.sln` step and publishes **three release assets**: - **`SqlPilot-vX.Y.Z.zip`** — the payload. Contains `SSMS22/` and `SSMS18-20/` subfolders with the extension DLLs + manifests + pkgdef, plus `Install-SqlPilot.ps1`, `Uninstall-SqlPilot.ps1`, `_SsmsHelpers.ps1`, LICENSE, NOTICE, README at the ZIP root. This is the file the WPF installer downloads at runtime, and what power users extract for scripted installs. - **`SqlPilotInstaller-vX.Y.Z.zip`** — the friendly user-facing installer. Contains `SqlPilotInstaller.exe` + its dependency DLLs. Users download this, extract, double-click the exe, approve UAC, and the installer fetches the matching payload ZIP from GitHub itself. +- **`SqlPilot-vX.Y.Z.vsix`** — the modern (SSMS 21–22) gallery artifact. See *Modern `.vsix` artifact* below. The installer pins to its own assembly version: it asks GitHub for `releases/tags/v{Major.Minor.Build}`, not `/releases/latest`. This keeps a `v1.0.0` installer from accidentally pulling a `v2.0.0` payload. +### Modern `.vsix` artifact + +`release.yml` also produces `SqlPilot-vX.Y.Z.vsix` as a *purely additive* output. A `.vsix` is just an OPC ZIP of the modern extension layout, so the workflow takes a **separate copy** of the already-assembled `SSMS22/` folder (DLLs from `build/SqlPilotDlls.txt` + `pkgdef` + `extension.vsixmanifest` (v2) + `manifest.json` + `catalog.json`), drops in a generated `[Content_Types].xml`, and zips it via `System.IO.Compression`. Because it reuses the same assembled folder, payload parity with the installer is automatic. The loose-file payload ZIP is built from its own folder and stays byte-for-byte unchanged — the `.vsix` is never substituted for it. + +The `.vsix` is attached to the GitHub release and, for non-prerelease tags only (tags without a `-` suffix), pushed to the [VSIX Gallery](https://www.vsixgallery.com/) and [SSMS Gallery](https://ssmsgallery.azurewebsites.net/). Manage tokens come from the repo secrets `VSIXGALLERY_TOKEN` and `SSMSGALLERY_TOKEN`; both galleries key the listing off the manifest `Identity Id`, so re-publishing updates the same entry. + +**Version-target policy:** the modern manifest targets SSMS `[21.0,23.0)` (SSMS 21–22). Keep the upper bound *pinned* — bump it per release only after testing on the new SSMS shell. Do **not** open-end the range. + ### Version substitution The version string `1.0.0` is hardcoded in **seven** files because we don't build a proper VSIX (no VSSDK packaging step — `dotnet build` doesn't produce one). The release workflow regex-patches all seven before calling `msbuild`: diff --git a/docs/INSTALL.md b/docs/INSTALL.md index e83706f..54eefea 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -63,6 +63,10 @@ For unattended deployment: Per-version errors don't abort the others — if SSMS 20 is locked because it's still running, the uninstaller still cleans up 18 and 22 and tells you which versions to retry. +## SSMS 22 via VSIX Gallery (alternative) + +SSMS 22 users can also install from the [VSIX Gallery](https://www.vsixgallery.com/extension/SqlPilot.8f4a3b2e-1c5d-4e6f-9a0b-7d8c2e3f4a5b/) or [SSMS Gallery](https://ssmsgallery.azurewebsites.net/): download the `.vsix` and run it (or subscribe to the gallery feed for update notifications). **SSMS 18 and 20 must use the installer or scripts above** — the `.vsix` targets the modern shell (SSMS 21–22) only. + ## Manual install (last resort) If both the WPF installer and the PowerShell scripts are unavailable for some reason, you can copy the files by hand. The release ZIP contains the exact file set under `SSMS22/` and `SSMS18-20/`. diff --git a/docs/SSMS_INTEGRATION_NOTES.md b/docs/SSMS_INTEGRATION_NOTES.md index bfb82c3..78dbb3f 100644 --- a/docs/SSMS_INTEGRATION_NOTES.md +++ b/docs/SSMS_INTEGRATION_NOTES.md @@ -357,3 +357,4 @@ For user-visible messages, use the SQL Pilot status bar (`IndexStatus.Text`). - **`SynchronizeTree` without pre-expanded tree** — triggers connection dialogs - **Building against SSMS 22's SMO and expecting it to work on SSMS 18** — version mismatch at runtime - **Passing a `nodeInfo` from `FindNode` to `OpenTableHelperClass.EditTopNRows` without setting `Connection.DatabaseName` first** — triggers a new connection dialog because the connection defaults to master/empty +- **`true` on `SqlPilot.Package.csproj`** to get a `.vsix` out of the build — produces **no** `.vsix`. The VSSDK `CreateVsixContainer` target never imports because `source.extension.vsixmanifest` is only auto-globbed as a plain `None` item in this SDK-style project; the property is read but no target runs (verified via `-v:diagnostic`). Instead, the release builds the `.vsix` by zipping a copy of the assembled `SSMS22/` release folder plus a generated `[Content_Types].xml` (an OPC ZIP), which guarantees payload parity with the installer via the shared `build/SqlPilotDlls.txt`. See `release.yml` and `docs/ARCHITECTURE.md` § Modern `.vsix` artifact. From dabaa2059109c4803eff73f6c2a8775e7f757b5d Mon Sep 17 00:00:00 2001 From: Christian Mourier Date: Mon, 22 Jun 2026 09:34:51 +0200 Subject: [PATCH 5/7] Fix release.yml: build [Content_Types].xml without a column-0 here-string The @'...'@ here-string placed its content at column 0, which terminates the YAML run: literal block and made the workflow file invalid (GitHub rejected it before any job ran). Build the XML via an indented array-join instead so every line stays within the block. --- .github/workflows/release.yml | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f7a0ffd..84d51fe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -165,16 +165,19 @@ jobs: $vsixStaging = "$artifacts/vsix-staging" New-Item -ItemType Directory -Path $vsixStaging -Force | Out-Null Copy-Item "$ssms22Dir/*" $vsixStaging -Recurse -Force - $contentTypes = @' - - - - - - - - -'@ + # Build the content as an indented array-join (NOT a column-0 here-string, + # which would break this YAML literal block). Single-quoted lines keep the + # XML angle brackets/quotes literal. + $contentTypes = @( + '' + '' + ' ' + ' ' + ' ' + ' ' + ' ' + '' + ) -join "`r`n" # Brackets in the filename are PowerShell wildcards — must use -LiteralPath. Set-Content -LiteralPath "$vsixStaging/[Content_Types].xml" -Value $contentTypes -Encoding utf8 -NoNewline $vsixPath = "$artifacts/SqlPilot-$tag.vsix" From 5ae8046d3d71f0f4ec6a14b3e1ea8fff36ea9eac Mon Sep 17 00:00:00 2001 From: Christian Mourier Date: Mon, 22 Jun 2026 09:44:46 +0200 Subject: [PATCH 6/7] Fail fast on a gallery upload error Check $LASTEXITCODE after each curl so a failed VSIX Gallery upload isn't masked by a successful SSMS Gallery upload (pwsh does not fail the step on a native non-zero exit). --- .github/workflows/release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 84d51fe..6fd3453 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -231,9 +231,11 @@ jobs: $vg = "https://www.vsixgallery.com/api/upload?repo=$repo&issuetracker=$issues&readmeUrl=$readme" Write-Host "Publishing to VSIX Gallery..." curl.exe --fail --silent --show-error -X POST $vg -H "X-Manage-Token: $env:VSIXGALLERY_TOKEN" --data-binary "@$vsix" + if ($LASTEXITCODE -ne 0) { throw "VSIX Gallery publish failed with exit code $LASTEXITCODE" } # SSMS Gallery (ssmsgallery.azurewebsites.net) — multipart form per its devguide. $sg = "https://ssmsgallery.azurewebsites.net/api/upload?repo=$repo&issuetracker=$issues&readmeUrl=$readme" Write-Host "Publishing to SSMS Gallery..." curl.exe --fail --silent --show-error -X POST $sg -H "X-Manage-Token: $env:SSMSGALLERY_TOKEN" -F "file=@$vsix" + if ($LASTEXITCODE -ne 0) { throw "SSMS Gallery publish failed with exit code $LASTEXITCODE" } Write-Host "Gallery publish complete." From bd6d5237caf75083c8a1e53b4001d360dd2d775b Mon Sep 17 00:00:00 2001 From: Christian Mourier Date: Mon, 22 Jun 2026 10:02:13 +0200 Subject: [PATCH 7/7] Exclude catalog.json/manifest.json from the .vsix so it installs The modern SSMS 22 VSIXInstaller rejects a .vsix that contains the loose-file deployment descriptors (catalog.json/manifest.json) with 'InvalidExtensionManifestException: version Invalid'. Strip them from the .vsix staging copy only; the payload ZIP still ships them for the file-copy installer. Verified: stripped .vsix installs with exit 0 to the per-user SSMS 22 Extensions store. Documented in SSMS_INTEGRATION_NOTES. --- .github/workflows/release.yml | 5 +++++ docs/SSMS_INTEGRATION_NOTES.md | 1 + 2 files changed, 6 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6fd3453..71a3ed9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -165,6 +165,11 @@ jobs: $vsixStaging = "$artifacts/vsix-staging" New-Item -ItemType Directory -Path $vsixStaging -Force | Out-Null Copy-Item "$ssms22Dir/*" $vsixStaging -Recurse -Force + # Drop the loose-file deployment metadata from the .vsix only. catalog.json/ + # manifest.json make the modern SSMS 22 VSIXInstaller take a v3-catalog path + # that rejects the manifest ("version Invalid"); a clean OPC vsix (manifest + + # payload) installs. The payload ZIP above still ships them for the installer. + Remove-Item "$vsixStaging/catalog.json","$vsixStaging/manifest.json" -Force -ErrorAction SilentlyContinue # Build the content as an indented array-join (NOT a column-0 here-string, # which would break this YAML literal block). Single-quoted lines keep the # XML angle brackets/quotes literal. diff --git a/docs/SSMS_INTEGRATION_NOTES.md b/docs/SSMS_INTEGRATION_NOTES.md index 78dbb3f..7b60423 100644 --- a/docs/SSMS_INTEGRATION_NOTES.md +++ b/docs/SSMS_INTEGRATION_NOTES.md @@ -358,3 +358,4 @@ For user-visible messages, use the SQL Pilot status bar (`IndexStatus.Text`). - **Building against SSMS 22's SMO and expecting it to work on SSMS 18** — version mismatch at runtime - **Passing a `nodeInfo` from `FindNode` to `OpenTableHelperClass.EditTopNRows` without setting `Connection.DatabaseName` first** — triggers a new connection dialog because the connection defaults to master/empty - **`true` on `SqlPilot.Package.csproj`** to get a `.vsix` out of the build — produces **no** `.vsix`. The VSSDK `CreateVsixContainer` target never imports because `source.extension.vsixmanifest` is only auto-globbed as a plain `None` item in this SDK-style project; the property is read but no target runs (verified via `-v:diagnostic`). Instead, the release builds the `.vsix` by zipping a copy of the assembled `SSMS22/` release folder plus a generated `[Content_Types].xml` (an OPC ZIP), which guarantees payload parity with the installer via the shared `build/SqlPilotDlls.txt`. See `release.yml` and `docs/ARCHITECTURE.md` § Modern `.vsix` artifact. +- **Shipping `catalog.json` / `manifest.json` *inside* the `.vsix`** — the modern SSMS 22 VSIXInstaller (the VS-Installer ServiceHub `VSIXInstaller.exe`) reads the metadata fine but **fails the actual install** with `InvalidExtensionManifestException: the provided vsixmanifest, version Invalid is not vsixV1, vsixV2, or vsixV3`. Their presence makes it take a v3-catalog install path that rejects the manifest. A clean OPC vsix containing only `extension.vsixmanifest` + `[Content_Types].xml` + payload (DLLs + pkgdef) installs with exit code 0 (to `%LOCALAPPDATA%\Microsoft\SSMS\\Extensions\\`). So `release.yml` deletes `catalog.json`/`manifest.json` from the `.vsix` staging copy only — the loose-file payload ZIP still ships them for the file-copy installer. (`catalog.json`/`manifest.json` are the VS2017+ loose-file deployment descriptors, not VSIX-container files.)