-
Notifications
You must be signed in to change notification settings - Fork 387
Add tool version lookup for correlating commits to daily builds #5753
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| @echo off | ||
| powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0tool-version-lookup.ps1""" %*" | ||
| exit /b %ErrorLevel% |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,386 @@ | ||||||||||||||||||||||||||||||
| <# | ||||||||||||||||||||||||||||||
| .SYNOPSIS | ||||||||||||||||||||||||||||||
| Correlates diagnostics tool versions with git commits and build dates. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| .DESCRIPTION | ||||||||||||||||||||||||||||||
| Daily builds of dotnet-trace, dotnet-dump, dotnet-counters, and dotnet-gcdump | ||||||||||||||||||||||||||||||
| are published to the dotnet-tools NuGet feed with stable version numbers like | ||||||||||||||||||||||||||||||
| 10.0.715501. The patch component encodes the build date using the Arcade SDK | ||||||||||||||||||||||||||||||
| formula, and the informational version (shown by --version) includes the commit | ||||||||||||||||||||||||||||||
| SHA after '+'. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| .EXAMPLE | ||||||||||||||||||||||||||||||
| eng\tool-version-lookup.ps1 decode 10.0.715501 | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Decodes the version to show its build date and OfficialBuildId. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| .EXAMPLE | ||||||||||||||||||||||||||||||
| eng\tool-version-lookup.ps1 decode "10.0.715501+86150ac0275658c5efc6035269499a86dee68e54" | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Decodes the version and resolves the embedded commit SHA. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| .EXAMPLE | ||||||||||||||||||||||||||||||
| eng\tool-version-lookup.ps1 before bda9ea7b | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Finds the latest daily build version published before a given commit. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| .EXAMPLE | ||||||||||||||||||||||||||||||
| eng\tool-version-lookup.ps1 after 18cf9d1 -Tool dotnet-dump | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Finds the earliest daily build version published after a given commit. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| .EXAMPLE | ||||||||||||||||||||||||||||||
| eng\tool-version-lookup.ps1 verify 10.0.711601 | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Checks whether a specific version exists on the dotnet-tools feed. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| .EXAMPLE | ||||||||||||||||||||||||||||||
| eng\tool-version-lookup.ps1 list -Last 5 | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Lists the 5 most recent daily build versions on the feed. | ||||||||||||||||||||||||||||||
| #> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| [CmdletBinding()] | ||||||||||||||||||||||||||||||
| param( | ||||||||||||||||||||||||||||||
| [Parameter(Mandatory=$true, Position=0)] | ||||||||||||||||||||||||||||||
| [ValidateSet("decode", "before", "after", "verify", "list")] | ||||||||||||||||||||||||||||||
| [string]$Command, | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| [Parameter(Position=1)] | ||||||||||||||||||||||||||||||
| [string]$Ref, | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| [string]$Date, | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| [ValidateSet("dotnet-trace", "dotnet-dump", "dotnet-counters", "dotnet-gcdump")] | ||||||||||||||||||||||||||||||
| [string]$Tool = "dotnet-trace", | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| [string]$MajorMinor, | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| [int]$Last = 10 | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Set-StrictMode -Version Latest | ||||||||||||||||||||||||||||||
| $ErrorActionPreference = "Stop" | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # NuGet V3 flat container API — the simplest endpoint for listing all versions of a package. | ||||||||||||||||||||||||||||||
| $FeedFlat2Base = "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/flat2" | ||||||||||||||||||||||||||||||
| # Full feed URL used in the 'dotnet tool update --add-source' install command printed for users. | ||||||||||||||||||||||||||||||
| $FeedBase = "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json" | ||||||||||||||||||||||||||||||
| # Arcade SDK epoch constant from Version.BeforeCommonTargets.targets. | ||||||||||||||||||||||||||||||
| # If Arcade changes this value, it must be updated here as well. | ||||||||||||||||||||||||||||||
| $VersionBaseShortDate = 19000 | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # The Arcade SDK (Version.BeforeCommonTargets.targets) encodes the OfficialBuildId | ||||||||||||||||||||||||||||||
| # (format: yyyyMMdd.revision) into the patch component of the version number: | ||||||||||||||||||||||||||||||
| # SHORT_DATE = YY*1000 + MM*50 + DD | ||||||||||||||||||||||||||||||
| # PATCH = (SHORT_DATE - VersionBaseShortDate) * 100 + revision | ||||||||||||||||||||||||||||||
| # MM*50 is used instead of MM*100 because months max at 12 (12*50=600), leaving | ||||||||||||||||||||||||||||||
| # room for days 1-31 without overflow into the next month's range. | ||||||||||||||||||||||||||||||
| function Decode-Patch([int]$Patch) { | ||||||||||||||||||||||||||||||
| [int]$revision = $Patch % 100 | ||||||||||||||||||||||||||||||
| [int]$shortDate = [math]::Floor($Patch / 100) + $VersionBaseShortDate | ||||||||||||||||||||||||||||||
| [int]$yy = [math]::Floor($shortDate / 1000) | ||||||||||||||||||||||||||||||
| [int]$remainder = $shortDate - ($yy * 1000) | ||||||||||||||||||||||||||||||
| [int]$mm = [math]::Floor($remainder / 50) | ||||||||||||||||||||||||||||||
| [int]$dd = $remainder - ($mm * 50) | ||||||||||||||||||||||||||||||
| return @{ Year = $yy; Month = $mm; Day = $dd; Revision = $revision } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| function Encode-Patch([int]$Year, [int]$Month, [int]$Day, [int]$Revision = 1) { | ||||||||||||||||||||||||||||||
| [int]$shortDate = $Year * 1000 + $Month * 50 + $Day | ||||||||||||||||||||||||||||||
| return ($shortDate - $VersionBaseShortDate) * 100 + $Revision | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| function Format-BuildDate([int]$Patch) { | ||||||||||||||||||||||||||||||
| $d = Decode-Patch $Patch | ||||||||||||||||||||||||||||||
| return "20{0:D2}-{1:D2}-{2:D2} (rev {3})" -f $d.Year, $d.Month, $d.Day, $d.Revision | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Strips the "+commitsha" metadata suffix and splits "Major.Minor.Patch". | ||||||||||||||||||||||||||||||
| function Parse-ToolVersion([string]$Version) { | ||||||||||||||||||||||||||||||
| $clean = $Version.Split("+")[0] | ||||||||||||||||||||||||||||||
| $parts = $clean.Split(".") | ||||||||||||||||||||||||||||||
| if ($parts.Length -ne 3) { return $null } | ||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||
| return @{ | ||||||||||||||||||||||||||||||
| Major = [int]$parts[0] | ||||||||||||||||||||||||||||||
| Minor = [int]$parts[1] | ||||||||||||||||||||||||||||||
| Patch = [int]$parts[2] | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| catch { | ||||||||||||||||||||||||||||||
| return $null | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| function Get-FeedVersions([string]$ToolName) { | ||||||||||||||||||||||||||||||
| $url = "$FeedFlat2Base/$ToolName/index.json" | ||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||
| $response = Invoke-RestMethod -Uri $url | ||||||||||||||||||||||||||||||
| return $response.versions | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| catch { | ||||||||||||||||||||||||||||||
| Write-Error "Failed to query feed for ${ToolName}: $_" | ||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Auto-detects the active major.minor series by finding the version with the | ||||||||||||||||||||||||||||||
| # highest patch number (most recent build), since the feed contains versions | ||||||||||||||||||||||||||||||
| # from multiple release branches (e.g., 6.0.x, 9.0.x, 10.0.x). | ||||||||||||||||||||||||||||||
| function Get-DetectedMajorMinor([string[]]$Versions) { | ||||||||||||||||||||||||||||||
| $bestPatch = -1 | ||||||||||||||||||||||||||||||
| $bestPrefix = $null | ||||||||||||||||||||||||||||||
| foreach ($v in $Versions) { | ||||||||||||||||||||||||||||||
| $parsed = Parse-ToolVersion $v | ||||||||||||||||||||||||||||||
| if ($parsed -and $parsed.Patch -gt $bestPatch) { | ||||||||||||||||||||||||||||||
| $bestPatch = $parsed.Patch | ||||||||||||||||||||||||||||||
| $bestPrefix = "$($parsed.Major).$($parsed.Minor)" | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return $bestPrefix | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Restrict to hex SHAs to prevent shell injection via git arguments. | ||||||||||||||||||||||||||||||
| function Validate-CommitRef([string]$CommitRef) { | ||||||||||||||||||||||||||||||
| if ($CommitRef -notmatch '^[a-fA-F0-9]{4,40}$') { | ||||||||||||||||||||||||||||||
| Write-Error "Invalid commit ref: '$CommitRef'. Expected a hex SHA." | ||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| function Get-CommitDate([string]$CommitRef) { | ||||||||||||||||||||||||||||||
| Validate-CommitRef $CommitRef | ||||||||||||||||||||||||||||||
| $result = git log -1 --format="%cI" $CommitRef 2>&1 | ||||||||||||||||||||||||||||||
| if ($LASTEXITCODE -ne 0) { | ||||||||||||||||||||||||||||||
| Write-Error "Could not find commit $CommitRef" | ||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return [DateTimeOffset]::Parse($result.Trim()) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| function Get-CommitInfo([string]$CommitRef) { | ||||||||||||||||||||||||||||||
| Validate-CommitRef $CommitRef | ||||||||||||||||||||||||||||||
| $result = git log -1 --format="%h %ai %s" $CommitRef 2>&1 | ||||||||||||||||||||||||||||||
| if ($LASTEXITCODE -ne 0) { | ||||||||||||||||||||||||||||||
| return "(could not resolve $CommitRef)" | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return $result.Trim() | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| function Resolve-MajorMinor([string[]]$Versions) { | ||||||||||||||||||||||||||||||
| if ($MajorMinor) { return $MajorMinor } | ||||||||||||||||||||||||||||||
| $detected = Get-DetectedMajorMinor $Versions | ||||||||||||||||||||||||||||||
| if (-not $detected) { | ||||||||||||||||||||||||||||||
| Write-Error "Could not determine major.minor version from feed." | ||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return $detected | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| function Invoke-Decode { | ||||||||||||||||||||||||||||||
| if (-not $Ref) { | ||||||||||||||||||||||||||||||
| Write-Error "Usage: tool-version-lookup.ps1 decode <version>" | ||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| $parsed = Parse-ToolVersion $Ref | ||||||||||||||||||||||||||||||
| if (-not $parsed) { | ||||||||||||||||||||||||||||||
| Write-Error "Could not parse version '$Ref'." | ||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| $d = Decode-Patch $parsed.Patch | ||||||||||||||||||||||||||||||
| Write-Host "Version: $Ref" | ||||||||||||||||||||||||||||||
| Write-Host ("Build date: 20{0:D2}-{1:D2}-{2:D2}" -f $d.Year, $d.Month, $d.Day) | ||||||||||||||||||||||||||||||
| Write-Host "Build revision: $($d.Revision)" | ||||||||||||||||||||||||||||||
| Write-Host ("OfficialBuildId: 20{0:D2}{1:D2}{2:D2}.{3}" -f $d.Year, $d.Month, $d.Day, $d.Revision) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if ($Ref.Contains("+")) { | ||||||||||||||||||||||||||||||
| $sha = $Ref.Split("+")[1] | ||||||||||||||||||||||||||||||
| Write-Host "Commit SHA: $sha" | ||||||||||||||||||||||||||||||
| $info = Get-CommitInfo $sha | ||||||||||||||||||||||||||||||
| Write-Host "Commit: $info" | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| function Invoke-BeforeOrAfter([bool]$IsBefore) { | ||||||||||||||||||||||||||||||
| $direction = if ($IsBefore) { "before" } else { "after" } | ||||||||||||||||||||||||||||||
| $label = if ($IsBefore) { "latest" } else { "earliest" } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if ($Date) { | ||||||||||||||||||||||||||||||
| $targetDate = [DateTimeOffset]::Parse($Date) | ||||||||||||||||||||||||||||||
| Write-Host "Finding $label $Tool version built $direction $Date..." | ||||||||||||||||||||||||||||||
|
Comment on lines
+211
to
+212
|
||||||||||||||||||||||||||||||
| $targetDate = [DateTimeOffset]::Parse($Date) | |
| Write-Host "Finding $label $Tool version built $direction $Date..." | |
| try { | |
| $targetDate = [DateTimeOffset]::ParseExact( | |
| $Date, | |
| 'yyyy-MM-dd', | |
| [System.Globalization.CultureInfo]::InvariantCulture, | |
| [System.Globalization.DateTimeStyles]::None) | |
| } | |
| catch { | |
| Write-Error "Invalid date format '$Date'. Expected yyyy-MM-dd." | |
| exit 1 | |
| } | |
| Write-Host "Finding $label $Tool version built $direction $($targetDate.ToString('yyyy-MM-dd'))..." |
Copilot
AI
Mar 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
verify compares the user-provided version string to feed entries verbatim ($versions -contains $Ref). If a user pastes the tool’s informational version (e.g. 10.0.715501+<sha>), this will incorrectly report NOT FOUND even though 10.0.715501 exists. Consider normalizing $Ref by stripping any +... suffix (similar to Parse-ToolVersion) before checking the feed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Get-DetectedMajorMinorselects the default major.minor by looking only at the highest patch value across all versions. Since the patch encodes build date/revision (independent of major/minor), a different release branch that ran later in the day (higher revision) can win even if it’s an older major.minor. Consider breaking ties (or sorting) by (build date, major, minor) or by semantic version (major, minor, patch) so the auto-detected series is stable and picks the newest major/minor.