Skip to content

Migrate ARM swagger linter rule to typespec linter rule #5274

Description

@msyyc
id swagger linter tsp lib tsp linter PR PR created time PR merged time approved by new comment after latest commit Status notes
1 AdditionalPropertiesAndProperties @azure-tools/typespec-azure-core/bad-record-type - - - - - Done -
2 AllProxyResourcesShouldHaveDelete @azure-tools/typespec-azure-resource-manager/no-resource-delete-operation - - - - - Done -
3 AllTrackedResourcesMustHaveDelete @azure-tools/typespec-azure-resource-manager/no-resource-delete-operation - - - - - Done -
4 ApiHost TypeSpec structural - - - - - Done -
5 ApiVersionParameterRequired @azure-tools/typespec-azure-core/operation-missing-api-version, @azure-tools/typespec-azure-resource-manager/arm-resource-operation - - - - - Done -
6 APIVersionPattern @azure-tools/typespec-azure-resource-manager/arm-resource-invalid-version-format - - - - - Done -
7 ArmResourcePropertiesBag @azure-tools/typespec-azure-resource-manager/arm-resource-duplicate-property - - - - - Done -
8 ArrayMustHaveType TypeSpec structural - - - - - Done -
9 ArraySchemaMustHaveItems TypeSpec structural - - - - - Done -
10 AvoidAdditionalProperties @azure-tools/typespec-azure-resource-manager/arm-no-record - - - - - Done -
11 AvoidEmptyResponseSchema TypeSpec structural - - - - - Done -
12 AvoidNestedProperties TypeSpec structural - - - - - Done -
13 BodyTopLevelProperties @azure-tools/typespec-azure-resource-manager/arm-resource-invalid-envelope-property - - - - - Done -
14 ControlCharactersAreNotAllowed TypeSpec structural - - - - - Done -
15 DefinitionsPropertiesNamesCamelCase @azure-tools/typespec-azure-core/casing-style - - - - - Done -
16 DeleteResponseCodes @azure-tools/typespec-azure-resource-manager/arm-delete-operation-response-codes, @azure-tools/typespec-azure-resource-manager/no-response-body - - - - - Done -
17 EnumMustHaveType TypeSpec structural - - - - - Done -
18 EnumMustNotHaveEmptyValue TypeSpec structural - - - - - Done -
19 EnumMustRespectType TypeSpec structural - - - - - Done -
20 EnumUniqueValue TypeSpec structural - - - - - Done -
21 HostParametersValidation TypeSpec structural - - - - - Done -
22 HttpsSupportedScheme TypeSpec structural - - - - - Done -
23 IntegerTypeMustHaveFormat TypeSpec structural - - - - - Done -
24 InvalidVerbUsed TypeSpec structural - - - - - Done -
25 LroLocationHeader @azure-tools/typespec-azure-resource-manager/lro-location-header - - - - - Done -
26 LroPostMustNotUseOriginalUriAsFinalState TypeSpec structural - - - - - Done -
27 LroStatusCodesReturnTypeSchema TypeSpec structural - - - - - Done -
28 MissingTypeObject TypeSpec structural - - - - - Done -
29 NamePropertyDefinitionInParameter TypeSpec structural - - - - - Done -
30 NextLinkPropertyMustExist TypeSpec structural - - - - - Done -
31 NoDuplicatePathsForScopeParameter TypeSpec structural - - - - - Done -
32 NonEmptyClientName TypeSpec structural - - - - - Done -
33 Nullable @azure-tools/typespec-azure-core/no-nullable - - - - - Done -
34 OperationIdRequired TypeSpec structural - - - - - Done -
35 OperationIdSingleUnderscore TypeSpec structural - - - - - Done -
36 OperationsAPIImplementation @azure-tools/typespec-azure-resource-manager/missing-operations-endpoint - - - - - Done -
37 OperationsApiTenantLevelOnly TypeSpec structural - - - - - Done -
38 OperationSummaryOrDescription @azure-tools/typespec-azure-core/documentation-required - - - - - Done -
39 ParameterDescription @azure-tools/typespec-azure-core/documentation-required - - - - - Done -
40 ParameterDescriptionRequired @azure-tools/typespec-azure-core/documentation-required - - - - - Done -
41 ParameterNotDefinedInGlobalParameters TypeSpec structural - - - - - Done -
42 ParameterOrder TypeSpec structural - - - - - Done -
43 ParametersOrder TypeSpec structural - - - - - Done -
44 PatchContentType TypeSpec structural - - - - - Done -
45 PathForResourceAction TypeSpec structural - - - - - Done -
46 PathResourceTypeNameCamelCase @azure-tools/typespec-azure-resource-manager/arm-resource-path-segment-invalid-chars - - - - - Done -
47 PropertiesTypeObjectNoDefinition @azure-tools/typespec-azure-resource-manager/no-empty-model - - - - - Done -
48 PropertyDescription @azure-tools/typespec-azure-core/documentation-required - - - - - Done -
49 ProvisioningStateMustBeReadOnly @azure-tools/typespec-azure-resource-manager/arm-resource-provisioning-state - - - - - Done -
50 ProvisioningStateSpecifiedForLROPatch TypeSpec structural - - - - - Done -
51 ProvisioningStateSpecifiedForLROPut TypeSpec structural - - - - - Done -
52 ProvisioningStateValidation @azure-tools/typespec-azure-resource-manager/arm-resource-provisioning-state - - - - - Done -
53 PutGetPatchResponseSchema @azure-tools/typespec-azure-resource-manager/arm-resource-operation-response - - - - - Done -
54 PutResponseCodes @azure-tools/typespec-azure-resource-manager/arm-put-operation-response-codes - - - - - Done -
55 RequestSchemaForTrackedResourcesMustHaveTags TypeSpec structural - - - - - Done -
56 ResourceNameRestriction @azure-tools/typespec-azure-resource-manager/arm-resource-name-pattern - - - - - Done -
57 ResponseSchemaSpecifiedForSuccessStatusCode @azure-tools/typespec-azure-resource-manager/no-response-body - - - - - Done -
58 SchemaDescriptionOrTitle @azure-tools/typespec-azure-core/documentation-required - - - - - Done -
59 SchemaNamesConvention @azure-tools/typespec-azure-core/casing-style - - - - - Done -
60 SubscriptionIdParameterInOperations TypeSpec structural - - - - - Done -
61 SystemDataDefinitionsCommonTypes @azure-tools/typespec-azure-resource-manager/arm-resource-invalid-envelope-property - - - - - Done -
62 TrackedExtensionResourcesAreNotAllowed @azure-tools/typespec-azure-resource-manager/arm-resource-invalid-envelope-property - - - - - Done -
63 TrackedResourceBeyondsThirdLevel @azure-tools/typespec-azure-resource-manager/beyond-nesting-levels - - - - - Done -
64 UniqueClientParameterName TypeSpec structural - - - - - Done -
65 UniqueModelName TypeSpec structural - - - - - Done -
66 ValidResponseCodeRequired TypeSpec structural - - - - - Done -
67 XmsIdentifierValidation @azure-tools/typespec-azure-resource-manager/missing-x-ms-identifiers - - - - - Done -
68 XMSSecretInResponse @azure-tools/typespec-azure-resource-manager/secret-prop - - - - - Done -
69 EnumInsteadOfBoolean @azure-tools/typespec-azure-core 5265 2026-08-20 2026-08-31 catalinaperalta - Done -
70 QueryParametersInCollectionGet @azure-tools/typespec-azure-resource-manager 5318 2026-08-26 2026-09-02 timotheeguerin - Done -
71 ValidQueryParametersForPointOperations @azure-tools/typespec-azure-resource-manager 5273 2026-08-20 2026-09-03 catalinaperalta, timotheeguerin - Done -
72 ParametersInPointGet @azure-tools/typespec-azure-resource-manager 5273 2026-08-20 2026-09-03 catalinaperalta, timotheeguerin - Done -
73 NonApplicationJsonType @azure-tools/typespec-azure-resource-manager 5302 2026-08-25 2026-09-03 timotheeguerin - Done -
74 GetCollectionOnlyHasValueAndNextLink @azure-tools/typespec-azure-resource-manager 5358 2026-09-01 2026-09-04 timotheeguerin - Done -
75 XmsPageableForListCalls @azure-tools/typespec-azure-resource-manager 5379 2026-09-02 2026-09-07 timotheeguerin - Done -
76 ParametersInPost @azure-tools/typespec-azure-resource-manager 5281 2026-08-24 2026-09-09 catalinaperalta, timotheeguerin - Done -
77 TenantLevelAPIsNotAllowed @azure-tools/typespec-azure-resource-manager 5420 2026-09-08 2026-09-09 timotheeguerin - Done -
78 GetInOperationName @azure-tools/typespec-client-generator-core 5364 2026-09-01 2026-09-15 timotheeguerin - Done -
79 PutInOperationName @azure-tools/typespec-client-generator-core 5457 2026-09-11 2026-09-15 timotheeguerin - Done -
80 ParametersSchemaAsTypeObject @azure-tools/typespec-azure-resource-manager 5361 2026-09-01 2026-09-18 timotheeguerin - Done -
81 XmsExamplesRequired - 5398 - - - - Hold on -
82 LatestVersionOfCommonTypesMustBeUsed @azure-tools/typespec-azure-resource-manager 5271 2026-08-20 - - No Doing -
83 PatchBodyParametersSchema @azure-tools/typespec-azure-resource-manager 5294 2026-08-25 - - No Doing -
84 GuidUsage @azure-tools/typespec-azure-resource-manager 5336 2026-08-28 - - No Doing heavy
85 UnSupportedPatchProperties @azure-tools/typespec-azure-resource-manager 5294 2026-08-25 - - No Doing heavy
86 ConsistentResponseSchemaForPut @azure-tools/typespec-azure-resource-manager 5423 2026-09-08 - - No Doing heavy
87 ConsistentPatchProperties @azure-tools/typespec-azure-resource-manager 5294 2026-08-25 - - No Doing heavy
88 LroErrorContent @azure-tools/typespec-azure-resource-manager 5451 2026-09-11 - - No Doing -
89 PatchPropertiesCorrespondToPutProperties @azure-tools/typespec-azure-resource-manager 5481 2026-09-15 - - No Doing heavy
90 PutRequestResponseSchemeArm @azure-tools/typespec-azure-resource-manager 5423 2026-09-08 - - No Doing heavy
91 TagsAreNotAllowedForProxyResources @azure-tools/typespec-azure-resource-manager 5502 2026-09-17 - - No Doing heavy
92 XmsResourceInPutResponse @azure-tools/typespec-azure-resource-manager 5423 2026-09-08 - - No Doing heavy
93 SummaryAndDescriptionMustNotBeSame @azure-tools/typespec-azure-core 5507 2026-09-17 - - No Doing -
94 AvoidAnonymousParameter @azure-tools/typespec-azure-core, @azure-tools/typespec-azure-resource-manager 5522 2026-09-20 - - No Doing heavy
95 RepeatedPathInfo @azure-tools/typespec-azure-resource-manager 5526 2026-09-21 - - No Doing -
96 AvoidAnonymousTypes @azure-tools/typespec-azure-core 5551 2026-09-23 - - No Doing heavy
97 EvenSegmentedPathForPutOperation @azure-tools/typespec-azure-resource-manager 5550 2026-09-23 - - No Doing -
98 NoErrorCodeResponses @azure-tools/typespec-azure-resource-manager 5556 2026-09-23 - - No Doing -
99 AllResourcesMustHaveGetOperation - - - - - - To do -
100 AvoidMsdnReferences - - - - - - To do -
101 CollectionObjectPropertiesNaming - - - - - - To do -
102 DeleteInOperationName - - - - - - To do -
103 DeleteResponseBodyEmpty - - - - - - To do -
104 DescriptionMustNotBeNodeName - - - - - - To do -
105 DescriptiveDescriptionRequired - - - - - - To do -
106 ExtensionResourcePathPattern - - - - - - To do -
107 GetOperationMustNotBeLongRunning - - - - - - To do -
108 GetResponseCodes - - - - - - To do -
109 ImplementPrivateEndpointAPIs - - - - - - To do -
110 InvalidSkuModel - - - - - - To do -
111 ListInOperationName - - - - - - To do -
112 LocationMustHaveXmsMutability - - - - - - To do -
113 LongRunningOperationsOptionsValidator - - - - - - To do -
114 LroExtension - - - - - - To do -
115 LroPatch202 - - - - - - To do -
116 MissingSegmentsInNestedResourceListOperation - - - - - - To do -
117 MissingXmsErrorResponse - - - - - - To do -
118 MutabilityWithReadOnly - - - - - - To do -
119 NestedResourcesMustHaveListOperation - - - - - - To do -
120 OperationIdNounConflictingModelNames - - - - - - To do -
121 OperationIdNounVerb - - - - - - To do -
122 OperationsApiResponseSchema - - - - - - To do -
123 OperationsApiSchemaUsesCommonTypes - - - - - - To do -
124 PageableRequires200Response - - - - - - To do -
125 ParameterNotUsingCommonTypes - - - - - - To do -
126 PatchIdentityProperty - - - - - - To do -
127 PatchInOperationName - - - - - - To do -
128 PatchResponseCodes - - - - - - To do -
129 PatchSkuProperty - - - - - - To do -
130 PathContainsResourceGroup - - - - - - To do -
131 PathContainsResourceType - - - - - - To do -
132 PathForNestedResource - - - - - - To do -
133 PathForTrackedResourceTypes - - - - - - To do -
134 PathResourceProviderNamePascalCase - - - - - - To do -
135 PostOperationIdContainsUrlVerb - - - - - - To do -
136 PostResponseCodes @azure-tools/typespec-azure-resource-manager/arm-post-operation-response-codes - - - - - To do -
137 PreviewVersionOverOneYear - - - - - - To do -
138 PutRequestResponseScheme - - - - - - To do -
139 RequestBodyMustExistForPutPatch - - - - - - To do -
140 ReservedResourceNamesModelAsEnum - - - - - - To do -
141 ResourceHasXMsResourceEnabled - - - - - - To do -
142 SubscriptionsAndResourceGroupCasing - - - - - - To do -
143 TopLevelResourcesListByResourceGroup - - - - - - To do -
144 TrackedResourcePatchOperation - - - - - - To do -
145 TrackedResourcesMustHavePut - - - - - - To do -
146 ValidFormats - - - - - - To do -
147 XMSLongRunningOperationProperty - - - - - - To do -
148 XmsPageableMustHaveCorrespondingResponse - - - - - - To do -
149 XmsPathsMustOverloadPaths - - - - - - To do -
150 AzureResourceTagsSchema - - - - - - Need investigation -
151 DefaultErrorResponseSchema - - - - - - Need investigation -
152 DefaultInEnum - - - - - - Need investigation -
153 DeleteMustNotHaveRequestBody - - - - - - Need investigation -
154 DeleteOperationResponses - - - - - - Need investigation -
155 DeprecatedXmsCodeGenerationSetting - - - - - - Need investigation -
156 docLinkLocale - - - - - - Need investigation -
157 GetCollectionResponseSchema - - - - - - Need investigation -
158 GetMustNotHaveRequestBody - - - - - - Need investigation -
159 LicenseHeaderMustNotBeSpecified - - - - - - Need investigation -
160 LroWithOriginalUriAsFinalState - - - - - - Need investigation -
161 MissingDefaultResponse - - - - - - Need investigation -
162 PageableOperation - - - - - - Need investigation -
163 PathContainsSubscriptionId - - - - - - Need investigation -
164 PathResourceProviderMatchNamespace - - - - - - Need investigation -
165 PrivateEndpointResourceSchemaValidation - - - - - - Need investigation -
166 RequiredDefaultResponse - - - - - - Need investigation -
167 RequiredPropertiesMissingInResourceModel - - - - - - Need investigation -
168 RequiredReadOnlySystemData - - - - - - Need investigation -
169 Rpaas_ResourceProvisioningState - - - - - - Need investigation -
170 SecurityDefinitionsStructure - - - - - - Need investigation -
171 SystemDataInPropertiesBag - - - - - - Need investigation -
172 TopLevelResourcesListBySubscription - - - - - - Need investigation -
173 UniqueXmsEnumName - - - - - - Need investigation -
174 UniqueXmsExample - - - - - - Need investigation -
175 XmsClientNameParameter - - - - - - Need investigation -
176 XmsClientNameProperty - - - - - - Need investigation -
177 XmsEnumValidation - - - - - - Need investigation -
178 XmsPageableListByRGAndSubscriptions - - - - - - Need investigation -
179 XmsParameterLocation - - - - - - Need investigation -
180 XmsClientName - - - - - - Need investigation -

