Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/update-integration-data.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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="<!-- failures:$(printf '%s' "$FAILURES" | sha256sum | cut -d' ' -f1) -->"
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
Comment on lines +239 to +244

- name: Summary
if: ${{ always() }}
shell: bash
Expand Down
30 changes: 28 additions & 2 deletions src/frontend/scripts/update-integration-data.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,9 @@ else {
$regenRan = $false
$pkgSummary = ''
$pkgSkippedPackages = ''
$pkgFailedPackages = ''
$tsFailedPackages = ''
$apiFailureDetails = [System.Collections.Generic.List[string]]::new()
$tsApiSummary = ''
$tsSkippedPackages = ''
$tsToleratedPackages = ''
Expand Down Expand Up @@ -417,7 +420,14 @@ 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)" }
$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" |
Where-Object { $_ -match '^\s*Skipped packages:' } |
Expand Down Expand Up @@ -476,7 +486,14 @@ 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)" }
$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" |
Where-Object { $_ -match '^\s*Skipped packages:' } |
Expand Down Expand Up @@ -662,6 +679,14 @@ 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("")
foreach ($detail in $apiFailureDetails) { [void]$sb.AppendLine("- ``$detail``") }
}
if ($tsToleratedPackages) {
[void]$sb.AppendLine("")
[void]$sb.AppendLine("### :warning: Tolerated ATS scanner diagnostics")
Expand Down Expand Up @@ -723,6 +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' ($apiFailureDetails -join "`n")

Write-Host "PR title : $prTitle"
Write-Host "PR body : $prBodyFile"
Expand Down
58 changes: 45 additions & 13 deletions src/tools/PackageJsonGenerator/generate-package-json.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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 }
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand All @@ -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) {
Expand All @@ -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)) {
Expand All @@ -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])"
}
}

Expand Down Expand Up @@ -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"
}
}
}
Expand All @@ -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
Expand All @@ -1229,8 +1252,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."
Comment on lines 1231 to +1232
}

# 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
Expand All @@ -1243,7 +1275,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
}
Expand All @@ -1253,7 +1285,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
}
Expand Down
Loading