From 333218f47ac2fcc1e33a763aac6bf11a1bcf14a3 Mon Sep 17 00:00:00 2001 From: darylmcd Date: Fri, 19 Jun 2026 22:17:05 -0500 Subject: [PATCH 1/3] fix(ci): assert McpServer packageType + embedded .mcp/server.json in packed nupkg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Verify package contents" step only size-checked the .nupkg, so a csproj regression dropping McpServer or the .mcp/server.json embed would still pass on green CI and ship a tool-only package — silently delisting the package from the NuGet MCP gallery. Unzip the packed .nupkg (it is a ZIP), read the embedded .nuspec, and fail closed unless it contains AND the archive holds a .mcp/server.json entry. The archive handle is disposed in a finally. Additive to the existing size-check; the workflow is otherwise unchanged. Verified locally (publish-nuget.yml does not run on PRs): a clean build+pack passes both assertions; deleting either csproj line and re-packing makes the step throw (red), then reverted. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/publish-nuget.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml index 942dc849..21b43cae 100644 --- a/.github/workflows/publish-nuget.yml +++ b/.github/workflows/publish-nuget.yml @@ -56,6 +56,29 @@ jobs: Write-Host "Package: $($nupkg.Name) ($([math]::Round($nupkg.Length / 1MB, 2)) MB)" Write-Host "Symbols: $($snupkg.Name) ($([math]::Round($snupkg.Length / 1MB, 2)) MB)" + # A .nupkg is a ZIP. Unzip it and assert the two MCP-gallery-critical bits the size + # check can't see: a csproj regression dropping McpServer or + # the .mcp/server.json embed would still pass size + push a tool-only package on green + # CI, silently delisting us from the NuGet MCP gallery. Fail closed on either miss. + Add-Type -AssemblyName System.IO.Compression.FileSystem + $zip = [System.IO.Compression.ZipFile]::OpenRead($nupkg.FullName) + try { + $nuspecEntry = $zip.Entries | Where-Object { $_.FullName -like "*.nuspec" } | Select-Object -First 1 + if (-not $nuspecEntry) { throw "No .nuspec found inside $($nupkg.Name)" } + $reader = New-Object System.IO.StreamReader($nuspecEntry.Open()) + $nuspec = $reader.ReadToEnd() + $reader.Dispose() + if ($nuspec -notmatch ' — csproj regression dropped McpServer; package would NOT list on the NuGet MCP gallery." + } + if (-not ($zip.Entries | Where-Object { $_.FullName -eq '.mcp/server.json' })) { + throw "Package is missing the .mcp/server.json embed — csproj regression dropped the pack item; the NuGet MCP gallery 'MCP Server' tab would not render." + } + Write-Host "Verified: McpServer packageType + .mcp/server.json embed present." + } finally { + $zip.Dispose() + } + - name: Push to NuGet if: ${{ github.event_name == 'release' || github.event_name == 'push' || inputs.dry_run != 'true' }} shell: pwsh From d7b3c4810b0bb5d1ffc91a58ac1f4d7d2ffe1255 Mon Sep 17 00:00:00 2001 From: darylmcd Date: Fri, 19 Jun 2026 22:21:19 -0500 Subject: [PATCH 2/3] fix(ci): dispose nuspec StreamReader via try/finally in verify step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code-quality review nit: the StreamReader on the nuspec entry used a bare Dispose() outside try/finally — a throw from ReadToEnd() would leak it (bounded by CI process exit, but tighten it). Wrap the read in try/finally. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/publish-nuget.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml index 21b43cae..2b1d7ff7 100644 --- a/.github/workflows/publish-nuget.yml +++ b/.github/workflows/publish-nuget.yml @@ -66,8 +66,7 @@ jobs: $nuspecEntry = $zip.Entries | Where-Object { $_.FullName -like "*.nuspec" } | Select-Object -First 1 if (-not $nuspecEntry) { throw "No .nuspec found inside $($nupkg.Name)" } $reader = New-Object System.IO.StreamReader($nuspecEntry.Open()) - $nuspec = $reader.ReadToEnd() - $reader.Dispose() + try { $nuspec = $reader.ReadToEnd() } finally { $reader.Dispose() } if ($nuspec -notmatch ' — csproj regression dropped McpServer; package would NOT list on the NuGet MCP gallery." } From 927c4926cd3c8856cb4f9456ddeb37893cb92c73 Mon Sep 17 00:00:00 2001 From: darylmcd Date: Fri, 19 Jun 2026 22:21:40 -0500 Subject: [PATCH 3/3] chore(backlog): close publish-nuget-verify-package-types + changelog fragment Co-Authored-By: Claude Opus 4.8 --- ai_docs/backlog.md | 3 +-- .../publish-nuget-verify-package-types.md | 21 ------------------- .../publish-nuget-verify-package-types.md | 5 +++++ 3 files changed, 6 insertions(+), 23 deletions(-) delete mode 100644 ai_docs/items/publish-nuget-verify-package-types.md create mode 100644 changelog.d/publish-nuget-verify-package-types.md diff --git a/ai_docs/backlog.md b/ai_docs/backlog.md index a8973840..ae501b06 100644 --- a/ai_docs/backlog.md +++ b/ai_docs/backlog.md @@ -3,7 +3,7 @@ -**updated_at:** 2026-06-20T02:56:16Z +**updated_at:** 2026-06-20T03:21:19Z ## Agent contract @@ -55,7 +55,6 @@ | `promotion-tier-execution-batch` | Medium | — | **Promotion-tier execution batch** — re-run the promotion scorecard against the current v2.3.x surface (canonical snapshot is v1.38.1), then ship experimental→stable promotions in bounded batches via `/promote-tier`. Catalog hotspot; sweep-shaped → `/backlog-sweep:prepare`. [type: ops] | L | items/promotion-tier-execution-batch.md | | `audit-21-analyzer-load-decision` | Medium | — | **AUDIT-21 analyzer-load decision** — execute the dormant IDE/CA analyzer-parity Draft plan via `/backlog-sweep:prepare`, OR re-status it superseded/parked with the product trigger; fix the plan's stale §13 row citation. Blocked-on product decision (full analyzer parity required?). | M | items/audit-21-analyzer-load-decision.md | | `compilation-cache-adoption-read-side` | Medium | — | **Compilation-cache read-side adoption** — batches 1–2 shipped (#913/#936; ~10/24 sites); split the remaining site groups (incl. forked-solution hazard) into bounded child batches at `/backlog-sweep:prepare`. [type: refactor] | L | items/compilation-cache-adoption-read-side.md | -| `publish-nuget-verify-package-types` | Medium | — | **Assert McpServer type + embedded manifest in the packed nupkg** — `publish-nuget.yml`'s "Verify package contents" step only size-checks the `.nupkg`, so a csproj regression dropping `McpServer` or the `.mcp/server.json` embed would ship a tool-only package on green CI (silent gallery delisting). Unzip + assert both. [type: ci] [source: 2026-06-19 top-n code-quality review] | S | items/publish-nuget-verify-package-types.md | | `nuget-version-checker-timeout-test-wallclock-race` | Medium | — | **Flaky NuGetVersionChecker timeout test** — `GetLatestVersion_OnTimeout_...` throws `TimeoutException` under runner load: `WaitForCompletionAsync`'s 5 s wall-clock `WaitAsync` races the checker's internal bounded timeout and expires first. Recurrence after `nuget-version-checker-test-wallclock-poll`; runs in `verify-release.ps1` (gates ubuntu publish). Make the wait event-driven / bound > internal timeout. [type: test] [source: 2026-06-19 top-n PR #986 CI flake] | S | items/nuget-version-checker-timeout-test-wallclock-race.md | ## Low diff --git a/ai_docs/items/publish-nuget-verify-package-types.md b/ai_docs/items/publish-nuget-verify-package-types.md deleted file mode 100644 index cd8cc63e..00000000 --- a/ai_docs/items/publish-nuget-verify-package-types.md +++ /dev/null @@ -1,21 +0,0 @@ -# publish-nuget-verify-package-types — assert McpServer type + embedded manifest in the packed nupkg - -**row:** `publish-nuget-verify-package-types` · **pri:** `Medium` · **size:** `S` - -## Anchors - -- `.github/workflows/publish-nuget.yml` — the "Verify package contents" step (~lines 49-57) -- `src/RoslynMcp.Host.Stdio/RoslynMcp.Host.Stdio.csproj` — `McpServer` + the `.mcp/server.json` `` (the regression surface) - -## Acceptance - -- [ ] The "Verify package contents" step unzips the packed `.nupkg` and fails the run unless the nuspec contains `` AND the archive contains `.mcp/server.json`. -- [ ] A deliberate check (removing either csproj line) confirms the step goes red instead of shipping a tool-only package on green CI. - -## Evidence - -- Code-quality review of `nuget-mcpserver-gallery-packaging` (2026-06-19 top-n-remediation): the verify step today only checks `.nupkg`/`.snupkg` exist by size; it never asserts the new `McpServer` package type or the embedded `.mcp/server.json` are present. A future csproj regression (someone drops `` or the `` line) would ship a tool-only package with green CI, silently delisting from the NuGet MCP gallery. - -## Context - -The NuGet MCP gallery listing now depends on the csproj staying correct, with no CI guard. `eng/verify-version-drift.ps1` covers the manifest *version* but not its *presence in the package*. This row adds the missing presence/type assertion to the release pack verification. diff --git a/changelog.d/publish-nuget-verify-package-types.md b/changelog.d/publish-nuget-verify-package-types.md new file mode 100644 index 00000000..acc981ae --- /dev/null +++ b/changelog.d/publish-nuget-verify-package-types.md @@ -0,0 +1,5 @@ +--- +category: Maintenance +--- + +- **Maintenance:** The `publish-nuget.yml` "Verify package contents" step now unzips the packed `.nupkg` and fails the release unless the nuspec carries `` **and** the archive embeds `.mcp/server.json` — so a csproj regression dropping either would no longer ship a tool-only package on green CI and silently delist from the NuGet MCP gallery. Previously the step only size-checked the package (`publish-nuget-verify-package-types`).