AI update notes

This table already contains the fixed set of 180 ARM swagger linter rules. Future AI updates should only refresh rows whose Status is neither Done nor Hold on, except for the notes-only update allowed for Hold on rows below:

  • Use [Swagger Linter Migration] <SwaggerRuleName> -> <TypeSpecRuleName> for new or renamed migration PR titles, for example [Swagger Linter Migration] GuidUsage -> no-uuid, following the review suggestion on #5503. Keep the original Swagger rule name before the arrow so the tracker can discover the PR.
  • Search Azure/typespec-azure PRs using the original Swagger rule name from the table, not the TypeSpec rule name after ->. Accept both the new mapping format and legacy [Swagger Linter Migration] <SwaggerRuleName> titles. Spaces around -> are recommended but not required for matching. Require a rule-name boundary (whitespace, ->, or end of title) to avoid matching longer Swagger rule names. Continue excluding any title containing (origin). Do not rename the table's Swagger rule entries or infer tsp lib from the title.
  • Before title-based discovery, consult the script's $pinnedPrs configuration: pin ConsistentResponseSchemaForPut, PutRequestResponseSchemeArm, and XmsResourceInPutResponse to #5423, which implements the combined put-resource-schema-consistency rule. PR [Swagger Linter Migration] PutRequestResponseSchemeArm + ConsistentResponseSchemaForPut + XmsResourceInPutResponse -> put-resource-schema-consistency #5423 supersedes #5493 and #5503; the latter was closed as superseded by the combined native registration check, as confirmed in this comment. Its combined title lists multiple Swagger rules, so fetch pinned PRs directly by number, bypassing title-based discovery and title-pattern validation. Refresh all three eligible rows from [Swagger Linter Migration] PutRequestResponseSchemeArm + ConsistentResponseSchemaForPut + XmsResourceInPutResponse -> put-resource-schema-consistency #5423's metadata; do not fall back to [Swagger Linter Migration] PutRequestResponseSchemeArm #5493 or [Swagger Linter Migration] XmsResourceInPutResponse -> use-resource-model-for-put #5503 or treat their closure as completion. All other refresh checks and the Done/Hold on protections still apply.
  • This consolidation does not replace arm-put-operation-response-codes or arm-resource-operation-response. The former checks the complete PUT status-code set; the latter checks the canonical resource across lifecycle operations, which intra-PUT consistency cannot replace. The new rule is not enabled yet. Do not infer retirement, equivalent coverage, or enablement of these existing checks from [Swagger Linter Migration] PutRequestResponseSchemeArm + ConsistentResponseSchemaForPut + XmsResourceInPutResponse -> put-resource-schema-consistency #5423; any future consolidation requires explicitly preserved coverage and rollout.
  • Also pin PatchBodyParametersSchema, ConsistentPatchProperties, and UnSupportedPatchProperties in $pinnedPrs to #5294, which implements the combined no-unsafe-patch-body-properties rule. Fetch [Swagger Linter Migration] PatchBodyParametersSchema + ConsistentPatchProperties + UnSupportedPatchProperties -> no-unsafe-patch-body-properties #5294 directly by number, bypassing title-based discovery and title-pattern validation, and refresh all three eligible rows from its metadata. Do not fall back to #5456 or #5384, or treat their closure as completion. Those PRs remain open and are proposed to be superseded after coverage is accepted; this tracker mapping does not assert coverage acceptance or authorize closing them. PatchPropertiesCorrespondToPutProperties (#5481) remains outside this consolidation. All other refresh checks and the Done/Hold on protections still apply.
  • When a valid PR exists, fill tsp linter PR, PR created time, and PR merged time if merged. Set Status to Doing while open and Done once merged.
  • Derive tsp lib from PR changed files: packages/typespec-azure-core -> @azure-tools/typespec-azure-core; packages/typespec-azure-resource-manager -> @azure-tools/typespec-azure-resource-manager.
  • Keep notes as the rightmost column. For every row whose current Status is not Done (including Hold on), check its PR body for the keyword heavy (case-insensitive, whole word). If present, set empty or - notes to heavy, or append ; heavy to existing notes only if they do not already contain the keyword. Preserve other notes, never add a duplicate heavy, and leave notes unchanged when the keyword is absent. Check the body, not the title. Rows without a PR keep their notes unchanged.
  • Once a row's Status is Hold on, only its notes may be updated using the rule above; fetch its existing linked PR directly, without title-based discovery or validation. Do not refresh or edit its other data cells. Its position and id may still change for sorting and renumbering. Done rows remain fully protected, including notes.
  • For each open PR in a row not marked Hold on, refresh approved by with the approving reviewers' GitHub aliases.
  • When Status is Done, set new comment after latest commit to -.
  • For each open PR in a row not marked Hold on, refresh new comment after latest commit as Yes when eligible feedback was created or submitted after the latest head commit; otherwise use No. Check all three feedback surfaces: top-level PR comments (issues/{pr}/comments), inline review comments (pulls/{pr}/comments), and non-empty review summaries (pulls/{pr}/reviews). Compare comment created_at or review submitted_at with the latest head commit's committer date; do not rely on gh pr view --json comments or the PR's updatedAt alone.
  • Only consider real GitHub user aliases for approvals and comments; ignore bot accounts and ignore comments/reviews from the PR author.
  • Preserve markdown table newlines by reading the issue body through gh api ... | ConvertFrom-Json and writing the edited body from a UTF-8 no-BOM file; do not assign gh issue view --jq .body multiline output directly to a PowerShell variable or stringify a line array, because that can collapse adjacent table rows and break the table. After every edit, verify the raw issue body still has 180 table rows and each table line starts and ends with a single row delimiter.
