Public/Sync-KnowledgeArticles.ps1
|
function Sync-KnowledgeArticles { <# .SYNOPSIS Pushes new or updated knowledge articles to ServiceNow as drafts. .DESCRIPTION Takes knowledge gap objects (from Get-KnowledgeGaps or manual input) and creates new KB articles in ServiceNow as drafts. Articles are NEVER published directly; they always require human review and manual publication. Supports -WhatIf for preview and -ReviewOnly for local file generation. .EXAMPLE $gaps = Get-KnowledgeGaps -Provider ServiceNow -Instance 'company.service-now.com' -Credential $cred $gaps | Sync-KnowledgeArticles -Instance 'company.service-now.com' -Credential $cred -KnowledgeBase 'abc123' .EXAMPLE $gaps | Sync-KnowledgeArticles -ReviewOnly -OutputDirectory '.\kb-review' #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory, ValueFromPipeline)] [object[]]$Articles, [Parameter()] [string]$Instance, [Parameter()] [PSCredential]$Credential, [Parameter()] [string]$ApiKey, [Parameter(HelpMessage = 'ServiceNow Knowledge Base sys_id')] [string]$KnowledgeBase, [Parameter()] [string]$Category, [Parameter()] [switch]$ReviewOnly, [Parameter()] [string]$OutputDirectory = '.\kb-review' ) begin { $allArticles = @() # Validate parameters for push mode if (-not $ReviewOnly) { if (-not $Instance) { throw 'ServiceNow -Instance is required when not using -ReviewOnly.' } if (-not $KnowledgeBase) { throw '-KnowledgeBase (ServiceNow KB sys_id) is required when pushing articles.' } } $results = @() } process { foreach ($article in $Articles) { $allArticles += $article } } end { if ($allArticles.Count -eq 0) { Write-Warning 'No articles provided for sync.' return @() } Write-Verbose "Processing $($allArticles.Count) articles (ReviewOnly: $ReviewOnly)" # ── ReviewOnly mode: save as local markdown files ── if ($ReviewOnly) { if (-not (Test-Path $OutputDirectory)) { New-Item -Path $OutputDirectory -ItemType Directory -Force | Out-Null } foreach ($article in $allArticles) { $title = if ($article.SuggestedTitle) { $article.SuggestedTitle } elseif ($article.Topic) { $article.Topic } else { 'Untitled Article' } $content = if ($article.SuggestedContent) { $article.SuggestedContent } else { '(No content generated)' } $relatedTickets = if ($article.RelatedTickets) { ($article.RelatedTickets -join ', ') } else { 'None' } $gapType = if ($article.GapType) { $article.GapType } else { 'New' } # Build markdown content $markdown = @" # $title **Gap Type:** $gapType **Related Tickets:** $relatedTickets **Category:** $(if ($Category) { $Category } else { 'Uncategorized' }) **Status:** Draft - Pending Review --- ## Article Content $content --- *Generated by ITSM-Insights on $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')* *This article requires human review before publication.* "@ # Sanitize filename $safeTitle = $title -replace '[^\w\s-]', '' -replace '\s+', '-' $safeTitle = $safeTitle.Substring(0, [math]::Min($safeTitle.Length, 80)) $filePath = Join-Path $OutputDirectory "$safeTitle.md" $markdown | Out-File -FilePath $filePath -Encoding UTF8 -Force Write-Verbose "Saved review article: $filePath" $results += [PSCustomObject]@{ ArticleTitle = $title Status = 'ReviewPending' SysId = $null Url = $filePath GapType = $gapType } } Write-Host "Saved $($results.Count) articles for review in: $OutputDirectory" -ForegroundColor Cyan return $results } # ── Push mode: create articles in ServiceNow ── $authParams = @{ Instance = $Instance } if ($Credential) { $authParams['Credential'] = $Credential } if ($ApiKey) { $authParams['ApiKey'] = $ApiKey } foreach ($article in $allArticles) { $title = if ($article.SuggestedTitle) { $article.SuggestedTitle } elseif ($article.Topic) { $article.Topic } else { 'Untitled Article' } $content = if ($article.SuggestedContent) { $article.SuggestedContent } else { '(Content pending)' } $relatedTickets = if ($article.RelatedTickets) { ($article.RelatedTickets -join ', ') } else { '' } # Build article HTML body for ServiceNow $articleBody = @" <h2>$([System.Net.WebUtility]::HtmlEncode($title))</h2> $([System.Net.WebUtility]::HtmlEncode($content) -replace "`n", '<br/>') <hr/> <p><em>Related Tickets: $([System.Net.WebUtility]::HtmlEncode($relatedTickets))</em></p> <p><em>Auto-generated by ITSM-Insights. This article requires review before publication.</em></p> "@ # ServiceNow KB article payload — ALWAYS draft $kbPayload = @{ short_description = $title text = $articleBody kb_knowledge_base = $KnowledgeBase workflow_state = 'draft' } if ($Category) { $kbPayload['category'] = $Category } # ShouldProcess check if ($PSCmdlet.ShouldProcess("ServiceNow KB: $title", 'Create draft knowledge article')) { try { Write-Verbose "Creating KB article: $title" $response = Connect-ServiceNow @authParams -Method POST ` -Endpoint 'api/now/table/kb_knowledge' ` -Body $kbPayload $createdArticle = if ($response.PSObject.Properties['result']) { $response.result } else { $response } $sysId = $createdArticle.sys_id $articleNumber = $createdArticle.number $baseUrl = if ($Instance -match '^https?://') { $Instance.TrimEnd('/') } else { "https://$Instance" } $articleUrl = "$baseUrl/kb_view.do?sys_kb_id=$sysId" Write-Verbose "Created article: $articleNumber ($sysId)" $results += [PSCustomObject]@{ ArticleTitle = $title Status = 'Created' SysId = $sysId Url = $articleUrl GapType = if ($article.GapType) { $article.GapType } else { 'New' } Number = $articleNumber } } catch { Write-Warning "Failed to create KB article '$title': $($_.Exception.Message)" $results += [PSCustomObject]@{ ArticleTitle = $title Status = "Failed: $($_.Exception.Message)" SysId = $null Url = $null GapType = if ($article.GapType) { $article.GapType } else { 'New' } Number = $null } } } else { # WhatIf was used $results += [PSCustomObject]@{ ArticleTitle = $title Status = 'WhatIf' SysId = $null Url = $null GapType = if ($article.GapType) { $article.GapType } else { 'New' } Number = $null } } } $created = @($results | Where-Object { $_.Status -eq 'Created' }).Count $failed = @($results | Where-Object { $_.Status -like 'Failed*' }).Count Write-Host "Sync complete: $created created, $failed failed (all as DRAFT - human review required)" -ForegroundColor Cyan return $results } } |