From 2bb7005f099361c6d9a3d9fef9dc405c81c327dc Mon Sep 17 00:00:00 2001 From: fadwen <110697945+fadwen@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:21:21 -0700 Subject: [PATCH] build(release): the rehearsal measures Description and the package's tags against the Gallery's limits The Gallery refused 0.27.0's first upload over the length of ReleaseNotes, and the rehearsal has measured that field since. Two more fields carry a limit it enforces, NuGet's 4,000 characters: the Description, and the package's tags, which are not the manifest's alone. The publish adds PSModule, a PSEdition_ tag per compatible edition, PSFunction_ and PSCommand_ for every exported function, and PSIncludes_Function; that is the list the Gallery shows for 0.27.0, 61 tags and 1,935 characters for 27 commands, so each new command costs about twice its name. Build/Publish-Module.ps1 builds that list from the manifest, measures all three fields, refuses a manifest over a limit and reports the three lengths. The module contract test holds the same three limits, so a pull request fails before the tag-driven release does. --- Build/Publish-Module.ps1 | 42 ++++++++++++++++++++++++++++ CHANGELOG.md | 5 ++++ Tests/Unit/Module.Contract.Tests.ps1 | 27 ++++++++++++++++++ 3 files changed, 74 insertions(+) diff --git a/Build/Publish-Module.ps1 b/Build/Publish-Module.ps1 index 00618f0..d018f2a 100644 --- a/Build/Publish-Module.ps1 +++ b/Build/Publish-Module.ps1 @@ -185,6 +185,22 @@ if (-not $WhatIfPreference -and -not $ApiKey) { } } +# The tags the Gallery lists for a published module: the manifest's, then the ones the publish +# adds for the package kind, the editions and every exported function. +function Get-PublishedTag { + param([Parameter(Mandatory)]$Manifest) + $tags = [System.Collections.Generic.List[string]]::new() + foreach ($tag in @($Manifest.Tags)) { $tags.Add($tag) } + $tags.Add('PSModule') + foreach ($edition in @($Manifest.CompatiblePSEditions)) { $tags.Add("PSEdition_$edition") } + foreach ($name in @($Manifest.ExportedFunctions.Keys | Sort-Object)) { + $tags.Add("PSFunction_$name") + $tags.Add("PSCommand_$name") + } + if ($Manifest.ExportedFunctions.Count) { $tags.Add('PSIncludes_Function') } + $tags +} + # --- 1. Help gates ---------------------------------------------------------------- # Before staging, because a stale-MAML failure should stop the release rather than leave a # staged tree that nobody notices is wrong. @@ -236,6 +252,32 @@ if ($releaseNotesLength -gt $releaseNotesLimit) { "$releaseNotesLimit. Move the older versions out, CHANGELOG.md has them.") } +# NuGet's limit on a package description, which the Gallery applies. +$descriptionLimit = 4000 +$descriptionLength = "$($manifest.Description)".Length +if ($descriptionLength -gt $descriptionLimit) { + throw ("The manifest's Description is $descriptionLength characters; the Gallery accepts at most " + + "$descriptionLimit.") +} + +# The Gallery refuses a package whose tags pass 4,000 characters ("A nuget package's Tags property +# may not be more than 4000 characters long"), and the tags it measures are not the manifest's +# alone: the publish adds PSModule, one PSEdition_ tag per compatible edition, PSFunction_ and +# PSCommand_ for every exported function, and PSIncludes_Function. That is the list the Gallery +# shows for 0.27.0 (61 tags, 1,935 characters for 27 commands), so every new command costs about +# twice its name. Measured here the way the Gallery sees it. +$tagsLimit = 4000 +$publishedTags = Get-PublishedTag -Manifest $manifest +$tagsLength = ($publishedTags -join ' ').Length +if ($tagsLength -gt $tagsLimit) { + throw ("The package's tags come to $tagsLength characters once the publish adds its own for " + + "$($manifest.ExportedFunctions.Count) commands; the Gallery accepts at most $tagsLimit. Drop " + + 'manifest tags, or fewer commands.') +} +$metadataSummary = ("Gallery metadata: Description $descriptionLength/$descriptionLimit, Tags " + + "$tagsLength/$tagsLimit, ReleaseNotes $releaseNotesLength/$releaseNotesLimit characters.") +Write-Information $metadataSummary -InformationAction Continue + # --- 3. Already published? -------------------------------------------------------- # A version number is consumed forever on first publish. Learning that from a rejected # upload is worse than a check costing one request. diff --git a/CHANGELOG.md b/CHANGELOG.md index a837347..9c39608 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,11 @@ release notes. `Import-Clixml`, a cast or a `[pscredential]` parameter, the finding is a note that says the call can go. A variable with any other source, a member or a call stays a warning. +- The release rehearsal (`Build/Publish-Module.ps1 -WhatIf`) measures the manifest's `Description` and the + package's tags against the Gallery's 4,000-character limits, next to the `ReleaseNotes` check from 0.27.0. + The tags counted are the ones the Gallery lists, the manifest's plus `PSModule`, the editions and two per + exported command, so a new command costs about twice its name. The module contract test holds the same + three limits, so a pull request fails before a release does. ## [0.27.0] - 2026-10-05 Fixes to the runtime harness and to four rules, each rule change backed by a tenth validation diff --git a/Tests/Unit/Module.Contract.Tests.ps1 b/Tests/Unit/Module.Contract.Tests.ps1 index b5db9b1..17c345d 100644 --- a/Tests/Unit/Module.Contract.Tests.ps1 +++ b/Tests/Unit/Module.Contract.Tests.ps1 @@ -26,6 +26,33 @@ Describe 'IntuneScriptLab module contract' -Tag 'Unit', 'Contract' { @((Test-ModuleManifest -Path $script:ManifestPath).RequiredModules).Count | Should-Be 0 } + It 'keeps the Gallery metadata within the limits the Gallery enforces: ' -ForEach @( + @{ Field = 'ReleaseNotes'; Limit = 10600 } + @{ Field = 'Description'; Limit = 4000 } + @{ Field = 'Tags'; Limit = 4000 } + ) { + # The Gallery refused 0.27.0's first upload: "A package's ReleaseNotes property extracted from + # the PowerShell manifest may not be more than 10600 characters long". Description and Tags + # carry NuGet's 4,000. The tags it measures include the ones the publish adds, PSModule, a + # PSEdition_ tag per edition, PSFunction_ and PSCommand_ per exported function and + # PSIncludes_Function: the list the Gallery shows for 0.27.0. Build/Publish-Module.ps1 checks + # the same three; this fails the pull request instead of the tag-driven release. + $manifest = Test-ModuleManifest -Path $script:ManifestPath + $text = switch ($Field) { + 'ReleaseNotes' { "$($manifest.ReleaseNotes)" } + 'Description' { "$($manifest.Description)" } + 'Tags' { + $tags = @($manifest.Tags) + 'PSModule' + $tags += @($manifest.CompatiblePSEditions | ForEach-Object { "PSEdition_$_" }) + foreach ($name in @($manifest.ExportedFunctions.Keys | Sort-Object)) { + $tags += "PSFunction_$name", "PSCommand_$name" + } + ($tags + 'PSIncludes_Function') -join ' ' + } + } + $text.Length | Should-BeLessThanOrEqual $Limit + } + It 'exports exactly the public functions' { $exported = @((Get-Command -Module IntuneScriptLab -CommandType Function).Name | Sort-Object) $exported | Should-BeCollection @(