Reusable PowerShell update script

Requires PowerShell 7+ and an authenticated GitHub CLI (gh). Save the script below as refresh-arm-linter-issue.ps1. The authenticated account needs issue-edit permission when applying changes.

  • Preview without changing the issue: pwsh -File .\refresh-arm-linter-issue.ps1
  • Refresh the issue: pwsh -File .\refresh-arm-linter-issue.ps1 -Apply

The script follows the AI update notes above, preserves Done row data and all Hold on data except the permitted notes update (apart from sorting and renumbering IDs), and preserves all content outside the table (including this section). It checks all three feedback surfaces and stops on ambiguous PRs or incomplete data. Dates are UTC. It also recognizes @azure-tools/typespec-client-generator-core when that package contains the implementation. Review the notes before running in case the update policy changes.

# Requires PowerShell 7+ and an authenticated GitHub CLI (gh).
# Preview: pwsh -File .\refresh-arm-linter-issue.ps1
# Update:  pwsh -File .\refresh-arm-linter-issue.ps1 -Apply
[CmdletBinding()]
param([switch]$Apply)

$ErrorActionPreference = 'Stop'
$repo = 'Azure/typespec-azure'
$issueNumber = 5274
$expectedRows = 180
$statusOrder = @{ 'Done' = 0; 'Hold on' = 1; 'Doing' = 2; 'To do' = 3; 'Need investigation' = 4 }
$pinnedPrs = @{
    'ConsistentResponseSchemaForPut' = 5423
    'PutRequestResponseSchemeArm' = 5423
    'XmsResourceInPutResponse' = 5423
    'PatchBodyParametersSchema' = 5294
    'ConsistentPatchProperties' = 5294
    'UnSupportedPatchProperties' = 5294
}

