From 71d4c0e2ae0ed14cd49eca16da4eede89d64791b Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 29 Jul 2026 09:09:49 +0900 Subject: [PATCH] Resolve reissued packages to the newest revision ruby/actions publishes a fixed package under a revisioned name such as ruby-3.4.5-1-x64-mswin64_140 (SIGNING.md). Substring resolution then reported "3.4.5" as ambiguous between the original and its reissue. When all matches share the version and platform and differ only in the numeric revision, pick the highest; everything else stays ambiguous. Co-Authored-By: Claude Fable 5 --- src/rbmanager/Program.cs | 32 ++++++++++++++++- tests/rbmanager.Tests/ResolveTests.cs | 50 +++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/rbmanager/Program.cs b/src/rbmanager/Program.cs index 33711f2..05bac5b 100644 --- a/src/rbmanager/Program.cs +++ b/src/rbmanager/Program.cs @@ -1,5 +1,6 @@ using System.IO.Compression; using System.Reflection; +using System.Text.RegularExpressions; namespace RbManager; @@ -208,11 +209,40 @@ internal static string Resolve(string query) { [var single] => single!, [] => throw new InvalidOperationException($"no installed ruby matches '{query}'"), - _ => throw new InvalidOperationException( + _ => HighestRevision(matches!) ?? throw new InvalidOperationException( $"'{query}' is ambiguous: {string.Join(", ", matches)}"), }; } + // ruby-[-]-: a reissued release package + // (SIGNING.md in ruby/actions) differs from the original only by the + // numeric revision after the version. The revision never collides + // with a prerelease segment, which starts with a letter (rc1, + // preview1), and dev snapshot names never parse here because their + // date/commit segments sit between the version and the platform. + private static readonly Regex PackageName = new( + @"^ruby-(?\d+\.\d+\.\d+(?:-[a-z][a-z0-9]*)?)(?:-(?\d+))?-(?(?:x64|x86|arm64)-.+)$", + RegexOptions.IgnoreCase); + + // When every match is the same ruby version on the same platform and + // they differ only in revision, the newest reissue wins; the + // superseded packages stay reachable by their full names. Anything + // else (different versions, dev snapshots, foreign names) stays + // ambiguous. + private static string? HighestRevision(string[] matches) + { + Match[] parsed = matches.Select(n => PackageName.Match(n)).ToArray(); + if (parsed.Any(m => !m.Success)) return null; + bool sameRuby = parsed + .DistinctBy(m => $"{m.Groups["ver"].Value}|{m.Groups["plat"].Value}", + StringComparer.OrdinalIgnoreCase) + .Count() == 1; + if (!sameRuby) return null; + return parsed + .MaxBy(m => m.Groups["rev"].Success ? int.Parse(m.Groups["rev"].Value) : 0)! + .Value; + } + internal static string? CurrentTarget() { var info = new DirectoryInfo(Current); diff --git a/tests/rbmanager.Tests/ResolveTests.cs b/tests/rbmanager.Tests/ResolveTests.cs index 2ff1fa0..cc490c8 100644 --- a/tests/rbmanager.Tests/ResolveTests.cs +++ b/tests/rbmanager.Tests/ResolveTests.cs @@ -74,4 +74,54 @@ public void NoRubiesDirectory_NoMatch() var ex = Assert.Throws(() => Program.Resolve("anything")); Assert.Equal("no installed ruby matches 'anything'", ex.Message); } + + // Reissued packages (ruby/actions SIGNING.md): same version and + // platform differing only in the trailing numeric revision resolve + // to the newest reissue instead of erroring as ambiguous. + + private const string R345 = "ruby-3.4.5-x64-mswin64_140"; + private const string R345r1 = "ruby-3.4.5-1-x64-mswin64_140"; + private const string R345r2 = "ruby-3.4.5-2-x64-mswin64_140"; + + [Fact] + public void RevisionsOfSameVersion_PickHighest() + { + using var sb = new RbSandbox(); + sb.Seed(R345, R345r2, R345r1); + Assert.Equal(R345r2, Program.Resolve("3.4.5")); + } + + [Fact] // revisions compare numerically, not lexicographically + public void RevisionsCompareNumerically() + { + using var sb = new RbSandbox(); + sb.Seed(R345r2, "ruby-3.4.5-10-x64-mswin64_140"); + Assert.Equal("ruby-3.4.5-10-x64-mswin64_140", Program.Resolve("3.4.5")); + } + + [Fact] // the superseded original stays reachable by its full name + public void SupersededOriginal_FullNameStillResolves() + { + using var sb = new RbSandbox(); + sb.Seed(R345, R345r1); + Assert.Equal(R345, Program.Resolve(R345)); + } + + [Fact] // revision preference never crosses version boundaries + public void RevisionPreference_DifferentVersionsStayAmbiguous() + { + using var sb = new RbSandbox(); + sb.Seed(R345, R345r1, "ruby-3.4.51-x64-mswin64_140"); // also contains "3.4.5" + var ex = Assert.Throws(() => Program.Resolve("3.4.5")); + Assert.Contains("is ambiguous", ex.Message); + } + + [Fact] // a prerelease is a different ruby, not a revision of the release + public void RevisionPreference_PrereleaseStaysAmbiguous() + { + using var sb = new RbSandbox(); + sb.Seed("ruby-3.4.0-x64-mswin64_140", "ruby-3.4.0-rc1-x64-mswin64_140"); + var ex = Assert.Throws(() => Program.Resolve("3.4.0")); + Assert.Contains("is ambiguous", ex.Message); + } }