From 2b16e644dbb73e72a481d56fefa18bc0bca5ac35 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 10 Mar 2026 13:51:00 -0400 Subject: [PATCH 1/4] Add version decode/encode and decode command 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> --- eng/tool-version-lookup.ps1 | 108 ++++++++++++++++++++++++++++++++++++ eng/tool-version-lookup.sh | 88 +++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 eng/tool-version-lookup.ps1 create mode 100644 eng/tool-version-lookup.sh diff --git a/eng/tool-version-lookup.ps1 b/eng/tool-version-lookup.ps1 new file mode 100644 index 0000000000..7cbe86121d --- /dev/null +++ b/eng/tool-version-lookup.ps1 @@ -0,0 +1,108 @@ +<# +.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 shows the embedded commit SHA. +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory=$true, Position=0)] + [ValidateSet("decode")] + [string]$Command, + + [Parameter(Position=1)] + [string]$Ref +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +# 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 Invoke-Decode { + if (-not $Ref) { + Write-Error "Usage: tool-version-lookup.ps1 decode " + 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" + } +} + +switch ($Command) { + "decode" { Invoke-Decode } +} diff --git a/eng/tool-version-lookup.sh b/eng/tool-version-lookup.sh new file mode 100644 index 0000000000..fab873a2f7 --- /dev/null +++ b/eng/tool-version-lookup.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# Correlates diagnostics tool versions with git commits and build dates. +# +# Usage: +# eng/tool-version-lookup.sh decode 10.0.715501 + +set -euo pipefail + +# Arcade SDK epoch constant from Version.BeforeCommonTargets.targets. +# If Arcade changes this value, it must be updated here as well. +VERSION_BASE_SHORT_DATE=19000 + +die() { echo "Error: $*" >&2; exit 1; } + +# 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. +decode_patch() { + local patch=$1 + PATCH_REV=$((patch % 100)) + local short_date=$((patch / 100 + VERSION_BASE_SHORT_DATE)) + PATCH_YY=$((short_date / 1000)) + local remainder=$((short_date - PATCH_YY * 1000)) + PATCH_MM=$((remainder / 50)) + PATCH_DD=$((remainder - PATCH_MM * 50)) +} + +encode_patch() { + local yy=$1 mm=$2 dd=$3 rev=${4:-1} + local short_date=$((yy * 1000 + mm * 50 + dd)) + echo $(( (short_date - VERSION_BASE_SHORT_DATE) * 100 + rev )) +} + +format_build_date() { + decode_patch "$1" + printf "20%02d-%02d-%02d (rev %d)" "$PATCH_YY" "$PATCH_MM" "$PATCH_DD" "$PATCH_REV" +} + +# Strips the "+commitsha" metadata suffix and splits "Major.Minor.Patch". +parse_version() { + local ver="${1%%+*}" + VER_MAJOR="" VER_MINOR="" VER_PATCH="" + IFS='.' read -r VER_MAJOR VER_MINOR VER_PATCH <<< "$ver" + if [ -z "$VER_MAJOR" ] || [ -z "$VER_MINOR" ] || [ -z "$VER_PATCH" ]; then + return 1 + fi + # Validate they're integers + case "$VER_MAJOR$VER_MINOR$VER_PATCH" in + *[!0-9]*) return 1 ;; + esac + return 0 +} + +cmd_decode() { + local version="$1" + parse_version "$version" || die "Could not parse version '$version'." + + decode_patch "$VER_PATCH" + echo "Version: $version" + printf "Build date: 20%02d-%02d-%02d\n" "$PATCH_YY" "$PATCH_MM" "$PATCH_DD" + echo "Build revision: $PATCH_REV" + printf "OfficialBuildId: 20%02d%02d%02d.%d\n" "$PATCH_YY" "$PATCH_MM" "$PATCH_DD" "$PATCH_REV" + + if [[ "$version" == *"+"* ]]; then + local sha="${version#*+}" + echo "Commit SHA: $sha" + fi +} + +# --- Argument parsing --- +COMMAND="${1:-}" +REF_ARG="${2:-}" + +case "$COMMAND" in + decode) + [ -n "$REF_ARG" ] || die "Usage: tool-version-lookup.sh decode " + cmd_decode "$REF_ARG" + ;; + "") + die "No command specified. Use --help for usage." + ;; + *) + die "Unknown command '$COMMAND'. Use --help for usage." + ;; +esac From cbbe85c811990869a8e756ec61e1967c1e16f152 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 10 Mar 2026 13:52:54 -0400 Subject: [PATCH 2/4] Add NuGet feed querying and list command 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> --- eng/tool-version-lookup.ps1 | 72 ++++++++++++++++++++++- eng/tool-version-lookup.sh | 114 +++++++++++++++++++++++++++++++++++- 2 files changed, 182 insertions(+), 4 deletions(-) diff --git a/eng/tool-version-lookup.ps1 b/eng/tool-version-lookup.ps1 index 7cbe86121d..d43e478da3 100644 --- a/eng/tool-version-lookup.ps1 +++ b/eng/tool-version-lookup.ps1 @@ -18,21 +18,37 @@ eng\tool-version-lookup.ps1 decode "10.0.715501+86150ac0275658c5efc6035269499a86dee68e54" Decodes the version and shows the embedded commit SHA. + +.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")] + [ValidateSet("decode", "list")] [string]$Command, [Parameter(Position=1)] - [string]$Ref + [string]$Ref, + + [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 @@ -80,6 +96,34 @@ function Parse-ToolVersion([string]$Version) { } } +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 +} + function Invoke-Decode { if (-not $Ref) { Write-Error "Usage: tool-version-lookup.ps1 decode " @@ -103,6 +147,30 @@ function Invoke-Decode { } } +function Invoke-List { + $versions = Get-FeedVersions $Tool + $prefix = if ($MajorMinor) { $MajorMinor } else { Get-DetectedMajorMinor $versions } + + $filtered = $versions | Where-Object { $_.StartsWith("$prefix.") } + $filtered = $filtered | Sort-Object { (Parse-ToolVersion $_).Patch } + $show = $filtered | Select-Object -Last $Last + + Write-Host "Recent $Tool $prefix.x versions on dotnet-tools feed:" + Write-Host "" + Write-Host ("{0,-20} {1,-16} {2}" -f "Version", "Build Date", "OfficialBuildId") + Write-Host ("{0,-20} {1,-16} {2}" -f ("-" * 20), ("-" * 16), ("-" * 15)) + foreach ($v in $show) { + $parsed = Parse-ToolVersion $v + if ($parsed) { + $d = Decode-Patch $parsed.Patch + $dateStr = "20{0:D2}-{1:D2}-{2:D2}" -f $d.Year, $d.Month, $d.Day + $buildId = "20{0:D2}{1:D2}{2:D2}.{3}" -f $d.Year, $d.Month, $d.Day, $d.Revision + Write-Host ("{0,-20} {1,-16} {2}" -f $v, $dateStr, $buildId) + } + } +} + switch ($Command) { "decode" { Invoke-Decode } + "list" { Invoke-List } } diff --git a/eng/tool-version-lookup.sh b/eng/tool-version-lookup.sh index fab873a2f7..4ac90eaac0 100644 --- a/eng/tool-version-lookup.sh +++ b/eng/tool-version-lookup.sh @@ -3,15 +3,34 @@ # # Usage: # eng/tool-version-lookup.sh decode 10.0.715501 +# eng/tool-version-lookup.sh list [--tool dotnet-trace] [--last 10] set -euo pipefail +# NuGet V3 flat container API — the simplest endpoint for listing all versions of a package. +FEED_FLAT2_BASE="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. +FEED_URL="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. VERSION_BASE_SHORT_DATE=19000 +VALID_TOOLS="dotnet-trace dotnet-dump dotnet-counters dotnet-gcdump" + +# Defaults +TOOL="dotnet-trace" +MAJOR_MINOR="" +LAST_COUNT=10 die() { echo "Error: $*" >&2; exit 1; } +validate_tool() { + local tool="$1" + for valid in $VALID_TOOLS; do + if [ "$tool" = "$valid" ]; then return 0; fi + done + die "Invalid tool '$tool'. Valid options: $VALID_TOOLS" +} + # 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 @@ -54,6 +73,44 @@ parse_version() { return 0 } +get_feed_versions() { + local tool="$1" + local url="$FEED_FLAT2_BASE/$tool/index.json" + if command -v jq >/dev/null 2>&1; then + curl -sfL "$url" | jq -r '.versions[]' + else + # Fallback: the flat2 response is {"versions":["x.y.z",...]}, simple enough to parse. + curl -sfL "$url" | tr ',' '\n' | grep -oE '"[0-9]+\.[0-9]+\.[0-9]+"' | tr -d '"' + fi +} + +# 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 + best_patch=$VER_PATCH + best_prefix="$VER_MAJOR.$VER_MINOR" + fi + fi + done + echo "$best_prefix" +} + +resolve_major_minor() { + if [ -n "$MAJOR_MINOR" ]; then + echo "$MAJOR_MINOR" + return + fi + local versions="$1" + echo "$versions" | detect_major_minor +} + cmd_decode() { local version="$1" parse_version "$version" || die "Could not parse version '$version'." @@ -70,15 +127,68 @@ cmd_decode() { fi } +cmd_list() { + local versions + versions=$(get_feed_versions "$TOOL") + local prefix + prefix=$(resolve_major_minor "$versions") + + local filtered + filtered=$(echo "$versions" | grep "^${prefix}\." | while IFS= read -r v; do + if parse_version "$v"; then + echo "$VER_PATCH:$v" + fi + done | sort -t: -k1 -n | tail -"$LAST_COUNT") + + echo "Recent $TOOL $prefix.x versions on dotnet-tools feed:" + echo "" + printf "%-20s %-16s %s\n" "Version" "Build Date" "OfficialBuildId" + printf "%-20s %-16s %s\n" "--------------------" "----------------" "---------------" + + echo "$filtered" | while IFS=: read -r patch ver; do + decode_patch "$patch" + printf "%-20s 20%02d-%02d-%02d 20%02d%02d%02d.%d\n" \ + "$ver" "$PATCH_YY" "$PATCH_MM" "$PATCH_DD" \ + "$PATCH_YY" "$PATCH_MM" "$PATCH_DD" "$PATCH_REV" + done +} + # --- Argument parsing --- -COMMAND="${1:-}" -REF_ARG="${2:-}" +POSITIONAL=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --tool) TOOL="$2"; validate_tool "$TOOL"; shift 2 ;; + --major-minor) MAJOR_MINOR="$2"; shift 2 ;; + --last) LAST_COUNT="$2"; shift 2 ;; + --help|-h) + echo "Usage: tool-version-lookup.sh [ref] [options]" + echo "" + echo "Commands:" + echo " decode Decode a tool version to its build date" + echo " list List recent versions on the feed" + echo "" + echo "Options:" + echo " --tool Tool name (default: dotnet-trace)" + echo " --major-minor Filter to specific major.minor (auto-detected)" + echo " --last Number of versions to show in list (default: 10)" + exit 0 + ;; + *) POSITIONAL+=("$1"); shift ;; + esac +done + +COMMAND="${POSITIONAL[0]:-}" +REF_ARG="${POSITIONAL[1]:-}" case "$COMMAND" in decode) [ -n "$REF_ARG" ] || die "Usage: tool-version-lookup.sh decode " cmd_decode "$REF_ARG" ;; + list) + cmd_list + ;; "") die "No command specified. Use --help for usage." ;; From 7c06857968c6b5e994b8bf0889ccbecfb8da69e9 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 10 Mar 2026 13:55:43 -0400 Subject: [PATCH 3/4] Add git integration and before/after commands 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> --- eng/tool-version-lookup.ps1 | 164 ++++++++++++++++++++++++++++++++++- eng/tool-version-lookup.sh | 167 ++++++++++++++++++++++++++++++++++++ 2 files changed, 329 insertions(+), 2 deletions(-) diff --git a/eng/tool-version-lookup.ps1 b/eng/tool-version-lookup.ps1 index d43e478da3..5cefca6494 100644 --- a/eng/tool-version-lookup.ps1 +++ b/eng/tool-version-lookup.ps1 @@ -17,7 +17,17 @@ .EXAMPLE eng\tool-version-lookup.ps1 decode "10.0.715501+86150ac0275658c5efc6035269499a86dee68e54" - Decodes the version and shows the embedded commit SHA. + 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 list -Last 5 @@ -28,12 +38,14 @@ [CmdletBinding()] param( [Parameter(Mandatory=$true, Position=0)] - [ValidateSet("decode", "list")] + [ValidateSet("decode", "before", "after", "list")] [string]$Command, [Parameter(Position=1)] [string]$Ref, + [string]$Date, + [ValidateSet("dotnet-trace", "dotnet-dump", "dotnet-counters", "dotnet-gcdump")] [string]$Tool = "dotnet-trace", @@ -124,6 +136,43 @@ function Get-DetectedMajorMinor([string[]]$Versions) { 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 " @@ -144,6 +193,115 @@ function Invoke-Decode { 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..." + } + elseif ($Ref) { + $info = Get-CommitInfo $Ref + $targetDate = Get-CommitDate $Ref + Write-Host "Commit: $info" + Write-Host "Date: $($targetDate.ToString('yyyy-MM-dd HH:mm zzz'))" + Write-Host "" + Write-Host "Finding $label $Tool version built $direction $($targetDate.ToString('yyyy-MM-dd'))..." + } + else { + Write-Error "Usage: tool-version-lookup.ps1 $direction [-Date yyyy-MM-dd]" + exit 1 + } + + $targetYY = $targetDate.Year - 2000 + $targetMM = $targetDate.Month + $targetDD = $targetDate.Day + + # For "before": use revision 0 so any build from that day is excluded + # (the build may or may not include the commit depending on timing). + # For "after": use revision 99 so any build from that day is excluded. + if ($IsBefore) { + $targetPatch = Encode-Patch $targetYY $targetMM $targetDD -Revision 0 + } + else { + $targetPatch = Encode-Patch $targetYY $targetMM $targetDD -Revision 99 + } + + $versions = Get-FeedVersions $Tool + $mm = Resolve-MajorMinor $versions + + $candidates = @() + foreach ($v in $versions) { + if (-not $v.StartsWith("$mm.")) { continue } + $parsed = Parse-ToolVersion $v + if (-not $parsed) { continue } + if ($IsBefore -and $parsed.Patch -lt $targetPatch) { + $candidates += @{ Version = $v; Patch = $parsed.Patch } + } + elseif (-not $IsBefore -and $parsed.Patch -gt $targetPatch) { + $candidates += @{ Version = $v; Patch = $parsed.Patch } + } + } + + if ($candidates.Length -eq 0) { + Write-Host "" + if (-not $IsBefore) { + Write-Host "The fix may not have been published yet." -ForegroundColor Yellow + } + Write-Error "No $Tool $mm.x versions found $direction that date." + exit 1 + } + + # @() wrapper is required: Sort-Object unwraps single-element arrays in PowerShell. + $candidates = @($candidates | Sort-Object { $_.Patch }) + + if ($IsBefore) { + $recommended = $candidates[$candidates.Length - 1] + $othersLabel = "Other recent options:" + if ($candidates.Length -ge 2) { + $start = [math]::Max(0, $candidates.Length - 4) + $end = $candidates.Length - 2 + $others = @($candidates[$start..$end]) + } + else { + $others = @() + } + } + else { + $recommended = $candidates[0] + $othersLabel = "Other options (newer):" + if ($candidates.Length -ge 2) { + $end = [math]::Min(3, $candidates.Length - 1) + $others = @($candidates[1..$end]) + } + else { + $others = @() + } + } + + $feedUrl = $FeedBase + Write-Host "" + Write-Host "Recommended version: $($recommended.Version)" + Write-Host " Built: $(Format-BuildDate $recommended.Patch)" + Write-Host "" + Write-Host "Install with:" + Write-Host " dotnet tool update $Tool -g --version $($recommended.Version) ``" + Write-Host " --add-source $feedUrl" + + if ($others -and $others.Length -gt 0) { + Write-Host "" + Write-Host $othersLabel + foreach ($c in $others) { + if ($c) { + Write-Host (" {0,-20} built {1}" -f $c.Version, (Format-BuildDate $c.Patch)) + } + } } } @@ -172,5 +330,7 @@ function Invoke-List { switch ($Command) { "decode" { Invoke-Decode } + "before" { Invoke-BeforeOrAfter -IsBefore $true } + "after" { Invoke-BeforeOrAfter -IsBefore $false } "list" { Invoke-List } } diff --git a/eng/tool-version-lookup.sh b/eng/tool-version-lookup.sh index 4ac90eaac0..4a03f41c77 100644 --- a/eng/tool-version-lookup.sh +++ b/eng/tool-version-lookup.sh @@ -3,6 +3,8 @@ # # Usage: # eng/tool-version-lookup.sh decode 10.0.715501 +# eng/tool-version-lookup.sh before [--tool dotnet-trace] +# eng/tool-version-lookup.sh after [--tool dotnet-trace] # eng/tool-version-lookup.sh list [--tool dotnet-trace] [--last 10] set -euo pipefail @@ -31,6 +33,14 @@ validate_tool() { die "Invalid tool '$tool'. Valid options: $VALID_TOOLS" } +# Restrict to hex SHAs to prevent shell injection via git arguments. +validate_commit_ref() { + local ref="$1" + if ! printf '%s\n' "$ref" | grep -qE '^[a-fA-F0-9]{4,40}$'; then + die "Invalid commit ref: '$ref'. Expected a hex SHA." + fi +} + # 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 @@ -111,6 +121,29 @@ resolve_major_minor() { echo "$versions" | detect_major_minor } +get_commit_date_iso() { + validate_commit_ref "$1" + local result + result=$(git log -1 --format="%cI" "$1" 2>/dev/null) || die "Could not find commit $1" + echo "$result" +} + +get_commit_info() { + validate_commit_ref "$1" + git log -1 --format="%h %ai %s" "$1" 2>/dev/null || echo "(could not resolve $1)" +} + +# Extract YYYY MM DD from ISO date string +parse_iso_date() { + local iso="$1" + ISO_YEAR="${iso:0:4}" + ISO_MONTH="${iso:5:2}" + ISO_DAY="${iso:8:2}" + # Remove leading zeros for arithmetic + ISO_MONTH=$((10#$ISO_MONTH)) + ISO_DAY=$((10#$ISO_DAY)) +} + cmd_decode() { local version="$1" parse_version "$version" || die "Could not parse version '$version'." @@ -124,6 +157,129 @@ cmd_decode() { if [[ "$version" == *"+"* ]]; then local sha="${version#*+}" echo "Commit SHA: $sha" + local info + info=$(get_commit_info "$sha") + echo "Commit: $info" + fi +} + +cmd_before_or_after() { + local is_before="$1" + local ref="$2" + local direction label + + if [ "$is_before" = "true" ]; then + direction="before" + label="latest" + else + direction="after" + label="earliest" + fi + + local target_iso + if [ -n "$DATE_ARG" ]; then + if ! echo "$DATE_ARG" | grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2}$'; then + die "Invalid date format '$DATE_ARG'. Expected YYYY-MM-DD." + fi + target_iso="${DATE_ARG}T00:00:00+00:00" + echo "Finding $label $TOOL version built $direction $DATE_ARG..." + elif [ -n "$ref" ]; then + local info + info=$(get_commit_info "$ref") + target_iso=$(get_commit_date_iso "$ref") + echo "Commit: $info" + echo "Date: $target_iso" + echo "" + parse_iso_date "$target_iso" + printf "Finding %s %s version built %s %04d-%02d-%02d...\n" "$label" "$TOOL" "$direction" "$ISO_YEAR" "$ISO_MONTH" "$ISO_DAY" + else + die "Usage: tool-version-lookup.sh $direction [--date YYYY-MM-DD]" + fi + + parse_iso_date "$target_iso" + local target_yy=$((ISO_YEAR - 2000)) + + # For "before": use revision 0 so any build from that day is excluded + # (the build may or may not include the commit depending on timing). + # For "after": use revision 99 so any build from that day is excluded. + local target_patch + if [ "$is_before" = "true" ]; then + target_patch=$(encode_patch "$target_yy" "$ISO_MONTH" "$ISO_DAY" 0) + else + target_patch=$(encode_patch "$target_yy" "$ISO_MONTH" "$ISO_DAY" 99) + fi + + local versions + versions=$(get_feed_versions "$TOOL") + local mm + mm=$(resolve_major_minor "$versions") + [ -n "$mm" ] || die "Could not determine major.minor version from feed." + + local candidates=() + local v + while IFS= read -r v; do + if [[ "$v" != "$mm."* ]]; then continue; fi + if parse_version "$v"; then + if [ "$is_before" = "true" ] && [ "$VER_PATCH" -lt "$target_patch" ]; then + candidates+=("$VER_PATCH:$v") + elif [ "$is_before" = "false" ] && [ "$VER_PATCH" -gt "$target_patch" ]; then + candidates+=("$VER_PATCH:$v") + fi + fi + done <<< "$versions" + + if [ ${#candidates[@]} -eq 0 ]; then + echo "" + if [ "$is_before" = "false" ]; then + echo "The fix may not have been published yet." >&2 + fi + die "No $TOOL $mm.x versions found $direction that date." + fi + + # Sort candidates by patch number + IFS=$'\n' sorted=($(printf '%s\n' "${candidates[@]}" | sort -t: -k1 -n)); unset IFS + + local recommended recommended_patch + if [ "$is_before" = "true" ]; then + recommended="${sorted[-1]}" + else + recommended="${sorted[0]}" + fi + recommended_patch="${recommended%%:*}" + recommended_ver="${recommended#*:}" + + echo "" + echo "Recommended version: $recommended_ver" + echo " Built: $(format_build_date "$recommended_patch")" + echo "" + echo "Install with:" + echo " dotnet tool update $TOOL -g --version $recommended_ver \\" + echo " --add-source $FEED_URL" + + if [ ${#sorted[@]} -ge 2 ]; then + echo "" + if [ "$is_before" = "true" ]; then + echo "Other recent options:" + local start=$(( ${#sorted[@]} - 4 )) + [ "$start" -lt 0 ] && start=0 + local end=$(( ${#sorted[@]} - 2 )) + for ((i=start; i<=end; i++)); do + local entry="${sorted[$i]}" + local p="${entry%%:*}" + local ver="${entry#*:}" + printf " %-20s built %s\n" "$ver" "$(format_build_date "$p")" + done + else + echo "Other options (newer):" + local end=$(( ${#sorted[@]} - 1 )) + [ "$end" -gt 3 ] && end=3 + for ((i=1; i<=end; i++)); do + local entry="${sorted[$i]}" + local p="${entry%%:*}" + local ver="${entry#*:}" + printf " %-20s built %s\n" "$ver" "$(format_build_date "$p")" + done + fi fi } @@ -154,11 +310,13 @@ cmd_list() { } # --- Argument parsing --- +DATE_ARG="" POSITIONAL=() while [[ $# -gt 0 ]]; do case "$1" in --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 ;; --help|-h) @@ -166,10 +324,13 @@ while [[ $# -gt 0 ]]; do echo "" echo "Commands:" echo " decode Decode a tool version to its build date" + echo " before Find latest feed version built before a commit/date" + echo " after Find earliest feed version built after a commit/date" echo " list List recent versions on the feed" echo "" echo "Options:" echo " --tool Tool name (default: dotnet-trace)" + echo " --date Use date instead of commit ref (before/after)" echo " --major-minor Filter to specific major.minor (auto-detected)" echo " --last Number of versions to show in list (default: 10)" exit 0 @@ -186,6 +347,12 @@ case "$COMMAND" in [ -n "$REF_ARG" ] || die "Usage: tool-version-lookup.sh decode " cmd_decode "$REF_ARG" ;; + before) + cmd_before_or_after "true" "$REF_ARG" + ;; + after) + cmd_before_or_after "false" "$REF_ARG" + ;; list) cmd_list ;; From 44474c6c72ccdea114721d1af330f88d01a76e85 Mon Sep 17 00:00:00 2001 From: Mitchell Hwang Date: Tue, 10 Mar 2026 13:57:56 -0400 Subject: [PATCH 4/4] Add verify command and .cmd wrapper 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> --- eng/tool-version-lookup.cmd | 3 +++ eng/tool-version-lookup.ps1 | 52 ++++++++++++++++++++++++++++++++++++- eng/tool-version-lookup.sh | 46 ++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 eng/tool-version-lookup.cmd diff --git a/eng/tool-version-lookup.cmd b/eng/tool-version-lookup.cmd new file mode 100644 index 0000000000..aa911e6f35 --- /dev/null +++ b/eng/tool-version-lookup.cmd @@ -0,0 +1,3 @@ +@echo off +powershell -ExecutionPolicy ByPass -NoProfile -command "& """%~dp0tool-version-lookup.ps1""" %*" +exit /b %ErrorLevel% diff --git a/eng/tool-version-lookup.ps1 b/eng/tool-version-lookup.ps1 index 5cefca6494..c9e8003f3c 100644 --- a/eng/tool-version-lookup.ps1 +++ b/eng/tool-version-lookup.ps1 @@ -29,6 +29,11 @@ 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 @@ -38,7 +43,7 @@ [CmdletBinding()] param( [Parameter(Mandatory=$true, Position=0)] - [ValidateSet("decode", "before", "after", "list")] + [ValidateSet("decode", "before", "after", "verify", "list")] [string]$Command, [Parameter(Position=1)] @@ -328,9 +333,54 @@ function Invoke-List { } } +function Invoke-Verify { + if (-not $Ref) { + Write-Error "Usage: tool-version-lookup.ps1 verify " + exit 1 + } + $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 + + $parsed = Parse-ToolVersion $Ref + if ($parsed) { + $nearby = @() + foreach ($v in $versions) { + $vp = Parse-ToolVersion $v + if ($vp -and $vp.Major -eq $parsed.Major -and $vp.Minor -eq $parsed.Minor) { + if ([math]::Abs($vp.Patch - $parsed.Patch) -lt 500) { + $nearby += $v + } + } + } + if ($nearby.Length -gt 0) { + Write-Host "" + Write-Host " Nearby versions:" + $nearby | Select-Object -Last 5 | ForEach-Object { + $vp = Parse-ToolVersion $_ + Write-Host (" {0,-20} built {1}" -f $_, (Format-BuildDate $vp.Patch)) + } + } + } + exit 1 + } +} + switch ($Command) { "decode" { Invoke-Decode } "before" { Invoke-BeforeOrAfter -IsBefore $true } "after" { Invoke-BeforeOrAfter -IsBefore $false } + "verify" { Invoke-Verify } "list" { Invoke-List } } diff --git a/eng/tool-version-lookup.sh b/eng/tool-version-lookup.sh index 4a03f41c77..38a103344a 100644 --- a/eng/tool-version-lookup.sh +++ b/eng/tool-version-lookup.sh @@ -5,6 +5,7 @@ # eng/tool-version-lookup.sh decode 10.0.715501 # eng/tool-version-lookup.sh before [--tool dotnet-trace] # eng/tool-version-lookup.sh after [--tool dotnet-trace] +# eng/tool-version-lookup.sh verify 10.0.711601 [--tool dotnet-trace] # eng/tool-version-lookup.sh list [--tool dotnet-trace] [--last 10] set -euo pipefail @@ -283,6 +284,46 @@ cmd_before_or_after() { fi } +cmd_verify() { + 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 target_major=$VER_MAJOR target_minor=$VER_MINOR target_patch=$VER_PATCH + local nearby=() + while IFS= read -r v; do + if parse_version "$v"; then + if [ "$VER_MAJOR" -eq "$target_major" ] && [ "$VER_MINOR" -eq "$target_minor" ]; then + local diff=$(( VER_PATCH - target_patch )) + [ "$diff" -lt 0 ] && diff=$(( -diff )) + if [ "$diff" -lt 500 ]; then + nearby+=("$v") + fi + fi + fi + done <<< "$versions" + + if [ ${#nearby[@]} -gt 0 ]; then + echo "" >&2 + echo " Nearby versions:" >&2 + printf '%s\n' "${nearby[@]}" | tail -5 | while IFS= read -r v; do + parse_version "$v" + printf " %-20s built %s\n" "$v" "$(format_build_date "$VER_PATCH")" >&2 + done + fi + fi + exit 1 + fi +} + cmd_list() { local versions versions=$(get_feed_versions "$TOOL") @@ -326,6 +367,7 @@ while [[ $# -gt 0 ]]; do echo " decode Decode a tool version to its build date" echo " before Find latest feed version built before a commit/date" echo " after Find earliest feed version built after a commit/date" + echo " verify Check if a version exists on the feed" echo " list List recent versions on the feed" echo "" echo "Options:" @@ -353,6 +395,10 @@ case "$COMMAND" in after) cmd_before_or_after "false" "$REF_ARG" ;; + verify) + [ -n "$REF_ARG" ] || die "Usage: tool-version-lookup.sh verify " + cmd_verify "$REF_ARG" + ;; list) cmd_list ;;