From 43e6e8c3079c69069cdd6ab52a1b494d403f7f5b Mon Sep 17 00:00:00 2001 From: Microsoft Graph DevX Tooling Date: Wed, 30 Sep 2026 17:23:23 -0700 Subject: [PATCH 1/2] Prune expired deprecated OpenAPI operations Add Kiota-compatible OpenAPI post-processing that removes expired deprecated operations and empty paths while recording removals in a profile-scoped JSON report. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f8237f39-2085-49c4-8029-67ce5a1a346f --- ...ove-ExpiredDeprecatedOpenApiOperations.ps1 | 406 ++++++++++++++++++ ...piredDeprecatedOpenApiOperations.Tests.ps1 | 240 +++++++++++ tools/UpdateOpenApiKiotaCompat.ps1 | 5 + 3 files changed, 651 insertions(+) create mode 100644 tools/Remove-ExpiredDeprecatedOpenApiOperations.ps1 create mode 100644 tools/Tests/Remove-ExpiredDeprecatedOpenApiOperations.Tests.ps1 diff --git a/tools/Remove-ExpiredDeprecatedOpenApiOperations.ps1 b/tools/Remove-ExpiredDeprecatedOpenApiOperations.ps1 new file mode 100644 index 00000000000..6fd25118aed --- /dev/null +++ b/tools/Remove-ExpiredDeprecatedOpenApiOperations.ps1 @@ -0,0 +1,406 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $OpenApiFilesPath, + + [Parameter(Mandatory = $false)] + [ValidateRange(0, [int]::MaxValue)] + [int] $RemovalDateBufferDays = 2, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [string] $ReportPath, + + [Parameter(Mandatory = $false)] + [datetime] $ReferenceDateUtc = [datetime]::UtcNow +) + +$ErrorActionPreference = 'Stop' +$httpOperationPattern = '^ (?get|put|post|delete|options|head|patch|trace):\s*(?:#.*)?$' +$pathPattern = '^ (?:(?[''"])(?/.+)\k|(?/[^:]*)):\s*(?:#.*)?$' +$cutoffDate = $ReferenceDateUtc.ToUniversalTime().Date.AddDays(-$RemovalDateBufferDays) + +function Read-TextFile { + param( + [Parameter(Mandatory = $true)] + [string] $Path + ) + + $reader = [System.IO.StreamReader]::new( + $Path, + [System.Text.UTF8Encoding]::new($false), + $true + ) + try { + $text = $reader.ReadToEnd() + return [pscustomobject]@{ + Text = $text + Encoding = $reader.CurrentEncoding + } + } + finally { + $reader.Dispose() + } +} + +function Get-RemovalDate { + param( + [Parameter(Mandatory = $true)] + [string[]] $Lines, + + [Parameter(Mandatory = $true)] + [int] $OperationStart, + + [Parameter(Mandatory = $true)] + [int] $OperationEnd, + + [Parameter(Mandatory = $true)] + [string] $FilePath, + + [Parameter(Mandatory = $true)] + [string] $Uri, + + [Parameter(Mandatory = $true)] + [string] $Method + ) + + $hasDeprecatedLabel = $false + $deprecationExtensionIndex = -1 + for ($lineIndex = $OperationStart + 1; $lineIndex -lt $OperationEnd; $lineIndex++) { + $line = $Lines[$lineIndex].TrimEnd([char[]]"`r`n") + if ($line -match '^ deprecated:\s*true\s*(?:#.*)?$') { + $hasDeprecatedLabel = $true + } + elseif ($line -match '^ x-ms-deprecation:\s*(?:#.*)?$') { + $deprecationExtensionIndex = $lineIndex + } + } + + if (-not $hasDeprecatedLabel -or $deprecationExtensionIndex -lt 0) { + return $null + } + + for ($lineIndex = $deprecationExtensionIndex + 1; $lineIndex -lt $OperationEnd; $lineIndex++) { + $line = $Lines[$lineIndex].TrimEnd([char[]]"`r`n") + if ($line -match '^ {0,6}\S') { + break + } + + if ($line -match '^ removalDate:\s*(?[^#]*?)\s*(?:#.*)?$') { + $value = $Matches.value.Trim() + if ( + $value.Length -ge 2 -and + (($value.StartsWith("'") -and $value.EndsWith("'")) -or + ($value.StartsWith('"') -and $value.EndsWith('"'))) + ) { + $value = $value.Substring(1, $value.Length - 2) + } + + $removalDate = [datetime]::MinValue + $isValidDate = [datetime]::TryParseExact( + $value, + 'yyyy-MM-dd', + [System.Globalization.CultureInfo]::InvariantCulture, + [System.Globalization.DateTimeStyles]::None, + [ref] $removalDate + ) + if (-not $isValidDate) { + throw "Invalid removalDate '$value' for $($Method.ToUpperInvariant()) '$Uri' in '$FilePath'. Expected yyyy-MM-dd." + } + + return $removalDate.Date + } + } + + return $null +} + +function Remove-LineRanges { + param( + [Parameter(Mandatory = $true)] + [string[]] $Lines, + + [Parameter(Mandatory = $true)] + [AllowEmptyCollection()] + [object[]] $Ranges + ) + + if ($Ranges.Count -eq 0) { + return ($Lines -join '') + } + + $builder = [System.Text.StringBuilder]::new() + $cursor = 0 + foreach ($range in ($Ranges | Sort-Object Start)) { + while ($cursor -lt $range.Start) { + [void] $builder.Append($Lines[$cursor]) + $cursor++ + } + $cursor = $range.End + } + while ($cursor -lt $Lines.Count) { + [void] $builder.Append($Lines[$cursor]) + $cursor++ + } + + return $builder.ToString() +} + +function Get-OpenApiPruningPlan { + param( + [Parameter(Mandatory = $true)] + [System.IO.FileInfo] $File, + + [Parameter(Mandatory = $true)] + [datetime] $CutoffDate + ) + + $fileContent = Read-TextFile -Path $File.FullName + $lines = [regex]::Split($fileContent.Text, '(?<=\n)') + $pathsStart = -1 + $pathsEnd = $lines.Count + + for ($lineIndex = 0; $lineIndex -lt $lines.Count; $lineIndex++) { + $line = $lines[$lineIndex].TrimEnd([char[]]"`r`n") + if ($line -match '^paths:\s*(?:#.*)?$') { + $pathsStart = $lineIndex + break + } + } + + if ($pathsStart -lt 0) { + return [pscustomobject]@{ + File = $File + Encoding = $fileContent.Encoding + UpdatedContent = $fileContent.Text + RemovalEntries = @() + RemovedCount = 0 + } + } + + for ($lineIndex = $pathsStart + 1; $lineIndex -lt $lines.Count; $lineIndex++) { + $line = $lines[$lineIndex].TrimEnd([char[]]"`r`n") + if ($line -match '^\S') { + $pathsEnd = $lineIndex + break + } + } + + $pathStarts = [System.Collections.Generic.List[object]]::new() + $hasPathObjectExtensions = $false + for ($lineIndex = $pathsStart + 1; $lineIndex -lt $pathsEnd; $lineIndex++) { + $line = $lines[$lineIndex].TrimEnd([char[]]"`r`n") + if ($line -match $pathPattern) { + $pathStarts.Add([pscustomobject]@{ + Start = $lineIndex + Uri = $Matches.uri + }) + } + elseif ($line -match '^ (?!#)\S') { + $hasPathObjectExtensions = $true + } + } + + $rangesToRemove = [System.Collections.Generic.List[object]]::new() + $removalEntries = [System.Collections.Generic.List[object]]::new() + $removedPathCount = 0 + for ($pathIndex = 0; $pathIndex -lt $pathStarts.Count; $pathIndex++) { + $path = $pathStarts[$pathIndex] + $pathEnd = $pathsEnd + for ($lineIndex = $path.Start + 1; $lineIndex -lt $pathsEnd; $lineIndex++) { + $line = $lines[$lineIndex].TrimEnd([char[]]"`r`n") + if ($line -match '^ \S') { + $pathEnd = $lineIndex + break + } + } + + $operations = [System.Collections.Generic.List[object]]::new() + for ($lineIndex = $path.Start + 1; $lineIndex -lt $pathEnd; $lineIndex++) { + $line = $lines[$lineIndex].TrimEnd([char[]]"`r`n") + if ($line -match $httpOperationPattern) { + $method = $Matches.method.ToLowerInvariant() + $operationEnd = $pathEnd + for ($nextLineIndex = $lineIndex + 1; $nextLineIndex -lt $pathEnd; $nextLineIndex++) { + $nextLine = $lines[$nextLineIndex].TrimEnd([char[]]"`r`n") + if ($nextLine -match '^ {0,4}\S') { + $operationEnd = $nextLineIndex + break + } + } + + $operations.Add([pscustomobject]@{ + Start = $lineIndex + End = $operationEnd + Method = $method + }) + } + } + + if ($operations.Count -eq 0) { + continue + } + + $expiredOperations = [System.Collections.Generic.List[object]]::new() + foreach ($operation in $operations) { + $removalDate = Get-RemovalDate ` + -Lines $lines ` + -OperationStart $operation.Start ` + -OperationEnd $operation.End ` + -FilePath $File.FullName ` + -Uri $path.Uri ` + -Method $operation.Method + if ($null -ne $removalDate -and $removalDate -le $CutoffDate) { + $expiredOperations.Add($operation) + } + } + + if ($expiredOperations.Count -eq 0) { + continue + } + + $uriRemoved = $expiredOperations.Count -eq $operations.Count + if ($uriRemoved) { + $removedPathCount++ + $rangesToRemove.Add([pscustomobject]@{ + Start = $path.Start + End = $pathEnd + }) + } + else { + foreach ($operation in $expiredOperations) { + $rangesToRemove.Add([pscustomobject]@{ + Start = $operation.Start + End = $operation.End + }) + } + } + + $removalEntries.Add([pscustomobject]@{ + Uri = $path.Uri + OperationsRemoved = @($expiredOperations.Method) + UriRemoved = $uriRemoved + }) + } + + if ( + $pathStarts.Count -gt 0 -and + $removedPathCount -eq $pathStarts.Count -and + -not $hasPathObjectExtensions + ) { + $pathsLineEnding = if ($lines[$pathsStart].EndsWith("`r`n")) { + "`r`n" + } + elseif ($lines[$pathsStart].EndsWith("`n")) { + "`n" + } + else { + '' + } + $lines[$pathsStart] = "paths: {}$pathsLineEnding" + } + + return [pscustomobject]@{ + File = $File + Encoding = $fileContent.Encoding + UpdatedContent = Remove-LineRanges -Lines $lines -Ranges $rangesToRemove + RemovalEntries = @($removalEntries) + RemovedCount = ($removalEntries | ForEach-Object { $_.OperationsRemoved.Count } | Measure-Object -Sum).Sum + } +} + +$resolvedPath = Resolve-Path -LiteralPath $OpenApiFilesPath +if (Test-Path -LiteralPath $resolvedPath -PathType Container) { + $openApiFiles = @(Get-ChildItem -LiteralPath $resolvedPath -File -Filter '*.yml' | Sort-Object Name) + $defaultReportDirectory = $resolvedPath.Path +} +else { + $openApiFile = Get-Item -LiteralPath $resolvedPath + if ($openApiFile.Extension -notin @('.yml', '.yaml')) { + throw "OpenAPI file '$resolvedPath' must have a .yml or .yaml extension." + } + $openApiFiles = @($openApiFile) + $defaultReportDirectory = $openApiFile.DirectoryName +} + +if ($openApiFiles.Count -eq 0) { + throw "No OpenAPI .yml files were found at '$resolvedPath'." +} + +if ([string]::IsNullOrWhiteSpace($ReportPath)) { + $ReportPath = Join-Path $defaultReportDirectory 'deprecated-removals.json' +} +elseif (-not [System.IO.Path]::IsPathRooted($ReportPath)) { + $ReportPath = Join-Path $defaultReportDirectory $ReportPath +} + +# Build and validate every plan before modifying any OpenAPI document. +$pruningPlans = @( + foreach ($openApiFile in $openApiFiles) { + Get-OpenApiPruningPlan -File $openApiFile -CutoffDate $cutoffDate + } +) + +$modules = [ordered]@{} +$totalOperationsRemoved = 0 +foreach ($plan in $pruningPlans) { + if ($plan.RemovalEntries.Count -eq 0) { + continue + } + + $moduleRemovals = [ordered]@{} + foreach ($entry in $plan.RemovalEntries) { + $moduleRemovals[$entry.Uri] = [ordered]@{ + operationsRemoved = @($entry.OperationsRemoved) + uriRemoved = $entry.UriRemoved + } + } + $modules[$plan.File.BaseName] = $moduleRemovals + $totalOperationsRemoved += $plan.RemovedCount +} + +$report = [ordered]@{ + generatedAtUtc = [datetime]::UtcNow.ToString('o') + cutoffDate = $cutoffDate.ToString('yyyy-MM-dd') + bufferDays = $RemovalDateBufferDays + modules = $modules +} +$reportJson = $report | ConvertTo-Json -Depth 10 +$fullReportPath = [System.IO.Path]::GetFullPath($ReportPath) +$reportDirectory = [System.IO.Path]::GetDirectoryName($fullReportPath) +if (-not [System.IO.Directory]::Exists($reportDirectory)) { + throw "Report directory '$reportDirectory' does not exist." +} + +# Prove that the report destination is writable before modifying any OpenAPI document. +$temporaryReportPath = Join-Path ` + $reportDirectory ` + ".$([System.IO.Path]::GetFileName($fullReportPath)).$([guid]::NewGuid()).tmp" +[System.IO.File]::WriteAllText( + $temporaryReportPath, + "$reportJson$([Environment]::NewLine)", + [System.Text.UTF8Encoding]::new($false) +) + +try { + foreach ($plan in $pruningPlans) { + if ($plan.RemovalEntries.Count -gt 0) { + [System.IO.File]::WriteAllText( + $plan.File.FullName, + $plan.UpdatedContent, + $plan.Encoding + ) + } + } + [System.IO.File]::Move($temporaryReportPath, $fullReportPath, $true) +} +finally { + if ([System.IO.File]::Exists($temporaryReportPath)) { + [System.IO.File]::Delete($temporaryReportPath) + } +} + +Write-Host "Removed $totalOperationsRemoved expired deprecated operation(s). Report: $fullReportPath" diff --git a/tools/Tests/Remove-ExpiredDeprecatedOpenApiOperations.Tests.ps1 b/tools/Tests/Remove-ExpiredDeprecatedOpenApiOperations.Tests.ps1 new file mode 100644 index 00000000000..83146acbf18 --- /dev/null +++ b/tools/Tests/Remove-ExpiredDeprecatedOpenApiOperations.Tests.ps1 @@ -0,0 +1,240 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +BeforeAll { + $scriptPath = Join-Path $PSScriptRoot '..\Remove-ExpiredDeprecatedOpenApiOperations.ps1' + $referenceDate = [datetime]::Parse('2026-09-30T12:00:00Z').ToUniversalTime() + + function Write-TestOpenApi { + param( + [Parameter(Mandatory = $true)] + [string] $Path, + + [Parameter(Mandatory = $true)] + [string] $Content + ) + + $crlfContent = $Content.TrimStart("`r", "`n").Replace("`r`n", "`n").Replace("`n", "`r`n") + [System.IO.File]::WriteAllText( + $Path, + $crlfContent, + [System.Text.UTF8Encoding]::new($false) + ) + } +} + +Describe 'Remove-ExpiredDeprecatedOpenApiOperations' { + BeforeEach { + $casePath = Join-Path $TestDrive ([guid]::NewGuid().ToString()) + $null = New-Item -Path $casePath -ItemType Directory + } + + It 'removes expired operations and empty paths and writes the removal report' { + $openApiPath = Join-Path $casePath 'ModuleA.yml' + Write-TestOpenApi -Path $openApiPath -Content @' +openapi: 3.0.1 +paths: + '/mixed': + get: + operationId: mixed_Get + responses: + 2XX: + description: Success + deprecated: true + x-ms-deprecation: + removalDate: '2026-09-28' + post: + operationId: mixed_Post + responses: + 2XX: + description: Success + deprecated: true + x-ms-deprecation: + removalDate: '2026-10-01' + '/fully-expired': + get: + operationId: fullyExpired_Get + deprecated: true + x-ms-deprecation: + removalDate: '2020-01-01' + delete: + operationId: fullyExpired_Delete + deprecated: true + x-ms-deprecation: + removalDate: '2021-01-01' + x-paths-metadata: retained + '/not-deprecated': + get: + operationId: notDeprecated_Get + deprecated: false + x-ms-deprecation: + removalDate: '2020-01-01' + '/missing-date': + get: + operationId: missingDate_Get + deprecated: true + x-ms-deprecation: + description: No removal date +components: + schemas: {} +'@ + + & $scriptPath ` + -OpenApiFilesPath $casePath ` + -ReferenceDateUtc $referenceDate ` + -RemovalDateBufferDays 2 + + $updatedContent = [System.IO.File]::ReadAllText($openApiPath) + $updatedContent | Should -Not -Match 'mixed_Get' + $updatedContent | Should -Match 'mixed_Post' + $updatedContent | Should -Not -Match '/fully-expired' + $updatedContent | Should -Match 'x-paths-metadata: retained' + $updatedContent | Should -Match 'notDeprecated_Get' + $updatedContent | Should -Match 'missingDate_Get' + $updatedContent | Should -Match "`r`n" + $updatedContent.Replace("`r`n", '') | Should -Not -Match "`n" + + $reportPath = Join-Path $casePath 'deprecated-removals.json' + $report = Get-Content -LiteralPath $reportPath -Raw | ConvertFrom-Json + $report.cutoffDate | Should -Be '2026-09-28' + $report.bufferDays | Should -Be 2 + $report.generatedAtUtc | Should -Not -BeNullOrEmpty + + $moduleReport = $report.modules.ModuleA + $mixedReport = $moduleReport.PSObject.Properties['/mixed'].Value + @($mixedReport.operationsRemoved) | Should -Be @('get') + $mixedReport.uriRemoved | Should -BeFalse + + $removedUriReport = $moduleReport.PSObject.Properties['/fully-expired'].Value + @($removedUriReport.operationsRemoved) | Should -Be @('get', 'delete') + $removedUriReport.uriRemoved | Should -BeTrue + } + + It 'uses the configurable buffer and removes operations on the cutoff date' { + $openApiPath = Join-Path $casePath 'ModuleB.yml' + Write-TestOpenApi -Path $openApiPath -Content @' +openapi: 3.0.1 +paths: + '/buffered': + get: + operationId: buffered_Get + deprecated: true + x-ms-deprecation: + removalDate: '2026-09-29' +components: + schemas: {} +'@ + + & $scriptPath ` + -OpenApiFilesPath $openApiPath ` + -ReferenceDateUtc $referenceDate ` + -RemovalDateBufferDays 2 ` + -ReportPath 'first-report.json' + + [System.IO.File]::ReadAllText($openApiPath) | Should -Match 'buffered_Get' + + & $scriptPath ` + -OpenApiFilesPath $openApiPath ` + -ReferenceDateUtc $referenceDate ` + -RemovalDateBufferDays 1 + + [System.IO.File]::ReadAllText($openApiPath) | Should -Not -Match '/buffered' + $report = Get-Content -LiteralPath (Join-Path $casePath 'deprecated-removals.json') -Raw | + ConvertFrom-Json + $report.cutoffDate | Should -Be '2026-09-29' + $report.modules.ModuleB.PSObject.Properties['/buffered'].Value.uriRemoved | + Should -BeTrue + } + + It 'is idempotent and replaces the report with an empty removal map' { + $openApiPath = Join-Path $casePath 'ModuleC.yml' + Write-TestOpenApi -Path $openApiPath -Content @' +openapi: 3.0.1 +paths: + '/expired': + trace: + operationId: expired_Trace + deprecated: true + x-ms-deprecation: + removalDate: '2020-01-01' +components: + schemas: {} +'@ + + & $scriptPath -OpenApiFilesPath $casePath -ReferenceDateUtc $referenceDate + $contentAfterFirstRun = [System.IO.File]::ReadAllText($openApiPath) + $contentAfterFirstRun | Should -Match '(?m)^paths: \{\}\r?$' + & $scriptPath -OpenApiFilesPath $casePath -ReferenceDateUtc $referenceDate + + [System.IO.File]::ReadAllText($openApiPath) | Should -BeExactly $contentAfterFirstRun + $report = Get-Content -LiteralPath (Join-Path $casePath 'deprecated-removals.json') -Raw | + ConvertFrom-Json + @($report.modules.PSObject.Properties).Count | Should -Be 0 + } + + It 'fails before modifying any document when a removal date is malformed' { + $validPath = Join-Path $casePath 'AValid.yml' + $validContent = @' +openapi: 3.0.1 +paths: + '/expired': + get: + operationId: expired_Get + deprecated: true + x-ms-deprecation: + removalDate: '2020-01-01' +components: + schemas: {} +'@ + Write-TestOpenApi -Path $validPath -Content $validContent + $originalValidContent = [System.IO.File]::ReadAllText($validPath) + + $invalidPath = Join-Path $casePath 'BInvalid.yml' + Write-TestOpenApi -Path $invalidPath -Content @' +openapi: 3.0.1 +paths: + '/invalid': + patch: + operationId: invalid_Patch + deprecated: true + x-ms-deprecation: + removalDate: 'September 1, 2020' +components: + schemas: {} +'@ + + { + & $scriptPath -OpenApiFilesPath $casePath -ReferenceDateUtc $referenceDate + } | Should -Throw "*Invalid removalDate 'September 1, 2020'*PATCH '/invalid'*" + + [System.IO.File]::ReadAllText($validPath) | Should -BeExactly $originalValidContent + Test-Path -LiteralPath (Join-Path $casePath 'deprecated-removals.json') | + Should -BeFalse + } + + It 'fails before modifying a document when the report directory is invalid' { + $openApiPath = Join-Path $casePath 'ModuleD.yml' + Write-TestOpenApi -Path $openApiPath -Content @' +openapi: 3.0.1 +paths: + '/expired': + get: + operationId: expired_Get + deprecated: true + x-ms-deprecation: + removalDate: '2020-01-01' +components: + schemas: {} +'@ + $originalContent = [System.IO.File]::ReadAllText($openApiPath) + $invalidReportPath = Join-Path $casePath 'missing\deprecated-removals.json' + + { + & $scriptPath ` + -OpenApiFilesPath $openApiPath ` + -ReferenceDateUtc $referenceDate ` + -ReportPath $invalidReportPath + } | Should -Throw "*Report directory*does not exist*" + + [System.IO.File]::ReadAllText($openApiPath) | Should -BeExactly $originalContent + } +} diff --git a/tools/UpdateOpenApiKiotaCompat.ps1 b/tools/UpdateOpenApiKiotaCompat.ps1 index cb7ed859c08..863ba7fd000 100644 --- a/tools/UpdateOpenApiKiotaCompat.ps1 +++ b/tools/UpdateOpenApiKiotaCompat.ps1 @@ -38,6 +38,7 @@ $OpenApiDocOutput = Join-Path $OpenApiDocOutput $GraphVersion # Load PS Scripts $DownloadOpenApiDocPS1 = Join-Path $PSScriptRoot ".\DownloadOpenApiDocKiotaCompat.ps1" -Resolve +$RemoveExpiredDeprecatedOperationsPS1 = Join-Path $PSScriptRoot ".\Remove-ExpiredDeprecatedOpenApiOperations.ps1" -Resolve if (-not (Test-Path $ModuleMappingConfigPath)) { Write-Error "Module mapping file not be found: $ModuleMappingConfigPath." @@ -68,6 +69,10 @@ $ModuleMapping.Keys | ForEach-Object -Begin { $RequestCount = 0 } -End { Write-D $RequestCount++ } } + +# Remove operations whose deprecation removal date has passed and record the removals. +& $RemoveExpiredDeprecatedOperationsPS1 -OpenApiFilesPath $OpenApiDocOutput + $stopwatch.Stop() Write-Debug "Downloaded $GraphVersion Kiota-compatible OpenAPI files in '$($Stopwatch.Elapsed.TotalMinutes)` minutes." Write-Host -ForegroundColor Green "-------------Done-------------" From c195117270a03e37a426e231d4ac997728c25650 Mon Sep 17 00:00:00 2001 From: Microsoft Graph DevX Tooling Date: Wed, 30 Sep 2026 17:25:59 -0700 Subject: [PATCH 2/2] Document deprecated OpenAPI pruning Explain single-document and profile-directory usage, cutoff buffering, report placement, and removal behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f8237f39-2085-49c4-8029-67ce5a1a346f --- tools/README.md | 1 + ...move-ExpiredDeprecatedOpenApiOperations.md | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 tools/Remove-ExpiredDeprecatedOpenApiOperations.md diff --git a/tools/README.md b/tools/README.md index 53ba1fde822..15c0bbd7535 100644 --- a/tools/README.md +++ b/tools/README.md @@ -40,6 +40,7 @@ Everything else is a narrower tool for one of the scenarios below. | `Derive-ParityResolutions.ps1` | derives parity renames/suppressions for the whole surface | frozen input ledger, or `-CaptureInput` to build one | `data/parity-*.json`, ledger CSVs | | `Update-WrapperParityData.ps1` | orchestrates a clean parity refresh in an isolated copy | — | the four `data/parity-*` files | | `New-WrapperOutputManifest.ps1` | reviewable inventory of the committed output | the corpus | `docs/WrapperCmdlets-*.csv` | +| [`Remove-ExpiredDeprecatedOpenApiOperations.ps1`](Remove-ExpiredDeprecatedOpenApiOperations.md) | prunes expired deprecated operations from one Kiota OpenAPI document or a profile directory | `openApiDocs_KiotaCompat` YAML | updated YAML and `deprecated-removals.json` | ## What depends on what diff --git a/tools/Remove-ExpiredDeprecatedOpenApiOperations.md b/tools/Remove-ExpiredDeprecatedOpenApiOperations.md new file mode 100644 index 00000000000..312c66b9c25 --- /dev/null +++ b/tools/Remove-ExpiredDeprecatedOpenApiOperations.md @@ -0,0 +1,56 @@ +# Remove expired deprecated OpenAPI operations + +`Remove-ExpiredDeprecatedOpenApiOperations.ps1` removes HTTP operations that: + +- have `deprecated: true`; +- have an `x-ms-deprecation.removalDate` in `yyyy-MM-dd` format; and +- have a removal date on or before UTC today minus the configured buffer. + +The default buffer is two days. The script updates OpenAPI documents in place and writes a +`deprecated-removals.json` report next to the processed document or in the processed directory. + +## Process one API document + +Pass a `.yml` or `.yaml` file to `OpenApiFilesPath`: + +```powershell +.\tools\Remove-ExpiredDeprecatedOpenApiOperations.ps1 ` + -OpenApiFilesPath .\openApiDocs_KiotaCompat\beta\Users.yml +``` + +## Process every API document in a profile + +Pass a directory to process each `.yml` file directly within it: + +```powershell +.\tools\Remove-ExpiredDeprecatedOpenApiOperations.ps1 ` + -OpenApiFilesPath .\openApiDocs_KiotaCompat\v1.0 +``` + +## Change the buffer or report location + +```powershell +.\tools\Remove-ExpiredDeprecatedOpenApiOperations.ps1 ` + -OpenApiFilesPath .\openApiDocs_KiotaCompat\beta\Users.yml ` + -RemovalDateBufferDays 1 ` + -ReportPath .\artifacts\users-deprecated-removals.json +``` + +`ReportPath` can be absolute or relative to the processed document's directory. Its parent +directory must already exist. + +## Removal behavior + +- Expired operations are removed individually. +- A URI path is removed only when no HTTP operations remain. +- Deprecated operations without `removalDate` are retained. +- An invalid `removalDate` stops processing before any OpenAPI document is changed. +- The JSON report is keyed by module filename and URI and lists removed methods plus whether the + URI itself was removed. + +The Kiota-compatible refresh invokes this script automatically after downloading a profile: + +```powershell +.\tools\UpdateOpenApiKiotaCompat.ps1 +.\tools\UpdateOpenApiKiotaCompat.ps1 -BetaGraphVersion +```