diff --git a/eng/docker-tools/skill-helpers/AzureDevOps.ps1 b/eng/docker-tools/skill-helpers/AzureDevOps.ps1 new file mode 100644 index 00000000..ad699067 --- /dev/null +++ b/eng/docker-tools/skill-helpers/AzureDevOps.ps1 @@ -0,0 +1,74 @@ +#!/usr/bin/env pwsh +# Lightweight wrapper for authenticated Azure DevOps REST API calls. +# Uses `az account get-access-token` for bearer token auth. +# +# Usage: +# . ./AzureDevOps.ps1 +# $response = Invoke-AzDORestMethod -Organization myorg -Project myproject ` +# -Endpoint "pipelines/42/runs" -Method POST -Body @{ resources = @{} } + +$ErrorActionPreference = "Stop" + +function Get-AzDOAccessToken { + <# + .SYNOPSIS + Returns a bearer token for Azure DevOps. + #> + + # Well-known Entra ID application ID for Azure DevOps + $tokenJson = az account get-access-token --resource "499b84ac-1321-427f-aa17-267ca6975798" 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "Failed to get access token. Run 'az login' first. Output: $tokenJson" + } + + $parsed = $tokenJson | ConvertFrom-Json + return $parsed.accessToken +} + +function Invoke-AzDORestMethod { + <# + .SYNOPSIS + Calls an Azure DevOps REST API endpoint with automatic authentication. + .PARAMETER Organization + Azure DevOps organization name (not the full URL). + .PARAMETER Project + Azure DevOps project name. + .PARAMETER Endpoint + API path after _apis/ (e.g. "pipelines/42/runs", "build/builds"). + .PARAMETER Method + HTTP method. Defaults to GET. + .PARAMETER Body + Request body as a hashtable. Automatically converted to JSON. + .PARAMETER ApiVersion + API version. Defaults to 7.1. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)][string] $Organization, + [Parameter(Mandatory)][string] $Project, + [Parameter(Mandatory)][string] $Endpoint, + [string] $Method = "GET", + [hashtable] $Body, + [string] $ApiVersion = "7.1" + ) + + $token = Get-AzDOAccessToken + $headers = @{ + Authorization = "Bearer $token" + "Content-Type" = "application/json" + } + + $uri = "https://dev.azure.com/$Organization/$Project/_apis/$($Endpoint)?api-version=$ApiVersion" + + $params = @{ + Uri = $uri + Headers = $headers + Method = $Method + } + + if ($Body) { + $params.Body = $Body | ConvertTo-Json -Depth 10 + } + + return Invoke-RestMethod @params +} diff --git a/eng/docker-tools/skill-helpers/Get-BuildLog.ps1 b/eng/docker-tools/skill-helpers/Get-BuildLog.ps1 new file mode 100644 index 00000000..1c430ad1 --- /dev/null +++ b/eng/docker-tools/skill-helpers/Get-BuildLog.ps1 @@ -0,0 +1,21 @@ +#!/usr/bin/env pwsh +# Retrieves a build log by log ID. +# Usage: +# ./Get-BuildLog.ps1 -Organization dnceng -Project internal -BuildId 12345 -LogId 47 + +[CmdletBinding()] +param( + [Parameter(Mandatory)][string] $Organization, + [Parameter(Mandatory)][string] $Project, + [Parameter(Mandatory)][int] $BuildId, + [Parameter(Mandatory)][int] $LogId +) + +$ErrorActionPreference = "Stop" + +. "$PSScriptRoot/AzureDevOps.ps1" + +Invoke-AzDORestMethod ` + -Organization $Organization ` + -Project $Project ` + -Endpoint "build/builds/$BuildId/logs/$LogId" diff --git a/eng/docker-tools/skill-helpers/Show-BuildTimeline.ps1 b/eng/docker-tools/skill-helpers/Show-BuildTimeline.ps1 new file mode 100644 index 00000000..a51b2287 --- /dev/null +++ b/eng/docker-tools/skill-helpers/Show-BuildTimeline.ps1 @@ -0,0 +1,77 @@ +#!/usr/bin/env pwsh +# Prints the build timeline as an indented tree with result indicators. +# Usage: +# ./Show-BuildTimeline.ps1 -Organization dnceng -Project internal -BuildId 12345 +# ./Show-BuildTimeline.ps1 -Organization dnceng -Project internal -BuildId 12345 -ShowAllTasks + +[CmdletBinding()] +param( + [Parameter(Mandatory)][string] $Organization, + [Parameter(Mandatory)][string] $Project, + [Parameter(Mandatory)][int] $BuildId, + [switch] $ShowAllTasks +) + +$ErrorActionPreference = "Stop" + +. "$PSScriptRoot/AzureDevOps.ps1" + +$build = Invoke-AzDORestMethod ` + -Organization $Organization ` + -Project $Project ` + -Endpoint "build/builds/$BuildId" + +Write-Host "# Build $BuildId - $($build.definition.name)" +Write-Host "" +Write-Host "- Status: $($build.status) $(if ($build.result) { "($($build.result))" })" +Write-Host "- Branch: $($build.sourceBranch)" +Write-Host "- Queued: $($build.queueTime)" +Write-Host "- URL: $($build._links.web.href)" +Write-Host "" + +$timeline = Invoke-AzDORestMethod ` + -Organization $Organization ` + -Project $Project ` + -Endpoint "build/builds/$BuildId/timeline" + +$records = $timeline.records + +# Build a lookup of children grouped by parentId +$childrenOf = @{} +foreach ($record in $records) { + $parentId = $record.parentId + if (-not $parentId) { $parentId = "" } + if (-not $childrenOf.ContainsKey($parentId)) { + $childrenOf[$parentId] = [System.Collections.Generic.List[object]]::new() + } + $childrenOf[$parentId].Add($record) +} + +# Sort children by order within each group +foreach ($key in @($childrenOf.Keys)) { + $childrenOf[$key] = $childrenOf[$key] | Sort-Object { $_.order } +} + +function Write-TimelineNode([string] $nodeId, [int] $depth) { + $children = $childrenOf[$nodeId] + if (-not $children) { return } + + foreach ($child in $children) { + $isTask = $child.type -eq "Task" + $isFailing = $child.result -in @("failed", "canceled", "abandoned") -or $child.state -eq "inProgress" + if ($isTask -and -not $ShowAllTasks -and -not $isFailing) { continue } + + $indent = " " * $depth + $status = if ($child.result) { $child.result } else { $child.state } + + $logId = $child.log.id + $logLabel = if ($logId) { " #$logId" } else { "" } + Write-Host "${indent}- $($child.type)$logLabel | $($child.name) | $status" + Write-TimelineNode $child.id ($depth + 1) + } +} + +Write-Host "## Build Timeline" +Write-Host "" +Write-TimelineNode "" 0 +Write-Host "" diff --git a/eng/docker-tools/templates/jobs/publish.yml b/eng/docker-tools/templates/jobs/publish.yml index 4b2aeacf..5839be2d 100644 --- a/eng/docker-tools/templates/jobs/publish.yml +++ b/eng/docker-tools/templates/jobs/publish.yml @@ -241,6 +241,9 @@ jobs: # # https://github.com/dotnet/docker-tools/issues/1698 tracks making this command no longer depend # on individual step displayNames. + # + # Skipped for PR builds because manifest lists are not created in PR builds (see post-build.yml). + # Without manifest lists present in image-info.json, the postPublishNotification fails with a NRE. - script: > $(runImageBuilderCmd) postPublishNotification '$(publishNotificationRepoName)' @@ -266,7 +269,7 @@ jobs: $(dryRunArg) $(imageBuilder.commonCmdArgs) displayName: Post Publish Notification - condition: and(always(), eq(variables['publishNotificationsEnabled'], 'true')) + condition: and(always(), eq(variables['publishNotificationsEnabled'], 'true'), ne(variables['Build.Reason'], 'PullRequest')) - powershell: | # Default to current build number if parameter was not overridden diff --git a/eng/docker-tools/templates/variables/docker-images.yml b/eng/docker-tools/templates/variables/docker-images.yml index a7f526d3..ae1089c7 100644 --- a/eng/docker-tools/templates/variables/docker-images.yml +++ b/eng/docker-tools/templates/variables/docker-images.yml @@ -1,5 +1,5 @@ variables: - imageNames.imageBuilderName: mcr.microsoft.com/dotnet-buildtools/image-builder:2936017 + imageNames.imageBuilderName: mcr.microsoft.com/dotnet-buildtools/image-builder:2940448 imageNames.imageBuilder: $(imageNames.imageBuilderName) imageNames.imageBuilder.withrepo: imagebuilder-withrepo:$(Build.BuildId)-$(System.JobId) imageNames.testRunner: mcr.microsoft.com/dotnet-buildtools/prereqs:azurelinux3.0-docker-testrunner