function Get-GitHubJson([string[]]$Arguments) {
    $raw = & gh @Arguments
    if ($LASTEXITCODE -ne 0) { throw "GitHub command failed: gh $($Arguments -join ' ')" }
    ($raw -join "`n") | ConvertFrom-Json
}

function Get-Api([string]$Path, [switch]$Paginate) {
    if ($Paginate) {
        $pages = Get-GitHubJson @('api', $Path, '--paginate', '--slurp')
        foreach ($page in $pages) { foreach ($item in $page) { $item } }
    } else {
        Get-GitHubJson @('api', $Path)
    }
}

function Test-EligibleUser($User, [string]$Author) {
    $User.type -eq 'User' -and $User.login -ne $Author -and $User.login -notmatch '\[bot\]$'
}

function Update-HeavyNotes([string]$Notes, [string]$PrBody) {
    if ($PrBody -notmatch '\bheavy\b' -or $Notes -match '\bheavy\b') { return $Notes }
    if ([string]::IsNullOrWhiteSpace($Notes) -or $Notes.Trim() -eq '-') { return 'heavy' }
    return "$Notes; heavy"
}

function Get-Rows([string]$Body) {
    foreach ($line in ($Body -split "`n")) {
        if ($line -notmatch '^\| \d+ \|') { continue }
        if ($line -notmatch '^\|(?:[^|\r\n]*\|){10}$') { throw 'Malformed table row.' }
        $cells = @($line.Split('|')[1..10] | ForEach-Object { $_.Trim() })
        if (-not $statusOrder.ContainsKey($cells[8])) { throw "Unknown status: $($cells[8])" }
        $rule = if ($cells[1] -match '^\[([^\]]+)\]') { $Matches[1] } else { $cells[1] }
        [pscustomobject]@{ OriginalId = [int]$cells[0]; Rule = $rule; Cells = $cells }
    }
}

