From 7128394ed4999be0533cd506c1fab4da6bbf6408 Mon Sep 17 00:00:00 2001 From: David Pine <7679720+IEvangelist@users.noreply.github.com> Date: Wed, 4 Nov 2026 06:02:36 -0600 Subject: [PATCH 1/2] Make API generation failures non-fatal and track them in one issue Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/update-integration-data.yml | 38 +++++++++++++++++++ .../scripts/update-integration-data.ps1 | 20 +++++++++- .../generate-package-json.ps1 | 17 +++++++-- 3 files changed, 69 insertions(+), 6 deletions(-) diff --git a/.github/workflows/update-integration-data.yml b/.github/workflows/update-integration-data.yml index e59bc9443..fad27f629 100644 --- a/.github/workflows/update-integration-data.yml +++ b/.github/workflows/update-integration-data.yml @@ -205,6 +205,44 @@ jobs: --body "The scheduled [Integration Data Updater]($RUN_URL) run failed. This issue is updated on each consecutive failure; close it once the workflow is green." fi + - name: Track API generation failures + if: ${{ always() && steps.update.outcome == 'success' && steps.update.outputs.regen_ran == 'true' }} + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + FAILURES: ${{ steps.update.outputs.api_failures }} + shell: bash + run: | + set -euo pipefail + + # One long-lived issue. Its body always lists the current failing + # packages; a comment is added only when that list changes, and the issue + # is closed once nothing fails, so the same failures never create noise. + title="Integration Data Updater: API generation failures" + existing=$(gh issue list \ + --state open \ + --search "\"$title\" in:title" \ + --json number,title \ + --jq "map(select(.title == \"$title\")) | .[0].number // empty") + + if [ -z "$FAILURES" ]; then + if [ -n "$existing" ]; then + gh issue close "$existing" --comment "No packages failed API generation in $RUN_URL." + fi + exit 0 + fi + + marker="" + body=$(printf '%s\n\nThese packages failed API generation in the latest [Integration Data Updater run](%s). Their previous API data is kept and the rest of the data still updates. This issue updates itself and closes when nothing fails.\n\n```\n%s\n```\n' "$marker" "$RUN_URL" "$FAILURES") + + if [ -z "$existing" ]; then + gh issue create --title "$title" --body "$body" + elif ! gh issue view "$existing" --json body --jq .body | grep -qF "$marker"; then + gh issue edit "$existing" --body "$body" + gh issue comment "$existing" --body "The set of failing packages changed: $RUN_URL" + fi + - name: Summary if: ${{ always() }} shell: bash diff --git a/src/frontend/scripts/update-integration-data.ps1 b/src/frontend/scripts/update-integration-data.ps1 index 3e08c5383..f9ee981d4 100644 --- a/src/frontend/scripts/update-integration-data.ps1 +++ b/src/frontend/scripts/update-integration-data.ps1 @@ -372,6 +372,8 @@ else { $regenRan = $false $pkgSummary = '' $pkgSkippedPackages = '' +$pkgFailedPackages = '' +$tsFailedPackages = '' $tsApiSummary = '' $tsSkippedPackages = '' $tsToleratedPackages = '' @@ -417,7 +419,9 @@ if ($versionsChanged -and -not $SkipRegen) { } $pkgSummary = "Success: $($Matches.Succeeded) | Failed: $($Matches.Failed) | Skipped: $($Matches.Skipped)" if ([int]$Matches.Failed -gt 0) { - Stop-ApiRegeneration "C# API generation reported $($Matches.Failed) package failure(s). Aborting; no PR will be opened." + $pkgFailedLine = ($pkgLog -split "`n" | Where-Object { $_ -match '^\s*Failed packages:' } | Select-Object -First 1) + $pkgFailedPackages = if ($pkgFailedLine) { ($pkgFailedLine -replace '^\s*Failed packages:\s*', '').Trim() } else { "$($Matches.Failed) unnamed package(s)" } + Write-Warning "C# API generation failed for: $pkgFailedPackages. Continuing; existing data for these packages is kept." } $pkgSkippedLine = ($pkgLog -split "`n" | Where-Object { $_ -match '^\s*Skipped packages:' } | @@ -476,7 +480,9 @@ if ($versionsChanged -and -not $SkipRegen) { } $tsApiSummary = "$($Matches.Succeeded) succeeded, $($Matches.Failed) failed, $($Matches.Skipped) skipped" if ([int]$Matches.Failed -gt 0) { - Stop-ApiRegeneration "TypeScript API generation reported $($Matches.Failed) package failure(s). Aborting; no PR will be opened." + $tsFailedLine = ($tsLog -split "`n" | Where-Object { $_ -match '^\s*Failed packages:' } | Select-Object -First 1) + $tsFailedPackages = if ($tsFailedLine) { ($tsFailedLine -replace '^\s*Failed packages:\s*', '').Trim() } else { "$($Matches.Failed) unnamed package(s)" } + Write-Warning "TypeScript API generation failed for: $tsFailedPackages. Continuing." } $tsSkippedLine = ($tsLog -split "`n" | Where-Object { $_ -match '^\s*Skipped packages:' } | @@ -662,6 +668,15 @@ if ($regenRan) { if ($tsSkippedPackages) { [void]$sb.AppendLine("- TypeScript packages skipped because they export no ATS functions: ``$tsSkippedPackages``") } + if ($pkgFailedPackages -or $tsFailedPackages) { + [void]$sb.AppendLine("") + [void]$sb.AppendLine("### :warning: API generation failures") + [void]$sb.AppendLine("") + [void]$sb.AppendLine("These packages failed to generate and kept their previous API data. They are tracked in the *Integration Data Updater: API generation failures* issue.") + [void]$sb.AppendLine("") + if ($pkgFailedPackages) { [void]$sb.AppendLine("- C#: ``$pkgFailedPackages``") } + if ($tsFailedPackages) { [void]$sb.AppendLine("- TypeScript: ``$tsFailedPackages``") } + } if ($tsToleratedPackages) { [void]$sb.AppendLine("") [void]$sb.AppendLine("### :warning: Tolerated ATS scanner diagnostics") @@ -723,6 +738,7 @@ Set-Output 'semantic_validation' $(if ($regenRan) { $semanticSummary } else { 'n Set-Output 'pr_title' $prTitle Set-Output 'pr_body_file' $prBodyFile Set-Output 'icon_warnings' $iconWarnings +Set-Output 'api_failures' (@(@($pkgFailedPackages, $tsFailedPackages) | Where-Object { $_ }) -join "`n") Write-Host "PR title : $prTitle" Write-Host "PR body : $prBodyFile" diff --git a/src/tools/PackageJsonGenerator/generate-package-json.ps1 b/src/tools/PackageJsonGenerator/generate-package-json.ps1 index a6d43352b..e0ea8f6bf 100644 --- a/src/tools/PackageJsonGenerator/generate-package-json.ps1 +++ b/src/tools/PackageJsonGenerator/generate-package-json.ps1 @@ -1229,8 +1229,17 @@ Write-Host "Output: $FinalOutputDir" Remove-Item (Join-Path $ScriptDir ".package-json-generator-work") -Recurse -Force -ErrorAction SilentlyContinue if ($failCount -gt 0) { - Remove-Item -Path $OutputDir -Recurse -Force -ErrorAction SilentlyContinue - exit 1 + Write-Warning "Skipping $failCount package(s) that failed to generate; their existing data is kept." +} + +# A failed package is non-fatal: keep its previously generated files so the rest +# of the catalog can still be refreshed. The caller reports the failures. +function Test-IsFailedPackageFile { + param([string]$FileName) + foreach ($failed in $failedPackageNames) { + if ($FileName -match ("^{0}\.\d.*\.json$" -f [regex]::Escape($failed))) { return $true } + } + return $false } New-Item -ItemType Directory -Path $FinalOutputDir -Force | Out-Null @@ -1243,7 +1252,7 @@ foreach ($file in $stagedFiles) { if ($isFullReconciliation) { foreach ($existingFile in Get-ChildItem -Path $FinalOutputDir -Filter "*.json" -File) { - if (-not $stagedNames.Contains($existingFile.Name)) { + if (-not $stagedNames.Contains($existingFile.Name) -and -not (Test-IsFailedPackageFile $existingFile.Name)) { Remove-Item -Path $existingFile.FullName -Force Write-Host "Removed stale package data: $($existingFile.Name)" -ForegroundColor DarkYellow } @@ -1253,7 +1262,7 @@ else { foreach ($packageId in $Packages) { $namePattern = "^{0}\.\d.*\.json$" -f [regex]::Escape($packageId) foreach ($existingFile in Get-ChildItem -Path $FinalOutputDir -Filter "*.json" -File) { - if ($existingFile.Name -match $namePattern -and -not $stagedNames.Contains($existingFile.Name)) { + if ($existingFile.Name -match $namePattern -and -not $stagedNames.Contains($existingFile.Name) -and -not (Test-IsFailedPackageFile $existingFile.Name)) { Remove-Item -Path $existingFile.FullName -Force Write-Host "Removed stale package data: $($existingFile.Name)" -ForegroundColor DarkYellow } From 3e6d4e5910fbc7f01d3b8bb427bb3c23c2ea334c Mon Sep 17 00:00:00 2001 From: David Pine <7679720+IEvangelist@users.noreply.github.com> Date: Wed, 4 Nov 2026 06:06:19 -0600 Subject: [PATCH 2/2] Record why each package failed API generation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../scripts/update-integration-data.ps1 | 16 ++++++-- .../generate-package-json.ps1 | 41 +++++++++++++++---- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/frontend/scripts/update-integration-data.ps1 b/src/frontend/scripts/update-integration-data.ps1 index f9ee981d4..b4500bbff 100644 --- a/src/frontend/scripts/update-integration-data.ps1 +++ b/src/frontend/scripts/update-integration-data.ps1 @@ -374,6 +374,7 @@ $pkgSummary = '' $pkgSkippedPackages = '' $pkgFailedPackages = '' $tsFailedPackages = '' +$apiFailureDetails = [System.Collections.Generic.List[string]]::new() $tsApiSummary = '' $tsSkippedPackages = '' $tsToleratedPackages = '' @@ -421,6 +422,11 @@ if ($versionsChanged -and -not $SkipRegen) { if ([int]$Matches.Failed -gt 0) { $pkgFailedLine = ($pkgLog -split "`n" | Where-Object { $_ -match '^\s*Failed packages:' } | Select-Object -First 1) $pkgFailedPackages = if ($pkgFailedLine) { ($pkgFailedLine -replace '^\s*Failed packages:\s*', '').Trim() } else { "$($Matches.Failed) unnamed package(s)" } + $pkgReasonLines = @($pkgLog -split "`r?`n" | Where-Object { $_ -match '^\s*FAILURE \S+: ' }) + foreach ($reasonLine in $pkgReasonLines) { + $apiFailureDetails.Add("C# " + ($reasonLine -replace '^\s*FAILURE ', '').Trim()) + } + if ($pkgReasonLines.Count -eq 0) { $apiFailureDetails.Add("C# ${pkgFailedPackages}: reason not captured; see the run log") } Write-Warning "C# API generation failed for: $pkgFailedPackages. Continuing; existing data for these packages is kept." } $pkgSkippedLine = ($pkgLog -split "`n" | @@ -482,6 +488,11 @@ if ($versionsChanged -and -not $SkipRegen) { if ([int]$Matches.Failed -gt 0) { $tsFailedLine = ($tsLog -split "`n" | Where-Object { $_ -match '^\s*Failed packages:' } | Select-Object -First 1) $tsFailedPackages = if ($tsFailedLine) { ($tsFailedLine -replace '^\s*Failed packages:\s*', '').Trim() } else { "$($Matches.Failed) unnamed package(s)" } + $tsReasonLines = @($tsLog -split "`r?`n" | Where-Object { $_ -match '^\s*(FAILED|FAILURE)\b' }) + foreach ($reasonLine in $tsReasonLines) { + $apiFailureDetails.Add("TypeScript " + ($reasonLine -replace '^\s*(FAILED|FAILURE)\s*', '').Trim()) + } + if ($tsReasonLines.Count -eq 0) { $apiFailureDetails.Add("TypeScript ${tsFailedPackages}: reason not captured; see the run log") } Write-Warning "TypeScript API generation failed for: $tsFailedPackages. Continuing." } $tsSkippedLine = ($tsLog -split "`n" | @@ -674,8 +685,7 @@ if ($regenRan) { [void]$sb.AppendLine("") [void]$sb.AppendLine("These packages failed to generate and kept their previous API data. They are tracked in the *Integration Data Updater: API generation failures* issue.") [void]$sb.AppendLine("") - if ($pkgFailedPackages) { [void]$sb.AppendLine("- C#: ``$pkgFailedPackages``") } - if ($tsFailedPackages) { [void]$sb.AppendLine("- TypeScript: ``$tsFailedPackages``") } + foreach ($detail in $apiFailureDetails) { [void]$sb.AppendLine("- ``$detail``") } } if ($tsToleratedPackages) { [void]$sb.AppendLine("") @@ -738,7 +748,7 @@ Set-Output 'semantic_validation' $(if ($regenRan) { $semanticSummary } else { 'n Set-Output 'pr_title' $prTitle Set-Output 'pr_body_file' $prBodyFile Set-Output 'icon_warnings' $iconWarnings -Set-Output 'api_failures' (@(@($pkgFailedPackages, $tsFailedPackages) | Where-Object { $_ }) -join "`n") +Set-Output 'api_failures' ($apiFailureDetails -join "`n") Write-Host "PR title : $prTitle" Write-Host "PR body : $prBodyFile" diff --git a/src/tools/PackageJsonGenerator/generate-package-json.ps1 b/src/tools/PackageJsonGenerator/generate-package-json.ps1 index e0ea8f6bf..7646e2af9 100644 --- a/src/tools/PackageJsonGenerator/generate-package-json.ps1 +++ b/src/tools/PackageJsonGenerator/generate-package-json.ps1 @@ -837,6 +837,22 @@ $skipCount = 0 $runtimeRefsByTfm = @{} $failedPackageNames = [System.Collections.Generic.HashSet[string]]::new( [System.StringComparer]::OrdinalIgnoreCase) +$failureReasons = @{} + +# Records why a package failed so the updater can surface an actionable reason +# (for example an NU1102 missing dependency) instead of only the package name. +function Add-PackageFailure { + param([string]$PackageId, [string]$Reason) + [void]$failedPackageNames.Add($PackageId) + $lines = @("$Reason" -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + $line = $lines | Where-Object { $_ -match '\berror\s+[A-Z]+\d+:' } | Select-Object -First 1 + if (-not $line) { $line = $lines | Select-Object -First 1 } + if (-not $line) { $line = 'unknown failure' } + $line = $line -replace '^.*?\berror\s+', 'error ' + $line = $line -replace '\s*\[[^\]]*\.csproj\]\s*$', '' + if ($line.Length -gt 300) { $line = $line.Substring(0, 300) + '...' } + if (-not $failureReasons.ContainsKey($PackageId)) { $failureReasons[$PackageId] = $line } +} $skippedPackageNames = [System.Collections.Generic.HashSet[string]]::new( [System.StringComparer]::OrdinalIgnoreCase) @@ -910,7 +926,7 @@ if ($packagesToResolve.Count -gt 3 -and -not $Sequential) { if (-not $resolved.Version) { Write-Warning "Could not resolve version for $($resolved.PackageId) from $($resolved.Source)." $failCount++ - [void]$failedPackageNames.Add($resolved.PackageId) + Add-PackageFailure $resolved.PackageId "could not resolve a version from $($resolved.Source)" continue } $packageInfos += $resolved @@ -923,7 +939,7 @@ if ($packagesToResolve.Count -gt 3 -and -not $Sequential) { if (-not $version) { Write-Warning "Could not resolve version for $packageId from $($sourceInfo.DisplaySource)." $failCount++ - [void]$failedPackageNames.Add($packageId) + Add-PackageFailure $packageId "could not resolve a version from $($sourceInfo.DisplaySource)" continue } $packageInfos += [PSCustomObject]@{ PackageId = $packageId; Version = $version } @@ -1021,7 +1037,7 @@ foreach ($info in $packageInfos) { $restoreError = if ($null -eq $restoreOutcome) { "no restore result was produced." } else { $restoreOutcome.Error } Write-Warning "Failed to restore $packageId $version`: $restoreError" $failCount++ - [void]$failedPackageNames.Add($packageId) + Add-PackageFailure $packageId "restore $version`: $restoreError" continue } $restoreGraph = $restoreOutcome.Graph @@ -1088,6 +1104,8 @@ if (-not $Sequential -and $manifestEntries.Count -gt 0) { $failedPackages = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $skippedPackages = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + $batchFailureReasons = @{} + $batchOutput | ForEach-Object { if ($_ -match "^Generated:") { Write-Host " $_" -ForegroundColor Green @@ -1101,8 +1119,9 @@ if (-not $Sequential -and $manifestEntries.Count -gt 0) { Write-Host " $_" -ForegroundColor Yellow } elseif ($_ -match "^FAILED") { - if ($_ -match "^FAILED \[(.+)\]:") { + if ($_ -match "^FAILED \[(.+?)\]:\s*(.*)$") { [void]$failedPackages.Add($Matches[1]) + $batchFailureReasons[$Matches[1]] = $Matches[2] } Write-Host " $_" -ForegroundColor Red } @@ -1126,12 +1145,12 @@ if (-not $Sequential -and $manifestEntries.Count -gt 0) { if ($batchExitCode -ne 0) { $failCount++ - [void]$failedPackageNames.Add($entry.packageName) + Add-PackageFailure $entry.packageName "batch tool exited with code $batchExitCode" } elseif ($reportedStatusCount -ne 1) { Write-Warning "Batch tool reported $reportedStatusCount terminal statuses for $($entry.packageName); expected exactly one." $failCount++ - [void]$failedPackageNames.Add($entry.packageName) + Add-PackageFailure $entry.packageName "batch tool reported $reportedStatusCount terminal statuses; expected exactly one" } elseif ($succeededPackages.Contains($entry.packageName)) { if (Test-Path -LiteralPath $entry.output -PathType Leaf) { @@ -1141,7 +1160,7 @@ if (-not $Sequential -and $manifestEntries.Count -gt 0) { else { Write-Warning "Batch tool reported success for $($entry.packageName) without creating $($entry.output)." $failCount++ - [void]$failedPackageNames.Add($entry.packageName) + Add-PackageFailure $entry.packageName "batch tool reported success without creating the output file" } } elseif ($skippedPackages.Contains($entry.packageName)) { @@ -1150,7 +1169,7 @@ if (-not $Sequential -and $manifestEntries.Count -gt 0) { } else { $failCount++ - [void]$failedPackageNames.Add($entry.packageName) + Add-PackageFailure $entry.packageName "generation failed: $($batchFailureReasons[$entry.packageName])" } } @@ -1209,7 +1228,8 @@ else { else { Write-Warning "Tool exited with code $generationExitCode for $($entry.packageName)" $failCount++ - [void]$failedPackageNames.Add($entry.packageName) + $failedLine = $generationOutput | Where-Object { $_ -match '^FAILED ' } | Select-Object -First 1 + Add-PackageFailure $entry.packageName "generation failed (exit $generationExitCode): $failedLine" } } } @@ -1220,6 +1240,9 @@ Write-Host "`n══════════════════════ Write-Host "Done! Success: $successCount | Failed: $failCount | Skipped: $skipCount" -ForegroundColor Cyan if ($failedPackageNames.Count -gt 0) { Write-Host "Failed packages: $(($failedPackageNames | Sort-Object) -join ', ')" -ForegroundColor Red + foreach ($name in ($failedPackageNames | Sort-Object)) { + Write-Host "FAILURE $name`: $($failureReasons[$name])" -ForegroundColor Red + } } if ($skippedPackageNames.Count -gt 0) { Write-Host "Skipped packages: $(($skippedPackageNames | Sort-Object) -join ', ')" -ForegroundColor Yellow