Add tool version lookup for correlating commits to daily builds#5753
Add tool version lookup for correlating commits to daily builds#5753mdh1418 wants to merge 4 commits intodotnet:mainfrom
Conversation
Introduce eng/tool-version-lookup.ps1 and eng/tool-version-lookup.sh with the foundational Arcade SDK version math. The decode command translates tool version patch numbers (e.g., 10.0.715501) into their build dates and OfficialBuildId using the formula from Version.BeforeCommonTargets.targets. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Query the dotnet-tools NuGet flat container API for available tool versions. Add the list command to display recent versions with their decoded build dates and OfficialBuildIds. Auto-detects the active major.minor series from the feed. Supports --tool, --major-minor, and --last options. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add commit SHA validation, git date/info resolution, and the before/after commands for issue triage. The before command finds the latest feed version before a regression commit; after finds the earliest version containing a fix. The decode command now resolves embedded commit SHAs via git log. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add the verify command to check if a specific version exists on the feed, with nearby version suggestions when not found. Add the tool-version-lookup.cmd Windows wrapper following the repo convention from installruntimes.cmd and privatebuild.cmd. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
We can keep this unmerged as well, in case we don't want this to flow into the VMR or some other compatibility issue. |
There was a problem hiding this comment.
Pull request overview
Adds a cross-platform helper script to correlate diagnostics tool daily-build versions with build dates and git commits, and to quickly locate versions before/after a given commit using the dotnet-tools NuGet feed.
Changes:
- Add a Bash implementation (
eng/tool-version-lookup.sh) supporting decode/before/after/verify/list against the dotnet-tools flat container API. - Add a PowerShell implementation (
eng/tool-version-lookup.ps1) with the same command surface and output guidance. - Add a Windows
.cmdwrapper (eng/tool-version-lookup.cmd) to invoke the PowerShell script following existing repo patterns.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| eng/tool-version-lookup.sh | Bash implementation of tool version decoding and NuGet feed queries for before/after/verify/list workflows. |
| eng/tool-version-lookup.ps1 | PowerShell implementation of the same functionality, including git commit resolution and user-facing output. |
| eng/tool-version-lookup.cmd | Thin Windows wrapper to run the PowerShell script from cmd.exe. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| local version="$1" | ||
| local versions | ||
| versions=$(get_feed_versions "$TOOL") | ||
|
|
||
| if echo "$versions" | grep -qxF "$version"; then | ||
| parse_version "$version" | ||
| echo "[OK] $TOOL $version exists on the feed" | ||
| echo " Built: $(format_build_date "$VER_PATCH")" | ||
| else | ||
| echo "[NOT FOUND] $TOOL $version NOT found on the feed" >&2 | ||
|
|
||
| if parse_version "$version"; then |
There was a problem hiding this comment.
verify checks for an exact string match against feed versions. If the user passes an informational version that includes +<sha>, the feed lookup will fail even though the underlying package version exists (since the feed versions don’t include build metadata). Consider stripping the +... suffix before the grep -qxF check, while still printing the original input for clarity.
| local version="$1" | |
| local versions | |
| versions=$(get_feed_versions "$TOOL") | |
| if echo "$versions" | grep -qxF "$version"; then | |
| parse_version "$version" | |
| echo "[OK] $TOOL $version exists on the feed" | |
| echo " Built: $(format_build_date "$VER_PATCH")" | |
| else | |
| echo "[NOT FOUND] $TOOL $version NOT found on the feed" >&2 | |
| if parse_version "$version"; then | |
| local input_version="$1" | |
| # Strip any SemVer build metadata (e.g., "+sha") for lookup and parsing, | |
| # since feed versions do not include build metadata. | |
| local version_no_meta="${input_version%%+*}" | |
| local versions | |
| versions=$(get_feed_versions "$TOOL") | |
| if echo "$versions" | grep -qxF "$version_no_meta"; then | |
| parse_version "$version_no_meta" | |
| echo "[OK] $TOOL $input_version exists on the feed" | |
| echo " Built: $(format_build_date "$VER_PATCH")" | |
| else | |
| echo "[NOT FOUND] $TOOL $input_version NOT found on the feed" >&2 | |
| if parse_version "$version_no_meta"; then |
| --tool) TOOL="$2"; validate_tool "$TOOL"; shift 2 ;; | ||
| --date) DATE_ARG="$2"; shift 2 ;; | ||
| --major-minor) MAJOR_MINOR="$2"; shift 2 ;; | ||
| --last) LAST_COUNT="$2"; shift 2 ;; |
There was a problem hiding this comment.
The option parser uses $2 for flags like --tool/--date/--major-minor/--last without checking that a value is present. With set -u, invoking the script with a trailing option (e.g., --tool as the last arg) will terminate with an unhelpful “unbound variable” error. Add a guard (e.g., [[ $# -ge 2 ]] || die ...) before reading $2 for each option.
| --tool) TOOL="$2"; validate_tool "$TOOL"; shift 2 ;; | |
| --date) DATE_ARG="$2"; shift 2 ;; | |
| --major-minor) MAJOR_MINOR="$2"; shift 2 ;; | |
| --last) LAST_COUNT="$2"; shift 2 ;; | |
| --tool) | |
| [[ $# -ge 2 ]] || die "--tool requires an argument" | |
| TOOL="$2" | |
| validate_tool "$TOOL" | |
| shift 2 | |
| ;; | |
| --date) | |
| [[ $# -ge 2 ]] || die "--date requires an argument in the form YYYY-MM-DD" | |
| DATE_ARG="$2" | |
| shift 2 | |
| ;; | |
| --major-minor) | |
| [[ $# -ge 2 ]] || die "--major-minor requires an argument in the form M.m" | |
| MAJOR_MINOR="$2" | |
| shift 2 | |
| ;; | |
| --last) | |
| [[ $# -ge 2 ]] || die "--last requires a numeric argument" | |
| LAST_COUNT="$2" | |
| shift 2 | |
| ;; |
| # 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 |
There was a problem hiding this comment.
Get-DetectedMajorMinor selects 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.
| # 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 | |
| # Auto-detects the active major.minor series by finding the newest semantic | |
| # version (major, minor, patch), 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) { | |
| $bestVersion = $null | |
| foreach ($v in $Versions) { | |
| $parsed = Parse-ToolVersion $v | |
| if (-not $parsed) { | |
| continue | |
| } | |
| if (-not $bestVersion -or | |
| $parsed.Major -gt $bestVersion.Major -or | |
| ($parsed.Major -eq $bestVersion.Major -and $parsed.Minor -gt $bestVersion.Minor) -or | |
| ($parsed.Major -eq $bestVersion.Major -and $parsed.Minor -eq $bestVersion.Minor -and $parsed.Patch -gt $bestVersion.Patch)) { | |
| $bestVersion = $parsed | |
| } | |
| } | |
| if ($bestVersion) { | |
| return "$($bestVersion.Major).$($bestVersion.Minor)" | |
| } | |
| return $null |
| $targetDate = [DateTimeOffset]::Parse($Date) | ||
| Write-Host "Finding $label $Tool version built $direction $Date..." |
There was a problem hiding this comment.
-Date is documented/printed as yyyy-MM-dd, but [DateTimeOffset]::Parse($Date) accepts many culture-dependent formats (e.g., 01/02/2024) which can produce unexpected results. Use ParseExact('yyyy-MM-dd', ...) (or validate with a regex first) and emit a clear error when the format is invalid.
| $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'))..." |
| $versions = Get-FeedVersions $Tool | ||
|
|
||
| if ($versions -contains $Ref) { | ||
| $parsed = Parse-ToolVersion $Ref | ||
| if ($parsed) { | ||
| Write-Host "[OK] $Tool $Ref exists on the feed" | ||
| Write-Host " Built: $(Format-BuildDate $parsed.Patch)" | ||
| } | ||
| else { | ||
| Write-Host "[OK] $Tool $Ref exists on the feed" | ||
| } | ||
| } | ||
| else { | ||
| Write-Host "[NOT FOUND] $Tool $Ref NOT found on the feed" -ForegroundColor Red |
There was a problem hiding this comment.
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.
| # 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). | ||
| detect_major_minor() { | ||
| local best_patch=-1 | ||
| local best_prefix="" | ||
| local v | ||
| while IFS= read -r v; do | ||
| if parse_version "$v"; then | ||
| if [ "$VER_PATCH" -gt "$best_patch" ]; then |
There was a problem hiding this comment.
detect_major_minor picks the major.minor series based only on the highest patch value across the entire feed. Because the patch encodes build date/revision (not major/minor), a different release branch that ran later (higher revision) can be selected even if it’s an older major/minor. Consider adding a deterministic tie-breaker (e.g., pick highest major/minor among the most recent build date) or sorting by (major, minor, patch) to avoid recommending versions from the wrong series by default.
| # 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). | |
| detect_major_minor() { | |
| local best_patch=-1 | |
| local best_prefix="" | |
| local v | |
| while IFS= read -r v; do | |
| if parse_version "$v"; then | |
| if [ "$VER_PATCH" -gt "$best_patch" ]; then | |
| # Auto-detects the active major.minor series by finding the highest version | |
| # in semantic order (major, minor, patch). This avoids preferring an older | |
| # major/minor series just because it has a later build (higher patch) value. | |
| detect_major_minor() { | |
| local best_major=-1 | |
| local best_minor=-1 | |
| local best_patch=-1 | |
| local best_prefix="" | |
| local v | |
| while IFS= read -r v; do | |
| if parse_version "$v"; then | |
| if [ "$VER_MAJOR" -gt "$best_major" ] || \ | |
| { [ "$VER_MAJOR" -eq "$best_major" ] && [ "$VER_MINOR" -gt "$best_minor" ]; } || \ | |
| { [ "$VER_MAJOR" -eq "$best_major" ] && [ "$VER_MINOR" -eq "$best_minor" ] && [ "$VER_PATCH" -gt "$best_patch" ]; }; then | |
| best_major=$VER_MAJOR | |
| best_minor=$VER_MINOR |
When users encounter problematic daily builds of our tools (e.g., #5752), it would be helpful to quickly provide them with either a version before the problematic commit to downgrade to, or a version after the fix commit to upgrade to. Today this requires manually decoding the Arcade SDK version encoding and searching the NuGet feed.
This PR adds
eng/tool-version-lookup.{ps1,sh,cmd}with five commands:decode <version>— Decode a tool version to its build date, OfficialBuildId, and commit SHAbefore <commit>— Find the latest feed version built before a commit (downgrade past a regression)after <commit>— Find the earliest feed version built after a commit (upgrade to include a fix)verify <version>— Check whether a version exists on the dotnet-tools feedlist— List recent daily build versions with decoded build datesExample: triaging #5752
Design notes
Version.BeforeCommonTargets.targets(PATCH = (YY*1000 + MM*50 + DD - 19000) * 100 + revision).ps1) and Bash (.sh) are independent native implementations;.cmdis a thin wrapper following repo convention