Functions/GenXdev.Coding/Publish-ReleaseNotesToManifest.ps1

<##############################################################################
Part of PowerShell module : GenXdev.Coding
Original cmdlet filename : Publish-ReleaseNotesToManifest.ps1
Original author : René Vaessen / GenXdev
Version : 3.28.2026
################################################################################
Copyright (c) 2026 René Vaessen / GenXdev

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
################################################################################>


###############################################################################
<#
.SYNOPSIS
Publishes uncompleted release notes from README.md to the module manifest.

.DESCRIPTION
Reads the "## Release notes" section from a README.md file, collects all
uncompleted release notes (lines starting with - ☐), writes them into the
ReleaseNotes field of GenXdev.psd1, and marks them as completed (☐ → ☒)
in the README. Both files are updated atomically in one operation.

.PARAMETER UseHomeREADME
Use README.md from PowerShell profile directory instead of current location.

.PARAMETER UseOneDriveREADME
Use README.md from OneDrive directory instead of current location.

.PARAMETER ManifestPath
Path to the .psd1 module manifest to update. If not specified, auto-detects
the GenXdev.psd1 in the module directory.

.EXAMPLE
Publish-ReleaseNotesToManifest

.EXAMPLE
Publish-ReleaseNotesToManifest -ManifestPath ".\MyModule.psd1"
#>

function Publish-ReleaseNotesToManifest {

    [CmdletBinding(SupportsShouldProcess = $true)]
    [Alias('pubrelnotes')]
    param(
        #######################################################################
        [Parameter(
            Mandatory = $true,
            HelpMessage = 'Path to the .psd1 module manifest'
        )]
        [string] $ManifestPath,
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Use README in PowerShell profile directory'
        )]
        [switch] $UseHomeREADME,

        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Use README in OneDrive directory'
        )]
        [switch] $UseOneDriveREADME
    )

    begin {
        Microsoft.PowerShell.Utility\Write-Verbose `
            'Starting Publish-ReleaseNotesToManifest'

        # Resolve README path (same logic as Add-LineToREADME begin block)
        if ($UseHomeREADME) {
            $readmePath = GenXdev\Expand-Path `
                -FilePath "$([IO.Path]::GetDirectoryName($Profile))\README.md"
        }
        elseif ($UseOneDriveREADME) {
            $readmePath = GenXdev\Expand-Path `
                -FilePath "$(GenXdev\Get-KnownFolderPath -KnownFolder 'OneDrive')\README.md" `
                -CreateDirectory
        }
        else {
            # Walk up from current directory to find README
            $startDir = GenXdev\Expand-Path $PWD
            $readmePath = $null
            $currentDir = $startDir
            while ($true) {
                $candidate = "$currentDir\README.md"
                if ([System.IO.File]::Exists($candidate)) {
                    $readmePath = $candidate
                    break
                }
                $parent = GenXdev\Expand-Path "$currentDir\..\"
                if ($parent -eq $currentDir) { break }
                $currentDir = $parent
            }
            if (-not $readmePath) {
                $readmePath = GenXdev\Expand-Path `
                    -FilePath "$(GenXdev\Get-KnownFolderPath -KnownFolder 'OneDrive')\README.md" `
                    -CreateDirectory
            }
        }

        Microsoft.PowerShell.Utility\Write-Verbose `
            "Using README at: ${readmePath}"

        if (![IO.File]::Exists($readmePath)) {
            throw "README file not found at: ${readmePath}"
        }

        if ([String]::IsNullOrWhiteSpace($ManifestPath)) {

            throw "Specify a valid ManifestPath -ManifestPath."
        }

        $ManifestPath = GenXdev\Expand-Path $ManifestPath;

        if (-not [IO.File]::Exists($ManifestPath)) {

            throw "Module manifest not found. Specify -ManifestPath."
        }

        Microsoft.PowerShell.Utility\Write-Verbose `
            "Using manifest at: ${ManifestPath}"
    }

    process {
        # Read README content
        $readmeContent = [IO.File]::ReadAllText($readmePath)
        $lineEnding = if ($readmeContent.IndexOf("`r`n") -ge 0) { "`r`n" } `
            else { "`n" }

        $section = '## Release notes'

        # Find the section
        $sectionStart = $readmeContent.IndexOf(
            $section,
            [System.StringComparison]::OrdinalIgnoreCase
        )
        if ($sectionStart -lt 0) {
            Microsoft.PowerShell.Utility\Write-Warning `
                "Section '${section}' not found in ${readmePath}"
            return
        }

        $sectionContentStart = $sectionStart + $section.Length +
            $lineEnding.Length
        $sectionLevel = ($section -replace '^([#]+).*$', '$1').Length
        $rest = $readmeContent.Substring($sectionContentStart)
        $nextHeading = [regex]::Match($rest, "(?m)^#{1,$sectionLevel}\s")
        $sectionEnd = if ($nextHeading.Success) {
            $sectionContentStart + $nextHeading.Index
        }
        else { $readmeContent.Length }

        $sectionContent = $readmeContent.Substring(
            $sectionContentStart,
            $sectionEnd - $sectionContentStart
        )
        $sectionLines = $sectionContent -split $lineEnding

        # Collect uncompleted release notes (lines with ☐)
        $uncompletedLines = [System.Collections.Generic.List[string]]::new()
        $uncompletedRaw = [System.Collections.Generic.List[string]]::new()
        $lineIndices = [System.Collections.Generic.List[int]]::new()

        for ($i = 0; $i -lt $sectionLines.Count; $i++) {
            $ln = $sectionLines[$i]
            if ($ln -match '^- ☐ \[P') {
                # Strip prefix and date: "- ☐ [P{N}] yyyyMMdd --> " → raw text
                $rawText = $ln -replace '^- ☐ \[P-?\d+\] \d{8}\s*-->\s*', ''
                $null = $uncompletedLines.Add($ln)
                $null = $uncompletedRaw.Add($rawText)
                $null = $lineIndices.Add($i)
            }
        }

        if ($uncompletedRaw.Count -eq 0) {
            Microsoft.PowerShell.Utility\Write-Host `
                'No uncompleted release notes found.'
            return
        }

        # Build release notes text for the manifest
        $releaseNotesText = ($uncompletedRaw -join "`n")

        # Mark lines as done in README
        foreach ($idx in $lineIndices) {
            $sectionLines[$idx] = $sectionLines[$idx].Replace('☐', '☒')
        }
        $newSectionContent = $sectionLines -join $lineEnding
        $newReadmeContent = $readmeContent.Substring(0, $sectionContentStart) +
            $newSectionContent +
            $readmeContent.Substring($sectionEnd)

        # Write both files atomically
        if ($PSCmdlet.ShouldProcess(
                "README and ${ManifestPath}",
                'Publish release notes to manifest'
            )) {
            # Save backup copies first
            $readmeBackup = $readmeContent
            $manifestBackup = [IO.File]::ReadAllText($ManifestPath)

            try {
                GenXdev\Write-TextFileAtomic `
                    -FilePath $readmePath `
                    -Data $newReadmeContent

                PowerShellGet\Update-ModuleManifest -Path $ManifestPath -ReleaseNotes $releaseNotesText -Confirm:$false

                Microsoft.PowerShell.Utility\Write-Host `
                    "Published $($uncompletedRaw.Count) release note(s) to ${ManifestPath}"
            }
            catch {
                # Rollback on failure
                Microsoft.PowerShell.Utility\Write-Warning `
                    "Write failed, rolling back: $_"
                try {
                    GenXdev\Write-TextFileAtomic `
                        -FilePath $readmePath `
                        -Data $readmeBackup
                }
                catch {
                    Microsoft.PowerShell.Utility\Write-Warning `
                        "Rollback also failed: $_"
                }
                throw
            }
        }
    }

    end {
    }
}