From feec694940f5137e7ef116396d826f838b332694 Mon Sep 17 00:00:00 2001 From: i343759 Date: Thu, 1 Oct 2026 12:30:20 +0300 Subject: [PATCH] Fix bundler version parsing for RubyGems/Bundler 4.x output format Fixes a hard staging failure exposed by #1171 (automated rubygems 3.4.19 -> 4.0.22 bump): **ERROR** Unable to add bundler path to default environment: failed to determine bundler version from output: 4.0.22 (2026-09-30 commit ff2bd50) Failed to run all supply scripts: exit status 15 GetBundlerVersion() parsed `bundle version`'s output with a regex requiring a literal "Bundler version " prefix (e.g. "Bundler version 2.7.2 (2025-09-09 commit b463ced1459)"). As of RubyGems/Bundler 4.x, that prefix has been dropped - the command now just prints the bare version number instead (e.g. "4.0.22 (2026-09-30 commit ff2bd50)"), causing the regex to find zero matches and the whole supply script to abort. Extracted the parsing logic into a small, directly unit-testable pure function (parseBundlerVersion) and made the "Bundler version " prefix optional in the regex, so both the legacy and the new output formats parse correctly. --- src/ruby/versions/ruby.go | 25 ++++++++-- src/ruby/versions/ruby_internal_test.go | 62 +++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 src/ruby/versions/ruby_internal_test.go diff --git a/src/ruby/versions/ruby.go b/src/ruby/versions/ruby.go index 12397ccb3..700099965 100644 --- a/src/ruby/versions/ruby.go +++ b/src/ruby/versions/ruby.go @@ -58,11 +58,24 @@ func (v *Versions) GetBundlerVersion() (string, error) { return "", err } - re := regexp.MustCompile(`Bundler version (\d+\.\d+\.\d+) .*`) - match := re.FindStringSubmatch(stdout.String()) + return parseBundlerVersion(stdout.String()) +} + +// parseBundlerVersion extracts the bundler version number from the output of +// `bundle version`. +// +// Historically this command printed output prefixed with "Bundler version " +// (e.g. "Bundler version 2.7.2 (2025-09-09 commit b463ced1459)"), but as of +// RubyGems/Bundler 4.x the prefix has been dropped and the command just +// prints the bare version number instead (e.g. +// "4.0.22 (2026-09-30 commit ff2bd50)"). This parses both formats by making +// the "Bundler version " prefix optional. +func parseBundlerVersion(output string) (string, error) { + re := regexp.MustCompile(`(?:Bundler version )?(\d+\.\d+\.\d+)`) + match := re.FindStringSubmatch(output) if len(match) != 2 { - return "", fmt.Errorf("failed to determine bundler version from output: %s", stdout) + return "", fmt.Errorf("failed to determine bundler version from output: %s", output) } return match[1], nil @@ -191,9 +204,11 @@ func (v *Versions) GemMajorVersion(gem string) (int, error) { } } -//Should return true if either: +// Should return true if either: // (1) the only platform in the Gemfile.lock is windows (mingw/mswin) -// -or- +// +// -or- +// // (2) the Gemfile.lock line endings are /r/n, rather than just /n func (v *Versions) HasWindowsGemfileLock() (bool, error) { gemfileLockPath := v.Gemfile() + ".lock" diff --git a/src/ruby/versions/ruby_internal_test.go b/src/ruby/versions/ruby_internal_test.go new file mode 100644 index 000000000..37faee094 --- /dev/null +++ b/src/ruby/versions/ruby_internal_test.go @@ -0,0 +1,62 @@ +package versions + +import "testing" + +// TestParseBundlerVersion is a regression test for a real CI failure caused +// by a rubygems dependency bump (3.4.19 -> 4.0.22): `bundle version`'s +// output format changed, dropping the "Bundler version " prefix, which broke +// the previous regex entirely (zero matches, hard failure during staging). +func TestParseBundlerVersion(t *testing.T) { + cases := []struct { + name string + output string + want string + wantErr bool + }{ + { + name: "legacy format with 'Bundler version ' prefix", + output: "Bundler version 2.7.2 (2025-09-09 commit b463ced1459)\n", + want: "2.7.2", + }, + { + name: "new format without prefix (RubyGems/Bundler 4.x)", + output: "4.0.22 (2026-09-30 commit ff2bd50)\n", + want: "4.0.22", + }, + { + name: "bare version number with no trailing metadata", + output: "1.17.2\n", + want: "1.17.2", + }, + { + name: "unrecognized output", + output: "error: something went wrong\n", + wantErr: true, + }, + { + name: "empty output", + output: "", + wantErr: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got, err := parseBundlerVersion(tc.output) + + if tc.wantErr { + if err == nil { + t.Fatalf("expected an error, got version %q", got) + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tc.want { + t.Fatalf("got %q, want %q", got, tc.want) + } + }) + } +}