Skip to content

Add tool version lookup for correlating commits to daily builds#5753

Open
mdh1418 wants to merge 4 commits intodotnet:mainfrom
mdh1418:dev/mihw/tool-version-lookup
Open

Add tool version lookup for correlating commits to daily builds#5753
mdh1418 wants to merge 4 commits intodotnet:mainfrom
mdh1418:dev/mihw/tool-version-lookup

Conversation

@mdh1418
Copy link
Member

@mdh1418 mdh1418 commented Mar 10, 2026

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 SHA
  • before <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 feed
  • list — List recent daily build versions with decoded build dates

Example: triaging #5752

# User reported version 10.0.715501+86150ac0... — what commit is that?
eng/tool-version-lookup.ps1 decode "10.0.715501+86150ac0275658c5efc6035269499a86dee68e54"

# Find a version before the problematic commit to downgrade to
eng/tool-version-lookup.ps1 before bda9ea7b

# Find the first version after the fix to upgrade to
eng/tool-version-lookup.ps1 after 18cf9d1

Design notes

  • Version math mirrors the Arcade SDK's Version.BeforeCommonTargets.targets (PATCH = (YY*1000 + MM*50 + DD - 19000) * 100 + revision)
  • Queries the dotnet-tools NuGet flat container API (read-only, no credentials)
  • Commit refs validated as hex-only to prevent argument injection
  • PowerShell (.ps1) and Bash (.sh) are independent native implementations; .cmd is a thin wrapper following repo convention

mdh1418 and others added 4 commits March 10, 2026 13:51
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>
@mdh1418
Copy link
Member Author

mdh1418 commented Mar 10, 2026

We can keep this unmerged as well, in case we don't want this to flow into the VMR or some other compatibility issue.
Just figured it would be helpful to provide daily build versions in case users hit something that's temporarily broken.

@mdh1418 mdh1418 added the DO NOT MERGE do not merge this PR label Mar 10, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 .cmd wrapper (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.

Comment on lines +288 to +299
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
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +359 to +362
--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 ;;
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
--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
;;

Copilot uses AI. Check for mistakes.
Comment on lines +128 to +141
# 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
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
# 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

Copilot uses AI. Check for mistakes.
Comment on lines +211 to +212
$targetDate = [DateTimeOffset]::Parse($Date)
Write-Host "Finding $label $Tool version built $direction $Date..."
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-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.

Suggested change
$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 uses AI. Check for mistakes.
Comment on lines +341 to +354
$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
Copy link

Copilot AI Mar 10, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +98 to +107
# 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
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
# 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

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

DO NOT MERGE do not merge this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants