Modules/AzureDevOpsDsc.Common/Resources/Functions/Public/AzDoWikiPage/Set-AzDoWikiPage.ps1

<#
.SYNOPSIS
Updates an Azure DevOps wiki page.
 
.DESCRIPTION
Re-reads the page immediately before writing it, so the ETag sent back as 'If-Match' is the page's
current version rather than one that may have gone stale during the wait between Get and Set. A
stale ETag is rejected with 412; Set-DevOpsWikiPage surfaces that as a clear error rather than a
raw HTTP status.
 
Content is only rewritten when it actually differs (normalized for line endings and trailing
whitespace only) - leaving Content and ContentPath both unspecified, or unchanged, updates the
page's order alone without touching its content. Order is only compared, and only moved, when it
was actually configured; Order left unspecified never triggers a page move.
 
The refusals Get-AzDoWikiPage decides on are re-checked here directly, because a Get status of
Error still routes to Set (see CLAUDE.md) and Set can also be reached without a prior Get in a
direct call.
 
.PARAMETER ProjectName
The name of the Azure DevOps project.
 
.PARAMETER WikiName
The name of the wiki that contains this page.
 
.PARAMETER Path
The full path of the wiki page.
 
.PARAMETER Content
The page's desired Markdown content. Mutually exclusive with ContentPath.
 
.PARAMETER ContentPath
A local file to read the page's Markdown content from. Mutually exclusive with Content.
 
.PARAMETER Order
The page's desired position among its sibling pages. Unspecified (sentinel -1), the page's order
is left alone.
 
.PARAMETER AllowRecursiveDelete
Permit deletion of a page that still has sub-pages. Unused by Set; declared because the DSC base
class splats every resource property into every action function.
 
.PARAMETER LookupResult
The lookup result supplied by the DSC base class.
 
.PARAMETER Ensure
The desired state, supplied by the DSC base class.
 
.PARAMETER Force
Forces the operation, supplied by the DSC base class.
 
.EXAMPLE
Set-AzDoWikiPage -ProjectName 'Contoso' -WikiName 'Contoso.wiki' -Path '/Runbooks/On-call' -Content '# On-call (updated)'
#>

Function Set-AzDoWikiPage
{
    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSObject[]])]
    param
    (
        [Parameter(Mandatory = $true)]
        [Alias('Name')]
        [System.String]$ProjectName,

        [Parameter(Mandatory = $true)]
        [System.String]$WikiName,

        [Parameter(Mandatory = $true)]
        [System.String]$Path,

        [Parameter()]
        [AllowEmptyString()]
        [System.String]$Content,

        [Parameter()]
        [AllowEmptyString()]
        [System.String]$ContentPath,

        [Parameter()]
        [System.Int32]$Order,

        [Parameter()]
        [System.Boolean]$AllowRecursiveDelete,

        [Parameter()]
        [HashTable]$LookupResult,

        [Parameter()]
        [Ensure]$Ensure,

        [Parameter()]
        [System.Management.Automation.SwitchParameter]$Force
    )

    Write-Verbose "[Set-AzDoWikiPage] Started."

    $normalizedPath = Format-AzDoWikiPagePath -Path $Path
    $organization   = Get-AzDoOrganizationName

    $hasContent     = -not [String]::IsNullOrEmpty($Content)
    $hasContentPath = -not [String]::IsNullOrWhiteSpace($ContentPath)

    if ($hasContent -and $hasContentPath)
    {
        throw "[Set-AzDoWikiPage] Wiki page '$normalizedPath' in project '$ProjectName' supplies both Content and ContentPath. They are mutually exclusive; supply only one."
    }

    if ($hasContentPath -and -not (Test-Path -LiteralPath $ContentPath -PathType Leaf))
    {
        throw "[Set-AzDoWikiPage] ContentPath '$ContentPath' for wiki page '$normalizedPath' in project '$ProjectName' does not exist."
    }

    # $null means neither was specified - leave the page's content alone.
    $desiredContent = if ($hasContentPath) { Get-Content -LiteralPath $ContentPath -Raw } elseif ($hasContent) { $Content } else { $null }

    $project = Resolve-AzDoProject -ProjectName $ProjectName
    if ($null -eq $project)
    {
        throw "[Set-AzDoWikiPage] Project '$ProjectName' does not exist. Cannot update wiki page '$normalizedPath'."
    }

    $wiki = Resolve-AzDoWiki -ProjectName $ProjectName -WikiName $WikiName
    if ($null -eq $wiki)
    {
        throw "[Set-AzDoWikiPage] Wiki '$WikiName' does not exist in project '$ProjectName'. Cannot update wiki page '$normalizedPath'."
    }

    if ("$($wiki.type)" -eq 'codeWiki')
    {
        throw "[Set-AzDoWikiPage] Wiki '$WikiName' in project '$ProjectName' is a code wiki: its pages are the content of a Git branch, so writing to one is a commit, not a configuration change. This resource only manages pages in a project wiki."
    }

    $wikiIdentifier = if ($wiki.id) { $wiki.id } else { $wiki.name }

    # Read the page fresh, inside this same Set, so the ETag used as If-Match is the page's
    # current version rather than one read earlier that may since have gone stale.
    $lookup = Get-DevOpsWikiPage -Organization $organization -ProjectName $ProjectName -WikiIdentifier $wikiIdentifier -Path $normalizedPath -IncludeContent

    if ($null -eq $lookup -or $null -eq $lookup.Page)
    {
        throw "[Set-AzDoWikiPage] Wiki page '$normalizedPath' in wiki '$WikiName' no longer exists. Cannot update it."
    }

    $contentUpdated = $false

    if ($null -ne $desiredContent)
    {
        $normalizedCurrent = ConvertTo-NormalizedWikiPageContent -Content $lookup.Page.content
        $normalizedDesired = ConvertTo-NormalizedWikiPageContent -Content $desiredContent

        if ($normalizedCurrent -ne $normalizedDesired)
        {
            Write-Verbose "[Set-AzDoWikiPage] Updating content for wiki page '$normalizedPath'."
            $null = Set-DevOpsWikiPage -Organization $organization -ProjectName $ProjectName -WikiIdentifier $wikiIdentifier `
                -Path $normalizedPath -Content $desiredContent -ETag $lookup.ETag
            $contentUpdated = $true
        }
        else
        {
            Write-Verbose "[Set-AzDoWikiPage] Content for wiki page '$normalizedPath' already matches the desired state."
        }
    }

    if ($PSBoundParameters.ContainsKey('Order') -and $Order -ge 0 -and [int]$lookup.Page.order -ne $Order)
    {
        Write-Verbose "[Set-AzDoWikiPage] Moving wiki page '$normalizedPath' to order $Order."

        try
        {
            $null = Move-DevOpsWikiPage -Organization $organization -ProjectName $ProjectName -WikiIdentifier $wikiIdentifier `
                -Path $normalizedPath -NewOrder $Order
        }
        catch
        {
            if ($contentUpdated)
            {
                throw "[Set-AzDoWikiPage] Wiki page '$normalizedPath' had its content updated, but could not be moved to order $Order. Error: $_"
            }

            throw "[Set-AzDoWikiPage] Wiki page '$normalizedPath' could not be moved to order $Order. Error: $_"
        }
    }
}