function Assert-Table([object[]]$Rows) {
    if ($Rows.Count -ne $expectedRows -or @($Rows.Rule | Sort-Object -Unique).Count -ne $expectedRows) {
        throw "Expected exactly $expectedRows unique rules."
    }
    for ($i = 0; $i -lt $Rows.Count; $i++) {
        if ($Rows[$i].OriginalId -ne $i + 1) { throw 'IDs must be consecutive from 1.' }
    }
}

$issue = Get-Api "repos/$repo/issues/$issueNumber"
$originalBody = $issue.body
$rows = @(Get-Rows $originalBody)
Assert-Table $rows
$allPrs = @(Get-GitHubJson @(
    'pr', 'list', '--repo', $repo, '--state', 'all',
    '--search', '"[Swagger Linter Migration]" in:title',
    '--limit', '1000', '--json', 'number,title'
))
if ($allPrs.Count -ge 1000) { throw 'PR search may be truncated; narrow or paginate the search.' }
$candidates = @($allPrs | Where-Object { $_.title -notmatch '\(origin\)' })
$changes = [Collections.Generic.List[object]]::new()

foreach ($row in $rows) {
    if ($row.Cells[8] -eq 'Done') { continue }
    if ($row.Cells[8] -eq 'Hold on') {
        if ($row.Cells[3] -match 'https://github\.com/Azure/typespec-azure/pull/(\d+)') {
            $number = [int]$Matches[1]
            $pr = Get-Api "repos/$repo/pulls/$number"
            $before = $row.Cells[9]
            $row.Cells[9] = Update-HeavyNotes $before $pr.body
            if ($before -cne $row.Cells[9]) {
                $changes.Add([pscustomobject]@{ Rule = $row.Rule; Before = $before; After = $row.Cells[9] })
            }
        } elseif ($row.Cells[3] -notin @('', '-')) {
            throw "Unrecognized linked PR for held rule $($row.Rule)."
        }
        continue
    }
    # Match the original Swagger name in legacy titles and OldRule -> new-rule mappings.
    $pattern = '^\[Swagger Linter Migration\]\s+' + [regex]::Escape($row.Rule) + '(?=\s|->|$)'
    $isPinned = $pinnedPrs.ContainsKey($row.Rule)
    if ($isPinned) {
        $number = $pinnedPrs[$row.Rule]
    } else {
        $matching = @($candidates | Where-Object { $_.title -match $pattern })
        if ($matching.Count -gt 1) { throw "Multiple matching PRs for $($row.Rule); resolve manually." }
        if ($matching.Count -eq 0) {
            if ($row.Cells[3] -match '/pull/\d+') {
                throw "Existing PR for $($row.Rule) was not found in search; investigate before updating."
            }
            continue
        }
        $number = $matching[0].number
    }

    $pr = Get-Api "repos/$repo/pulls/$number"
    if (-not $isPinned -and ($pr.title -notmatch $pattern -or $pr.title -match '\(origin\)')) {
        throw "PR #$number title changed."
    }
    if (-not $pr.merged -and $pr.state -ne 'open') {
        throw "PR #$number is closed without merging; resolve its table status manually."
    }
    $files = @(Get-Api "repos/$repo/pulls/$number/files?per_page=100" -Paginate)
    if ($files.Count -ne $pr.changed_files) { throw "Incomplete changed-file list for PR #$number." }
    $libs = @(
        foreach ($package in @('typespec-azure-core', 'typespec-azure-resource-manager', 'typespec-client-generator-core')) {
            if ($files.filename -like "packages/$package/*") { "@azure-tools/$package" }
        }
    )
    if (-not $libs.Count) { throw "Unrecognized library in PR #$number; extend the package mapping." }

    $reviews = @(Get-Api "repos/$repo/pulls/$number/reviews?per_page=100" -Paginate)
    # A later change request or dismissal supersedes an approval; a comment does not.
    $approvals = @($reviews | Where-Object {
        (Test-EligibleUser $_.user $pr.user.login) -and
        $_.state -in @('APPROVED', 'CHANGES_REQUESTED', 'DISMISSED')
    } | Group-Object { $_.user.login } | ForEach-Object {
        $last = $_.Group | Sort-Object { [DateTimeOffset]$_.submitted_at }, id | Select-Object -Last 1
        if ($last.state -eq 'APPROVED') { $last.user.login }
    } | Sort-Object -Unique)

    $feedbackFlag = '-'
    if (-not $pr.merged) {
        $comments = @(Get-Api "repos/$repo/issues/$number/comments?per_page=100" -Paginate)
        $inline = @(Get-Api "repos/$repo/pulls/$number/comments?per_page=100" -Paginate)
        $head = Get-Api "repos/$repo/commits/$($pr.head.sha)"
        $latest = [DateTimeOffset]$head.commit.committer.date
        $feedback = @(
            @($comments) + @($inline) | Where-Object {
                (Test-EligibleUser $_.user $pr.user.login) -and
                [DateTimeOffset]$_.created_at -gt $latest
            }
            $reviews | Where-Object {
                (Test-EligibleUser $_.user $pr.user.login) -and
                -not [string]::IsNullOrWhiteSpace($_.body) -and
                $_.submitted_at -and [DateTimeOffset]$_.submitted_at -gt $latest
            }
        )
        $feedbackFlag = if ($feedback.Count) { 'Yes' } else { 'No' }
    }
    $fresh = Get-Api "repos/$repo/pulls/$number"
    if ($fresh.head.sha -ne $pr.head.sha -or $fresh.state -ne $pr.state -or
        $fresh.merged -ne $pr.merged -or $fresh.title -cne $pr.title -or $fresh.body -cne $pr.body) {
        throw "PR #$number changed while reading; rerun."
    }

    $before = $row.Cells[2..9] -join ' | '
    $row.Cells[2] = $libs -join ', '
    $row.Cells[3] = "[$number](https://github.com/$repo/pull/$number)"
    $row.Cells[4] = ([DateTimeOffset]$pr.created_at).ToString('yyyy-MM-dd')
    $row.Cells[5] = if ($pr.merged_at) { ([DateTimeOffset]$pr.merged_at).ToString('yyyy-MM-dd') } else { '-' }
    $row.Cells[6] = if ($approvals.Count) { $approvals -join ', ' } else { '-' }
    $row.Cells[7] = $feedbackFlag
    $row.Cells[9] = Update-HeavyNotes $row.Cells[9] $pr.body
    $row.Cells[8] = if ($pr.merged) { 'Done' } else { 'Doing' }
    $after = $row.Cells[2..9] -join ' | '
    if ($before -cne $after) {
        $changes.Add([pscustomobject]@{ Rule = $row.Rule; Before = $before; After = $after })
    }
}

