From af7e8139fc9ac5daf8036268c725995524a63455 Mon Sep 17 00:00:00 2001 From: Trent Blackburn Date: Tue, 25 Aug 2026 18:38:43 -0400 Subject: [PATCH 1/2] feat: Migrate updatable help to the PlatyPS 1.x cabinet pipeline Closes #152. Closes #101. Closes #169. Part of #105, Phase 2 of #120. Build-PSBuildUpdatableHelp produces a cabinet, its zip, and a HelpInfo.xml. It never could before. #169 recorded three defects, and every one of them alone was fatal: the module landing page New-ExternalHelpCab needs was never generated, $moduleOutDir was undefined anywhere in the codebase, and the task passed no -Module so the parameter defaulted from an unset caller-scope variable. New-HelpCabinetFile maps cleanly onto the old call -- CabinetFilesFolder / MarkdownModuleFile / OutputFolder against CabFilesFolder / LandingPagePath / OutputFolder -- so the port itself was small. The defects were the work. Build-PSBuildMarkdown now passes -WithModulePage. #173 deliberately did not, because 0.14.x produced no landing page and adding one puts a new file in every consumer's docs tree; the cabinet needs it, so it lands here with its migration-guide entry. It stays filtered out of the MAML export, where a module page aborts the whole batch (PowerShell/platyPS#862). The function refuses when the manifest declares no HelpInfoUri. Measured: without one, New-HelpCabinetFile writes the cabinet and the zip and then fails before the HelpInfo.xml that Update-Help resolves them through, leaving output that looks complete and cannot be used. Both test fixtures gained a placeholder URI for the same reason. Defect 3 lived in the task rather than the function, so a function-level test could never catch it: build.tests.ps1 now runs the GenerateUpdatableHelp task itself through the child-process pattern the other build contexts use, and asserts both artifacts. That was Copilot's point on #170 and it was right. Also corrects the header of Build-PSBuildHelp.tests.ps1, which claimed nothing observed these functions. build.tests.ps1 has covered GenerateMarkdown and GenerateMAML end to end all along. Suite: 470 passed, 0 failed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012AKaM9i6NyMFDcJNeC34h5 --- CHANGELOG.md | 14 +++ PowerShellBuild/IB.tasks.ps1 | 8 +- .../Public/Build-PSBuildMarkdown.ps1 | 4 + .../Public/Build-PSBuildUpdatableHelp.ps1 | 99 +++++++++++---- PowerShellBuild/en-US/Messages.psd1 | 3 +- PowerShellBuild/psakeFile.ps1 | 9 +- docs/migration-v0.8-to-v1.0.md | 86 ++++++++++--- tests/Build-PSBuildHelp.tests.ps1 | 113 ++++++++++++++---- tests/TestModule/TestModule/TestModule.psd1 | 5 + tests/build.tests.ps1 | 45 +++++++ .../PSBuildTestFixture.psd1 | 5 + 11 files changed, 329 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78fa432..2797f23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +### Fixed + +- [**#169**](https://github.com/psake/PowerShellBuild/issues/169) + `Build-PSBuildUpdatableHelp` produces a help cabinet. It never could before: + it needed a module landing page that `Build-PSBuildMarkdown` did not + generate, passed an undefined variable as the cabinet source folder, and was + never given the module name — so any build that reached the + `GenerateUpdatableHelp` task failed with a parameter-binding error. + `Build-PSBuildMarkdown` now writes the landing page, and the task passes the + module name and output path it always should have. Using the task requires a + `HelpInfoUri` in your module manifest; without one it warns and produces + nothing, rather than writing a cabinet with no `HelpInfo.xml` to find it by. + See the [v0.8 → v1.0 migration guide](docs/migration-v0.8-to-v1.0.md). + ### Changed - [**#105**](https://github.com/psake/PowerShellBuild/issues/105) diff --git a/PowerShellBuild/IB.tasks.ps1 b/PowerShellBuild/IB.tasks.ps1 index fe50828..c2b7a01 100644 --- a/PowerShellBuild/IB.tasks.ps1 +++ b/PowerShellBuild/IB.tasks.ps1 @@ -155,7 +155,13 @@ $genUpdatableHelpPreReqs = { # Synopsis: Create updatable help .cab file based on PlatyPS markdown help Task GenerateUpdatableHelp -if (. $genUpdatableHelpPreReqs) BuildHelp, { - Build-PSBuildUpdatableHelp -DocsPath $PSBPreference.Docs.RootDir -OutputPath $PSBPreference.Help.UpdatableHelpOutDir + $buildUpdatableHelpParams = @{ + DocsPath = $PSBPreference.Docs.RootDir + OutputPath = $PSBPreference.Help.UpdatableHelpOutDir + ModulePath = $PSBPreference.Build.ModuleOutDir + Module = $PSBPreference.General.ModuleName + } + Build-PSBuildUpdatableHelp @buildUpdatableHelpParams } # Synopsis: Publish module to the defined PowerShell repository diff --git a/PowerShellBuild/Public/Build-PSBuildMarkdown.ps1 b/PowerShellBuild/Public/Build-PSBuildMarkdown.ps1 index 0f44807..60b449b 100644 --- a/PowerShellBuild/Public/Build-PSBuildMarkdown.ps1 +++ b/PowerShellBuild/Public/Build-PSBuildMarkdown.ps1 @@ -95,6 +95,10 @@ function Build-PSBuildMarkdown { ModuleInfo = $moduleInfo OutputFolder = $stagingPath Locale = $Locale + # The landing page is what carries the module GUID, locale, and help + # version into the updatable-help cabinet. 0.14.x never produced one, + # which is why Build-PSBuildUpdatableHelp could not work. + WithModulePage = $true # PlatyPS 1.x defaults this front matter key to "-Help.xml", where # 0.14.x wrote "-help.xml". The key is what Export-MamlCommandHelp # names the MAML file after, so pinning it here keeps the markdown, the MAML diff --git a/PowerShellBuild/Public/Build-PSBuildUpdatableHelp.ps1 b/PowerShellBuild/Public/Build-PSBuildUpdatableHelp.ps1 index 986a22e..e23b939 100644 --- a/PowerShellBuild/Public/Build-PSBuildUpdatableHelp.ps1 +++ b/PowerShellBuild/Public/Build-PSBuildUpdatableHelp.ps1 @@ -5,25 +5,23 @@ function Build-PSBuildUpdatableHelp { .DESCRIPTION Create updatable help .cab file based on PlatyPS markdown help. - Not implemented against PlatyPS 1.x yet. The cabinet pipeline is migrated in - psake/PowerShellBuild#152 along with the three defects in #169 that prevented this - function from ever succeeding. Until then it reports that updatable help was skipped - and returns without writing anything. + Requires the module manifest to declare a HelpInfoUri. That URI is where Update-Help + looks for the help content, so a cabinet built without one cannot be consumed. .PARAMETER DocsPath - Path to PlatyPS markdown help files. + Path to PlatyPS markdown help files. Must contain a locale directory holding the + module landing page, /.md. .PARAMETER OutputPath Path to create updatable help .cab file in. + .PARAMETER ModulePath + Path to the built module. The MAML written by Build-PSBuildMAMLHelp is read from + /, and the manifest is read from /.psd1. .PARAMETER Module - Name of the module to create a .cab file for. Defaults to the - $ModuleName variable from the parent scope. + Name of the module to create a .cab file for. .EXAMPLE - PS> Build-PSBuildUpdatableHelp -DocsPath ./docs -OutputPath ./Output/UpdatableHelp + PS> Build-PSBuildUpdatableHelp -DocsPath ./docs -OutputPath ./Output/UpdatableHelp -ModulePath ./Output/MyModule/1.0.0 -Module MyModule - Reports that updatable help is not available and returns. + Create help .cab file based on PlatyPS markdown help. #> - # The parameters are unused only because the body is stubbed. They stay so the public - # signature does not change twice -- once here and again when #152 restores the body. - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] [CmdletBinding()] param( [parameter(Mandatory)] @@ -32,14 +30,75 @@ function Build-PSBuildUpdatableHelp { [parameter(Mandatory)] [string]$OutputPath, - [string]$Module = $ModuleName + [parameter(Mandatory)] + [string]$ModulePath, + + [parameter(Mandatory)] + [string]$Module ) - # Deliberately references no PlatyPS command. Naming New-ExternalHelpCab here is enough to - # make PowerShell autoload platyPS 0.14.2 on any session that resolves it, and once that - # module is loaded, Microsoft.PowerShell.PlatyPS can no longer be imported in the same - # process -- both ship their own YamlDotNet with different assembly identities. Leaving the - # old call in place would poison every session that still has 0.14.2 installed, which is - # every consumer part-way through the upgrade. - Write-Warning $LocalizedData.UpdatableHelpNotMigrated + if ($null -ne $IsWindows -and -not $IsWindows) { + Write-Warning $LocalizedData.MakeCabNotAvailable + return + } + + # Update-Help resolves help content through the manifest's HelpInfoUri. Without one, + # New-HelpCabinetFile still writes the cabinet and its zip but fails before writing the + # HelpInfo.xml that makes them findable, leaving output that looks complete and is not. + # Refusing up front is better than producing that. + $manifestPath = [IO.Path]::Combine($ModulePath, "$Module.psd1") + if (-not (Test-Path -LiteralPath $manifestPath)) { + Write-Warning ($LocalizedData.UnableToFindModuleManifest -f $manifestPath) + return + } + $helpInfoUri = (Import-PowerShellDataFile -Path $manifestPath).HelpInfoUri + if ([string]::IsNullOrWhiteSpace($helpInfoUri)) { + Write-Warning ($LocalizedData.HelpInfoUriRequired -f $Module) + return + } + + $helpLocales = (Get-ChildItem -Path $DocsPath -Directory).Name + + # Create updatable help output directory + if (-not (Test-Path -LiteralPath $OutputPath)) { + $newItemSplat = @{ + ItemType = 'Directory' + Verbose = ($VerbosePreference -eq 'Continue') + Path = $OutputPath + } + New-Item @newItemSplat > $null + } else { + Write-Verbose ($LocalizedData.DirectoryAlreadyExists -f $OutputPath) + $removeItemSplat = @{ + Recurse = $true + Force = $true + Verbose = ($VerbosePreference -eq 'Continue') + } + Get-ChildItem $OutputPath | Remove-Item @removeItemSplat + } + + foreach ($locale in $helpLocales) { + # The landing page is a module document rather than command help. It is what carries + # the module GUID, locale, and help version into the cabinet, and it is generated by + # Build-PSBuildMarkdown; a docs tree from an older build will not have one. + $markdownModuleFile = [IO.Path]::Combine($DocsPath, $locale, "$Module.md") + if (-not (Test-Path -LiteralPath $markdownModuleFile)) { + Write-Warning ($LocalizedData.ModuleLandingPageNotFound -f $markdownModuleFile, $locale) + continue + } + + $cabinetFilesFolder = [IO.Path]::Combine($ModulePath, $locale) + if (-not (Test-Path -LiteralPath $cabinetFilesFolder)) { + Write-Warning ($LocalizedData.FolderDoesNotExist -f $cabinetFilesFolder) + continue + } + + $cabinetParams = @{ + CabinetFilesFolder = $cabinetFilesFolder + MarkdownModuleFile = $markdownModuleFile + OutputFolder = $OutputPath + Verbose = ($VerbosePreference -eq 'Continue') + } + New-HelpCabinetFile @cabinetParams > $null + } } diff --git a/PowerShellBuild/en-US/Messages.psd1 b/PowerShellBuild/en-US/Messages.psd1 index fae187d..48c8516 100644 --- a/PowerShellBuild/en-US/Messages.psd1 +++ b/PowerShellBuild/en-US/Messages.psd1 @@ -3,7 +3,8 @@ NoCommandsExported=No commands have been exported. Skipping markdown generation. FailedToGenerateMarkdownHelp=Failed to generate markdown help. : {0} AddingFileToPsm1=Adding [{0}] to PSM1 MakeCabNotAvailable=MakeCab.exe is not available. Cannot create help cab. -UpdatableHelpNotMigrated=Updatable help was skipped. The cabinet pipeline has not been migrated to Microsoft.PowerShell.PlatyPS 1.x yet; see psake/PowerShellBuild#152. +HelpInfoUriRequired=Updatable help was skipped for [{0}]. The module manifest does not declare a HelpInfoUri, which is where Update-Help looks for the help content, so a cabinet built without one cannot be used. +ModuleLandingPageNotFound=Updatable help was skipped for locale [{1}]. The module landing page [{0}] does not exist. It is generated by the GenerateMarkdown task; regenerate the documentation and try again. DirectoryAlreadyExists=Directory already exists [{0}]. PathLongerThan3Chars=Path [{0}] must be longer than 3 characters. BuildSystemDetails=Build System Details: diff --git a/PowerShellBuild/psakeFile.ps1 b/PowerShellBuild/psakeFile.ps1 index 2208e18..9f4e19f 100644 --- a/PowerShellBuild/psakeFile.ps1 +++ b/PowerShellBuild/psakeFile.ps1 @@ -215,7 +215,14 @@ $genUpdatableHelpPreReqs = { $result } Task GenerateUpdatableHelp -Depends $PSBGenerateUpdatableHelpDependency -PreCondition $genUpdatableHelpPreReqs { - Build-PSBuildUpdatableHelp -DocsPath $PSBPreference.Docs.RootDir -OutputPath $PSBPreference.Help.UpdatableHelpOutDir -Verbose:($VerbosePreference -eq 'Continue') + $buildUpdatableHelpParams = @{ + DocsPath = $PSBPreference.Docs.RootDir + OutputPath = $PSBPreference.Help.UpdatableHelpOutDir + ModulePath = $PSBPreference.Build.ModuleOutDir + Module = $PSBPreference.General.ModuleName + Verbose = ($VerbosePreference -eq 'Continue') + } + Build-PSBuildUpdatableHelp @buildUpdatableHelpParams } -Description 'Create updatable help .cab file based on PlatyPS markdown help' Task Publish -Depends $PSBPublishDependency { diff --git a/docs/migration-v0.8-to-v1.0.md b/docs/migration-v0.8-to-v1.0.md index ee05ac1..6457270 100644 --- a/docs/migration-v0.8-to-v1.0.md +++ b/docs/migration-v0.8-to-v1.0.md @@ -34,8 +34,10 @@ One line per break; follow the link for details and migration steps. — PlatyPS 1.x always sorts alphabetically, so the setting could no longer do anything. - [Generated markdown uses the PlatyPS 1.x schema](#generated-markdown-uses-the-platyps-1x-schema) — expect a large diff in `docs/` on the first 1.0.0 build. -- [Updatable help is temporarily unavailable](#updatable-help-is-temporarily-unavailable) - — the cabinet pipeline is migrated before 1.0.0 ships. +- [Updatable help works, and now requires a `HelpInfoUri`](#updatable-help-works-and-now-requires-a-helpinfouri) + — it could never succeed in 0.8.x; using it now needs a `HelpInfoUri` in your manifest. +- [Your `docs/` tree gains a module landing page](#your-docs-tree-gains-a-module-landing-page) + — a new `.md` appears alongside the per-command documents. > More entries will follow as the remaining Phase 2 work lands. @@ -307,24 +309,74 @@ this should be schema churn rather than content loss — but verify. Consumer guidance for converting a committed tree is [#154](https://github.com/psake/PowerShellBuild/issues/154). -### Updatable help is temporarily unavailable +### Updatable help works, and now requires a `HelpInfoUri` -`Build-PSBuildUpdatableHelp` and the `GenerateUpdatableHelp` task write a -warning and return without producing a cabinet. The 1.x cabinet pipeline -is migrated in [#152](https://github.com/psake/PowerShellBuild/issues/152) -before 1.0.0 ships. +`Build-PSBuildUpdatableHelp` and the `GenerateUpdatableHelp` task produce a +help cabinet, its `.zip`, and a `HelpInfo.xml`. In 0.8.x they could not: +the function needed a module landing page that `Build-PSBuildMarkdown` +never generated, passed an undefined variable as the cabinet source +folder, and never received the module name — three separate defects, +recorded in [#169](https://github.com/psake/PowerShellBuild/issues/169). +Any 0.8.x build that reached this task failed with a parameter-binding +error, so nothing that worked before stops working. -This costs nothing in practice: the function could never succeed in -0.8.x either. It required a module landing page that -`Build-PSBuildMarkdown` never generated, and it passed an undefined -variable as the cabinet source folder — three separate defects, recorded -in [#169](https://github.com/psake/PowerShellBuild/issues/169). The task -is opt-in and is not part of the default build, so most consumers never -reached it. +**Your module manifest must declare a `HelpInfoUri`.** That URI is where +`Update-Help` looks for the content, so a cabinet built without one cannot +be consumed. If it is missing, the task now writes a warning and produces +nothing: -**Detection:** `Updatable help was skipped. The cabinet pipeline has not -been migrated to Microsoft.PowerShell.PlatyPS 1.x yet` in the build -output, where 0.8.x raised a parameter-binding error. +```text +Updatable help was skipped for [MyModule]. The module manifest does not +declare a HelpInfoUri, ... +``` + +Refusing is deliberate. `New-HelpCabinetFile` will otherwise write the +cabinet and its `.zip` and then fail before writing the `HelpInfo.xml` +that makes them findable — output that looks complete and is useless. + +**Migration:** add the URI where you publish help. + +```powershell +# In your module manifest +HelpInfoUri = 'https://example.com/mymodule/help' +``` + +Nothing is needed if you do not use the `GenerateUpdatableHelp` task; it +is opt-in and not part of the default build. + +**Calling the function directly?** Its signature changed. `Module` is now +mandatory rather than defaulting from a caller-scope variable, and +`ModulePath` is new — it is where the MAML written by the `GenerateMAML` +task is read from. + +**Before (0.8.x):** + +```powershell +Build-PSBuildUpdatableHelp -DocsPath ./docs -OutputPath ./Output/UpdatableHelp +``` + +**After (1.0.0):** + +```powershell +Build-PSBuildUpdatableHelp -DocsPath ./docs -OutputPath ./Output/UpdatableHelp ` + -ModulePath ./Output/MyModule/1.0.0 -Module MyModule +``` + +### Your `docs/` tree gains a module landing page + +`Build-PSBuildMarkdown` now generates `//.md` +alongside the per-command documents. 0.14.x never produced one. + +The page carries the module GUID, locale, and help version into the +updatable-help cabinet, which is why its absence was one of the three +defects above. It is also a different document type from command help, and +is excluded from MAML generation automatically — a module page in a MAML +export batch aborts the entire export +([PowerShell/platyPS#862](https://github.com/PowerShell/platyPS/issues/862)), +so `Build-PSBuildMAMLHelp` filters it out. + +**Detection:** a new `.md` file appears in your docs tree on the +first 1.0.0 build. If you commit `docs/`, commit it too. ## Adding an entry (for PR contributors) diff --git a/tests/Build-PSBuildHelp.tests.ps1 b/tests/Build-PSBuildHelp.tests.ps1 index 1d38a2b..82eb025 100644 --- a/tests/Build-PSBuildHelp.tests.ps1 +++ b/tests/Build-PSBuildHelp.tests.ps1 @@ -1,12 +1,15 @@ -# Baseline coverage for the three help-building functions (psake/PowerShellBuild#149). +# Unit coverage for the three help-building functions (psake/PowerShellBuild#149). # -# Build-PSBuildMarkdown, Build-PSBuildMAMLHelp, and Build-PSBuildUpdatableHelp have had no -# tests. The repository does not run its own docs tasks either -- the root psakeFile.ps1 goes -# Init -> Clean -> Build -> Analyze -> Pester -> Publish and never invokes GenerateMarkdown, -# GenerateMAML, or GenerateUpdatableHelp -- so nothing observes these functions today. That -# makes the PlatyPS 1.x migration (#105) a rewrite of three uncovered functions. This file is -# the red-before-green baseline they regress against, written against the CURRENT platyPS -# 0.14.2 behavior. +# These functions had no tests of their own before #149. They were not entirely unobserved, +# though: build.tests.ps1 builds tests/TestModule through -FromModule PowerShellBuild, whose +# Build task depends on BuildHelp, so GenerateMarkdown and GenerateMAML have been exercised +# end to end all along and "Has MAML help XML" pinned the output layout. What was missing was +# coverage of the functions directly, with specific options, and anything at all for +# GenerateUpdatableHelp -- which is not in the default Build chain. +# +# The file began as a red-before-green baseline against platyPS 0.14.2 and now asserts the +# PlatyPS 1.x behavior the migration produces. The task wiring, which no function-level test +# can reach, is covered in build.tests.ps1. # # Every invocation runs in a background job. That is not incidental: platyPS 0.14.2 and # Microsoft.PowerShell.PlatyPS 1.x each load their own YamlDotNet.dll through NestedModules, @@ -71,10 +74,9 @@ Describe 'Help building functions' -Skip:(-not $script:platyPSAvailable) { } } - It 'writes a module landing page named for the module' -Skip { - # Skipped: New-MarkdownHelp is called without -WithModulePage, so the landing page - # is never produced. That is defect 1 of psake/PowerShellBuild#169 and the reason - # Build-PSBuildUpdatableHelp cannot run at all. Unskip when #169 is fixed. + It 'writes a module landing page named for the module' { + # The cabinet step reads the module GUID, locale, and help version from this page. + # Its absence was defect 1 of psake/PowerShellBuild#169. $landingPageName = '{0}.md' -f $script:markdownScenario.ModuleName Join-Path -Path $script:markdownScenario.LocalePath -ChildPath $landingPageName | Should -Exist @@ -143,34 +145,101 @@ Describe 'Help building functions' -Skip:(-not $script:platyPSAvailable) { } } - Context 'Build-PSBuildUpdatableHelp' { + Context 'Build-PSBuildUpdatableHelp' -Skip:(-not $script:onWindows) { BeforeAll { $script:cabScenario = New-PSBuildDocsScenario -Path $TestDrive -Name 'cab' + + $cabMarkdownJobParameter = @{ + ModulePath = $script:builtModulePath + CommandName = 'Build-PSBuildMarkdown' + Parameter = New-PSBuildMarkdownParameter -Scenario $script:cabScenario + } + $null = Invoke-PSBuildCommandInJob @cabMarkdownJobParameter + + # MAML has to land in the module directory, because that is where the cabinet step + # reads it from -- the same path the GenerateUpdatableHelp task supplies. + $cabMamlJobParameter = @{ + ModulePath = $script:builtModulePath + CommandName = 'Build-PSBuildMAMLHelp' + Parameter = @{ + Path = $script:cabScenario.DocsPath + DestinationPath = $script:cabScenario.ModulePath + } + } + $null = Invoke-PSBuildCommandInJob @cabMamlJobParameter + $cabJobParameter = @{ ModulePath = $script:builtModulePath CommandName = 'Build-PSBuildUpdatableHelp' Parameter = @{ DocsPath = $script:cabScenario.DocsPath OutputPath = $script:cabScenario.UpdatableHelpPath + ModulePath = $script:cabScenario.ModulePath Module = $script:cabScenario.ModuleName } } $script:cabResult = Invoke-PSBuildCommandInJob @cabJobParameter } - # Pins the documented intermediate state, not the destination. The cabinet pipeline - # migrates in #152 together with the three defects in #169 that stopped this function - # ever succeeding. Until then it must fail quietly rather than throw, and above all it - # must not name a platyPS 0.14.2 command: resolving one autoloads that module, and a - # session holding it can no longer import Microsoft.PowerShell.PlatyPS at all. - It 'returns without throwing' { - $script:cabResult.Threw | Should -BeFalse + It 'completes without error' { $script:cabResult.ErrorMessage | Should -BeNullOrEmpty + $script:cabResult.Threw | Should -BeFalse } - It 'writes nothing' { - $script:cabScenario.UpdatableHelpPath | Should -Not -Exist + It 'produces a cabinet file' { + Get-ChildItem -Path $script:cabScenario.UpdatableHelpPath -Filter '*.cab' | + Should -Not -BeNullOrEmpty + } + + It 'produces the help info manifest' { + # Written only when the manifest declares a HelpInfoUri. Without one the cabinet is + # still produced and this file is not, which is output that looks complete and is + # useless to Update-Help. + Get-ChildItem -Path $script:cabScenario.UpdatableHelpPath -Filter '*HelpInfo.xml' | + Should -Not -BeNullOrEmpty + } + + It 'names the cabinet for the module, its GUID, and the locale' { + $manifestPath = Join-Path -Path $script:cabScenario.ModulePath -ChildPath ( + '{0}.psd1' -f $script:cabScenario.ModuleName + ) + $moduleGuid = (Import-PowerShellDataFile -Path $manifestPath).GUID + $expectedName = '{0}_{1}_{2}_HelpContent.cab' -f + $script:cabScenario.ModuleName, $moduleGuid, $script:cabScenario.Locale + + @(Get-ChildItem -Path $script:cabScenario.UpdatableHelpPath -Filter '*.cab')[0].Name | + Should -Be $expectedName + } + } + + Context 'Build-PSBuildUpdatableHelp refuses to half-produce' -Skip:(-not $script:onWindows) { + + It 'declines when the manifest declares no HelpInfoUri' { + # New-HelpCabinetFile writes the cabinet and its zip, then fails before writing the + # HelpInfo.xml that Update-Help needs to find them. Stopping first is the whole + # point of the guard, so this asserts nothing at all was written. + $scenario = New-PSBuildDocsScenario -Path $TestDrive -Name 'nouri' + $manifestPath = Join-Path -Path $scenario.ModulePath -ChildPath ( + '{0}.psd1' -f $scenario.ModuleName + ) + (Get-Content -Path $manifestPath -Raw) -replace "(?m)^\s*HelpInfoUri.*$", '' | + Set-Content -Path $manifestPath + + $guardJobParameter = @{ + ModulePath = $script:builtModulePath + CommandName = 'Build-PSBuildUpdatableHelp' + Parameter = @{ + DocsPath = $scenario.DocsPath + OutputPath = $scenario.UpdatableHelpPath + ModulePath = $scenario.ModulePath + Module = $scenario.ModuleName + } + } + $result = Invoke-PSBuildCommandInJob @guardJobParameter + + $result.Threw | Should -BeFalse + $scenario.UpdatableHelpPath | Should -Not -Exist } } } diff --git a/tests/TestModule/TestModule/TestModule.psd1 b/tests/TestModule/TestModule/TestModule.psd1 index c93090b..4d2d09e 100644 --- a/tests/TestModule/TestModule/TestModule.psd1 +++ b/tests/TestModule/TestModule/TestModule.psd1 @@ -12,6 +12,11 @@ CmdletsToExport = @() VariablesToExport = @() AliasesToExport = @() + # Updatable help is resolved through this URI. It is a placeholder: the fixture + # is never published, but New-HelpCabinetFile refuses to write a HelpInfo.xml + # without one, so the cabinet tests need it present. + HelpInfoUri = 'https://example.com/testmodule/help' + PrivateData = @{ PSData = @{ # Tags = @() diff --git a/tests/build.tests.ps1 b/tests/build.tests.ps1 index ba218ae..d86d593 100644 --- a/tests/build.tests.ps1 +++ b/tests/build.tests.ps1 @@ -1,4 +1,10 @@ # spell-checker:ignore excludeme +BeforeDiscovery { + # Cabinet generation shells out to makecab.exe. $IsWindows does not exist on Windows + # PowerShell 5.1, so its absence also means Windows. + $script:onWindows = $IsWindows -or $null -eq $IsWindows +} + Describe 'Build' { BeforeAll { @@ -115,4 +121,43 @@ Describe 'Build' { "$script:testModuleOutputPath/en-US/TestModule-help.xml" | Should -Exist } } + + Context 'Updatable help task' -Skip:(-not $script:onWindows) { + + # Exercises the GenerateUpdatableHelp TASK, not just Build-PSBuildUpdatableHelp. + # psake/PowerShellBuild#169 defect 3 lived in the wiring rather than the function: the + # task passed neither the module name nor the module output path, so every + # function-level fix could pass while the task stayed broken. Only running the task + # catches that, and psake cannot nest, so it runs in a job like the builds above. + BeforeAll { + $script:updatableHelpOutputPath = if ($env:GITHUB_ACTION) { + [IO.Path]::Combine($env:BHProjectPath, 'Output', 'UpdatableHelp') + } else { + [IO.Path]::Combine($script:testModuleSource, 'Output', 'UpdatableHelp') + } + + # Not received, matching the contexts above -- psake writes its task banner and + # build report to the host, and receiving them here would interleave that with + # Pester's own output. + Start-Job -ScriptBlock { + Set-Location -Path $using:testModuleSource + ./build.ps1 -Task GenerateUpdatableHelp + } | Wait-Job > $null + } + + AfterAll { + Remove-Item $script:updatableHelpOutputPath -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item $script:testModuleOutputPath -Recurse -Force -ErrorAction SilentlyContinue + } + + It 'Produces a help cabinet' { + Get-ChildItem -Path $script:updatableHelpOutputPath -Filter '*.cab' -ErrorAction SilentlyContinue | + Should -Not -BeNullOrEmpty + } + + It 'Produces the help info manifest' { + Get-ChildItem -Path $script:updatableHelpOutputPath -Filter '*HelpInfo.xml' -ErrorAction SilentlyContinue | + Should -Not -BeNullOrEmpty + } + } } diff --git a/tests/fixtures/PSBuildTestFixture/PSBuildTestFixture.psd1 b/tests/fixtures/PSBuildTestFixture/PSBuildTestFixture.psd1 index 587d7da..036cb15 100644 --- a/tests/fixtures/PSBuildTestFixture/PSBuildTestFixture.psd1 +++ b/tests/fixtures/PSBuildTestFixture/PSBuildTestFixture.psd1 @@ -14,6 +14,11 @@ CmdletsToExport = @() VariablesToExport = @() AliasesToExport = @() + # Updatable help is resolved through this URI. It is a placeholder: the fixture + # is never published, but New-HelpCabinetFile refuses to write a HelpInfo.xml + # without one, so the cabinet tests need it present. + HelpInfoUri = 'https://example.com/psbuildtestfixture/help' + PrivateData = @{ PSData = @{ Tags = @('PowerShellBuild', 'TestFixture') From 2e6e872c7fdb240f93d8a00894b43cec84ef0321 Mon Sep 17 00:00:00 2001 From: Trent Blackburn Date: Tue, 25 Aug 2026 23:08:52 -0400 Subject: [PATCH 2/2] fix: Address the review findings on the cabinet migration The landing page was never refreshed after the first build. The refresh pass filters to CommandHelp documents, which excludes it, and the move loop then skipped it because a copy already existed -- under a comment claiming it had "already refreshed above", which was true of command help and false of the module page. It is now always replaced. The review's stated consequence does not hold, and it is worth recording why. It argued that a stale page freezes HelpContentVersion so Update-Help stops fetching after a version bump. Measured: "Help Version" is a constant 1.0.0.0 that New-MarkdownCommandHelp does not derive from ModuleVersion -- the fixture is 0.1.0 and its page still reads 1.0.0.0 -- so a version bump never moved it either way. Update-MarkdownModuleFile is no help: it leaves the version alone and rewrites the body, losing prose. What does go stale is the module GUID, which stamps the cabinet file name, and the command index, which is the page's whole body. Both justify always replacing it, and the test asserts the GUID case because that one silently misnames the cabinet. Also from the review, all correct: - OutputPath was cleared before anything was known to be writable, so a run where every locale hit a guard deleted the previous cabinet and replaced it with nothing. The work is now resolved first and the wipe skipped when there is none, matching the refuse-before-producing posture of the guards above it. - The changelog's #105 entry still said updatable help "warns and returns until the cabinet pipeline migrates in #152" -- in the release that is #152. Rewritten, with updatable help left to the Fixed entry. - Unreleased carried two "### Fixed" headings. Merged. - Three new $*Params variables contradicted shorthand.instructions.md and the test file added alongside them. Renamed to Parameters. Suite: 471 passed, 0 failed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012AKaM9i6NyMFDcJNeC34h5 --- CHANGELOG.md | 35 ++++++------ PowerShellBuild/IB.tasks.ps1 | 4 +- .../Public/Build-PSBuildMarkdown.ps1 | 13 ++++- .../Public/Build-PSBuildUpdatableHelp.ps1 | 54 +++++++++++-------- PowerShellBuild/psakeFile.ps1 | 4 +- tests/Build-PSBuildHelp.tests.ps1 | 32 +++++++++++ 6 files changed, 95 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2797f23..5c20229 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,20 +7,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased -### Fixed - -- [**#169**](https://github.com/psake/PowerShellBuild/issues/169) - `Build-PSBuildUpdatableHelp` produces a help cabinet. It never could before: - it needed a module landing page that `Build-PSBuildMarkdown` did not - generate, passed an undefined variable as the cabinet source folder, and was - never given the module name — so any build that reached the - `GenerateUpdatableHelp` task failed with a parameter-binding error. - `Build-PSBuildMarkdown` now writes the landing page, and the task passes the - module name and output path it always should have. Using the task requires a - `HelpInfoUri` in your module manifest; without one it warns and produces - nothing, rather than writing a cabinet with no `HelpInfo.xml` to find it by. - See the [v0.8 → v1.0 migration guide](docs/migration-v0.8-to-v1.0.md). - ### Changed - [**#105**](https://github.com/psake/PowerShellBuild/issues/105) @@ -33,12 +19,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). yourself if you build help. `$PSBPreference.Docs.AlphabeticParamsOrder` is removed, because PlatyPS 1.x always sorts parameters alphabetically and offers no way back. Generated markdown carries the 1.x schema, though its - on-disk layout is unchanged. `Build-PSBuildUpdatableHelp` warns and returns - until the cabinet pipeline migrates in - [#152](https://github.com/psake/PowerShellBuild/issues/152); it could never - succeed in 0.8.x either, see - [#169](https://github.com/psake/PowerShellBuild/issues/169). See the - [v0.8 → v1.0 migration guide](docs/migration-v0.8-to-v1.0.md). + on-disk layout is unchanged, and now includes a module landing page that + 0.14.x never produced. Updatable help is covered separately under **Fixed**. + See the [v0.8 → v1.0 migration guide](docs/migration-v0.8-to-v1.0.md). - [**#144**](https://github.com/psake/PowerShellBuild/issues/144) **Breaking:** `Test-PSBuildScriptAnalysis` now counts PSScriptAnalyzer @@ -70,6 +53,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed +- [**#169**](https://github.com/psake/PowerShellBuild/issues/169) + `Build-PSBuildUpdatableHelp` produces a help cabinet. It never could before: + it needed a module landing page that `Build-PSBuildMarkdown` did not + generate, passed an undefined variable as the cabinet source folder, and was + never given the module name — so any build that reached the + `GenerateUpdatableHelp` task failed with a parameter-binding error. + `Build-PSBuildMarkdown` now writes the landing page, and the task passes the + module name and output path it always should have. Using the task requires a + `HelpInfoUri` in your module manifest; without one it warns and produces + nothing, rather than writing a cabinet with no `HelpInfo.xml` to find it by. + See the [v0.8 → v1.0 migration guide](docs/migration-v0.8-to-v1.0.md). + - [**#147**](https://github.com/psake/PowerShellBuild/issues/147) `Test-PSBuildScriptAnalysis` retries the analysis when a PSScriptAnalyzer rule crashes on an internal race diff --git a/PowerShellBuild/IB.tasks.ps1 b/PowerShellBuild/IB.tasks.ps1 index c2b7a01..969e3d0 100644 --- a/PowerShellBuild/IB.tasks.ps1 +++ b/PowerShellBuild/IB.tasks.ps1 @@ -155,13 +155,13 @@ $genUpdatableHelpPreReqs = { # Synopsis: Create updatable help .cab file based on PlatyPS markdown help Task GenerateUpdatableHelp -if (. $genUpdatableHelpPreReqs) BuildHelp, { - $buildUpdatableHelpParams = @{ + $buildUpdatableHelpParameters = @{ DocsPath = $PSBPreference.Docs.RootDir OutputPath = $PSBPreference.Help.UpdatableHelpOutDir ModulePath = $PSBPreference.Build.ModuleOutDir Module = $PSBPreference.General.ModuleName } - Build-PSBuildUpdatableHelp @buildUpdatableHelpParams + Build-PSBuildUpdatableHelp @buildUpdatableHelpParameters } # Synopsis: Publish module to the defined PowerShell repository diff --git a/PowerShellBuild/Public/Build-PSBuildMarkdown.ps1 b/PowerShellBuild/Public/Build-PSBuildMarkdown.ps1 index 60b449b..24fc461 100644 --- a/PowerShellBuild/Public/Build-PSBuildMarkdown.ps1 +++ b/PowerShellBuild/Public/Build-PSBuildMarkdown.ps1 @@ -128,10 +128,19 @@ function Build-PSBuildMarkdown { ) foreach ($markdownFile in $generatedMarkdown) { $destinationPath = Join-Path -Path $localePath -ChildPath $markdownFile.Name - if ((Test-Path -LiteralPath $destinationPath) -and -not $Overwrite) { - # Already refreshed above; regenerating would discard hand-written prose. + $isModuleLandingPage = $markdownFile.BaseName -eq $ModuleName + + # The landing page is always replaced. It is generated content -- an index of + # the module's commands, plus the module GUID and locale that the cabinet step + # stamps its .cab name from -- so a copy left in place goes stale the moment a + # command is added or removed. PlatyPS has no refresh that would preserve an + # edited body either: Update-MarkdownModuleFile rewrites it wholesale. Command + # help is different, and is skipped here because it was already refreshed in + # place above, where hand-written prose survives. + if (-not $isModuleLandingPage -and (Test-Path -LiteralPath $destinationPath) -and -not $Overwrite) { continue } + Move-Item -LiteralPath $markdownFile.FullName -Destination $destinationPath -Force } } finally { diff --git a/PowerShellBuild/Public/Build-PSBuildUpdatableHelp.ps1 b/PowerShellBuild/Public/Build-PSBuildUpdatableHelp.ps1 index e23b939..37e94af 100644 --- a/PowerShellBuild/Public/Build-PSBuildUpdatableHelp.ps1 +++ b/PowerShellBuild/Public/Build-PSBuildUpdatableHelp.ps1 @@ -57,7 +57,34 @@ function Build-PSBuildUpdatableHelp { return } - $helpLocales = (Get-ChildItem -Path $DocsPath -Directory).Name + # Work out what can actually be built before touching OutputPath. Clearing it first and + # then skipping every locale would delete a previous build's cabinet and replace it with + # nothing, which is the same half-produced outcome the guards above exist to prevent. + $cabinetWork = foreach ($locale in (Get-ChildItem -Path $DocsPath -Directory).Name) { + # The landing page is a module document rather than command help. It is what carries + # the module GUID and locale into the cabinet, and it is generated by + # Build-PSBuildMarkdown; a docs tree from an older build will not have one. + $markdownModuleFile = [IO.Path]::Combine($DocsPath, $locale, "$Module.md") + if (-not (Test-Path -LiteralPath $markdownModuleFile)) { + Write-Warning ($LocalizedData.ModuleLandingPageNotFound -f $markdownModuleFile, $locale) + continue + } + + $cabinetFilesFolder = [IO.Path]::Combine($ModulePath, $locale) + if (-not (Test-Path -LiteralPath $cabinetFilesFolder)) { + Write-Warning ($LocalizedData.FolderDoesNotExist -f $cabinetFilesFolder) + continue + } + + [PSCustomObject]@{ + CabinetFilesFolder = $cabinetFilesFolder + MarkdownModuleFile = $markdownModuleFile + } + } + + if (-not $cabinetWork) { + return + } # Create updatable help output directory if (-not (Test-Path -LiteralPath $OutputPath)) { @@ -77,28 +104,13 @@ function Build-PSBuildUpdatableHelp { Get-ChildItem $OutputPath | Remove-Item @removeItemSplat } - foreach ($locale in $helpLocales) { - # The landing page is a module document rather than command help. It is what carries - # the module GUID, locale, and help version into the cabinet, and it is generated by - # Build-PSBuildMarkdown; a docs tree from an older build will not have one. - $markdownModuleFile = [IO.Path]::Combine($DocsPath, $locale, "$Module.md") - if (-not (Test-Path -LiteralPath $markdownModuleFile)) { - Write-Warning ($LocalizedData.ModuleLandingPageNotFound -f $markdownModuleFile, $locale) - continue - } - - $cabinetFilesFolder = [IO.Path]::Combine($ModulePath, $locale) - if (-not (Test-Path -LiteralPath $cabinetFilesFolder)) { - Write-Warning ($LocalizedData.FolderDoesNotExist -f $cabinetFilesFolder) - continue - } - - $cabinetParams = @{ - CabinetFilesFolder = $cabinetFilesFolder - MarkdownModuleFile = $markdownModuleFile + foreach ($work in $cabinetWork) { + $cabinetParameters = @{ + CabinetFilesFolder = $work.CabinetFilesFolder + MarkdownModuleFile = $work.MarkdownModuleFile OutputFolder = $OutputPath Verbose = ($VerbosePreference -eq 'Continue') } - New-HelpCabinetFile @cabinetParams > $null + New-HelpCabinetFile @cabinetParameters > $null } } diff --git a/PowerShellBuild/psakeFile.ps1 b/PowerShellBuild/psakeFile.ps1 index 9f4e19f..daef4ce 100644 --- a/PowerShellBuild/psakeFile.ps1 +++ b/PowerShellBuild/psakeFile.ps1 @@ -215,14 +215,14 @@ $genUpdatableHelpPreReqs = { $result } Task GenerateUpdatableHelp -Depends $PSBGenerateUpdatableHelpDependency -PreCondition $genUpdatableHelpPreReqs { - $buildUpdatableHelpParams = @{ + $buildUpdatableHelpParameters = @{ DocsPath = $PSBPreference.Docs.RootDir OutputPath = $PSBPreference.Help.UpdatableHelpOutDir ModulePath = $PSBPreference.Build.ModuleOutDir Module = $PSBPreference.General.ModuleName Verbose = ($VerbosePreference -eq 'Continue') } - Build-PSBuildUpdatableHelp @buildUpdatableHelpParams + Build-PSBuildUpdatableHelp @buildUpdatableHelpParameters } -Description 'Create updatable help .cab file based on PlatyPS markdown help' Task Publish -Depends $PSBPublishDependency { diff --git a/tests/Build-PSBuildHelp.tests.ps1 b/tests/Build-PSBuildHelp.tests.ps1 index 82eb025..614fde8 100644 --- a/tests/Build-PSBuildHelp.tests.ps1 +++ b/tests/Build-PSBuildHelp.tests.ps1 @@ -98,6 +98,38 @@ Describe 'Help building functions' -Skip:(-not $script:platyPSAvailable) { $markdown | Should -Not -Match 'schema:\s*2\.0\.0' } + It 'replaces the landing page on rebuild rather than leaving it stale' { + # The cabinet's .cab file name is stamped from this page's module GUID, so a copy + # left in place after the GUID changes names the cabinet for a module that no + # longer exists and Update-Help cannot match it. The page is also the command + # index, which goes stale as soon as a command is added or removed. Command help + # is deliberately not replaced this way -- that is the assertion below. + $landingPagePath = Join-Path -Path $script:markdownScenario.LocalePath -ChildPath ( + '{0}.md' -f $script:markdownScenario.ModuleName + ) + $originalGuid = ([regex]::Match( + (Get-Content -Path $landingPagePath -Raw), 'Module Guid:\s*(\S+)' + )).Groups[1].Value + $originalGuid | Should -Not -BeNullOrEmpty + + $manifestPath = Join-Path -Path $script:markdownScenario.ModulePath -ChildPath ( + '{0}.psd1' -f $script:markdownScenario.ModuleName + ) + $newGuid = [guid]::NewGuid().ToString() + (Get-Content -Path $manifestPath -Raw) -replace [regex]::Escape($originalGuid), $newGuid | + Set-Content -Path $manifestPath + + $rebuildJobParameter = @{ + ModulePath = $script:builtModulePath + CommandName = 'Build-PSBuildMarkdown' + Parameter = New-PSBuildMarkdownParameter -Scenario $script:markdownScenario + } + $rebuild = Invoke-PSBuildCommandInJob @rebuildJobParameter + $rebuild.Threw | Should -BeFalse + + Get-Content -Path $landingPagePath -Raw | Should -Match ([regex]::Escape($newGuid)) + } + It 'keeps the markdown directly under the locale directory' { # New-MarkdownCommandHelp writes to /. The function # flattens that back out, so a nested module directory here means the flattening