$sorted = @($rows | Sort-Object { $statusOrder[$_.Cells[8]] }, OriginalId)
$index = 0
$newLines = foreach ($line in ($originalBody -split "`n")) {
    if ($line -match '^\| \d+ \|') {
        $sorted[$index].Cells[0] = ($index + 1).ToString()
        '| ' + ($sorted[$index].Cells -join ' | ') + ' |'
        $index++
    } else { $line }
}
$newBody = $newLines -join "`n"
$newRows = @(Get-Rows $newBody)
Assert-Table $newRows
foreach ($old in @(Get-Rows $originalBody)) {
    $new = $newRows | Where-Object Rule -eq $old.Rule
    if (-not $new) { throw "Lost rule: $($old.Rule)" }
    if ($old.Cells[8] -in @('Done', 'Hold on') -and ($old.Cells[1..8] -join '|') -cne ($new.Cells[1..8] -join '|')) {
        throw "Modified a protected Done/Hold on row: $($old.Rule)"
    }
    if ($old.Cells[8] -eq 'Done' -and $old.Cells[9] -cne $new.Cells[9]) {
        throw "Modified protected Done notes: $($old.Rule)"
    }
}
for ($i = 1; $i -lt $newRows.Count; $i++) {
    if ($statusOrder[$newRows[$i - 1].Cells[8]] -gt $statusOrder[$newRows[$i].Cells[8]]) {
        throw 'Incorrect status ordering.'
    }
}
$changes | Format-List
$newRows | Group-Object { $_.Cells[8] } | Select-Object Name, Count | Format-Table
if ($newBody -ceq $originalBody) { Write-Output 'Already up to date; no edit needed.'; return }
if (-not $Apply) { Write-Output 'Preview only. Rerun with -Apply to update the issue.'; return }

$current = Get-Api "repos/$repo/issues/$issueNumber"
if ($current.body -cne $originalBody) { throw 'Issue changed during refresh; rerun to avoid overwriting edits.' }
$tempFile = [IO.Path]::GetTempFileName()
try {
    [IO.File]::WriteAllText($tempFile, $newBody, [Text.UTF8Encoding]::new($false))
    & gh issue edit $issueNumber --repo $repo --body-file $tempFile
    if ($LASTEXITCODE -ne 0) { throw 'Issue update failed.' }
    $saved = Get-Api "repos/$repo/issues/$issueNumber"
    if ($saved.body -cne $newBody) { throw 'Saved issue differs from proposed body; inspect it before retrying.' }
    Assert-Table @(Get-Rows $saved.body)
    Write-Output "Updated https://github.com/$repo/issues/$issueNumber"
} finally {
    Remove-Item -LiteralPath $tempFile
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

lib:tcgcIssues for @azure-tools/typespec-client-generator-core librarylinterIssues related to linter rules

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions