diff --git a/lib/gitlab/ci/runner_upgrade_check.rb b/lib/gitlab/ci/runner_upgrade_check.rb index d251ecf67dc82558aba4cc41e020fc628c887557..ad0fd73e9dab9d5bb6b8ec835e572f09f57ec266 100644 --- a/lib/gitlab/ci/runner_upgrade_check.rb +++ b/lib/gitlab/ci/runner_upgrade_check.rb @@ -13,7 +13,7 @@ class RunnerUpgradeCheck }.freeze def check_runner_upgrade_status(runner_version) - runner_version = ::Gitlab::VersionInfo.parse(runner_version) unless runner_version.is_a?(::Gitlab::VersionInfo) + runner_version = ::Gitlab::VersionInfo.parse(runner_version, parse_suffix: true) return :invalid unless runner_version.valid? diff --git a/lib/gitlab/version_info.rb b/lib/gitlab/version_info.rb index 88a5b735d8ca72c972329af60c0b3de229732a20..f967a12b9597b006b4452c9d8dfac4a719be0b1e 100644 --- a/lib/gitlab/version_info.rb +++ b/lib/gitlab/version_info.rb @@ -6,20 +6,27 @@ class VersionInfo attr_reader :major, :minor, :patch - def self.parse(str) - if str && m = str.match(/(\d+)\.(\d+)\.(\d+)/) - VersionInfo.new(m[1].to_i, m[2].to_i, m[3].to_i) + VERSION_REGEX = /(\d+)\.(\d+)\.(\d+)/.freeze + + def self.parse(str, parse_suffix: false) + if str.is_a?(self.class) + str + elsif str && m = str.match(VERSION_REGEX) + VersionInfo.new(m[1].to_i, m[2].to_i, m[3].to_i, parse_suffix ? m.post_match : nil) else VersionInfo.new end end - def initialize(major = 0, minor = 0, patch = 0) + def initialize(major = 0, minor = 0, patch = 0, suffix = nil) @major = major @minor = minor @patch = patch + @suffix_s = suffix.to_s end + # rubocop:disable Metrics/CyclomaticComplexity + # rubocop:disable Metrics/PerceivedComplexity def <=>(other) return unless other.is_a? VersionInfo return unless valid? && other.valid? @@ -36,19 +43,31 @@ def <=>(other) 1 elsif @patch < other.patch -1 + elsif @suffix_s.empty? && other.suffix.present? + 1 + elsif other.suffix.empty? && @suffix_s.present? + -1 else - 0 + suffix <=> other.suffix end end + # rubocop:enable Metrics/CyclomaticComplexity + # rubocop:enable Metrics/PerceivedComplexity def to_s if valid? - "%d.%d.%d" % [@major, @minor, @patch] + "%d.%d.%d%s" % [@major, @minor, @patch, @suffix_s] else - "Unknown" + 'Unknown' end end + def suffix + @suffix ||= @suffix_s.strip.gsub('-', '.pre.').scan(/\d+|[a-z]+/i).map do |s| + /^\d+$/ =~ s ? s.to_i : s + end.freeze + end + def valid? @major >= 0 && @minor >= 0 && @patch >= 0 && @major + @minor + @patch > 0 end diff --git a/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb b/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb index 3953a1aa81aca2631af3fd3d6942fb005b5b89f1..fd3218d7d845981bec4f37892e2b6df59e529f33 100644 --- a/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb +++ b/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb @@ -21,7 +21,9 @@ end context 'with available_runner_releases configured up to 14.1.1' do - let(:available_runner_releases) { %w[13.9.0 13.9.1 13.9.2 13.10.0 13.10.1 14.0.0 14.0.1 14.0.2 14.1.0 14.1.1] } + let(:available_runner_releases) do + %w[13.9.0 13.9.1 13.9.2 13.10.0 13.10.1 14.0.0 14.0.1 14.0.2-rc1 14.0.2 14.1.0 14.1.1] + end context 'with nil runner_version' do let(:runner_version) { nil } @@ -62,10 +64,11 @@ 'v14.1.0/1.1.0' | :recommended # suffixes are correctly handled 'v14.1.0' | :recommended # recommended since even though the GitLab instance is still on 14.0.x, there is a patch release (14.1.1) available which might contain security fixes 'v14.0.1' | :recommended # recommended upgrade since 14.0.2 is available + 'v14.0.2-rc1' | :recommended # recommended upgrade since 14.0.2 is available and we'll move out of a release candidate 'v14.0.2' | :not_available # not available since 14.0.2 is the latest 14.0.x release available within the instance's major.minor version 'v13.10.1' | :available # available upgrade: 14.1.1 - 'v13.10.1~beta.1574.gf6ea9389' | :available # suffixes are correctly handled - 'v13.10.1/1.1.0' | :available # suffixes are correctly handled + 'v13.10.1~beta.1574.gf6ea9389' | :recommended # suffixes are correctly handled, official 13.10.1 is available + 'v13.10.1/1.1.0' | :recommended # suffixes are correctly handled, official 13.10.1 is available 'v13.10.0' | :recommended # recommended upgrade since 13.10.1 is available 'v13.9.2' | :recommended # recommended upgrade since backports are no longer released for this version 'v13.9.0' | :recommended # recommended upgrade since backports are no longer released for this version diff --git a/spec/lib/gitlab/version_info_spec.rb b/spec/lib/gitlab/version_info_spec.rb index a77d51fab882b6b46da4fc245abf952cfbcb83d9..6ed094f11c8537daf5771e41c35c3226854df8a0 100644 --- a/spec/lib/gitlab/version_info_spec.rb +++ b/spec/lib/gitlab/version_info_spec.rb @@ -2,28 +2,44 @@ require 'fast_spec_helper' -RSpec.describe 'Gitlab::VersionInfo' do +RSpec.describe Gitlab::VersionInfo do before do - @unknown = Gitlab::VersionInfo.new - @v0_0_1 = Gitlab::VersionInfo.new(0, 0, 1) - @v0_1_0 = Gitlab::VersionInfo.new(0, 1, 0) - @v1_0_0 = Gitlab::VersionInfo.new(1, 0, 0) - @v1_0_1 = Gitlab::VersionInfo.new(1, 0, 1) - @v1_1_0 = Gitlab::VersionInfo.new(1, 1, 0) - @v2_0_0 = Gitlab::VersionInfo.new(2, 0, 0) + @unknown = described_class.new + @v0_0_1 = described_class.new(0, 0, 1) + @v0_1_0 = described_class.new(0, 1, 0) + @v1_0_0 = described_class.new(1, 0, 0) + @v1_0_1 = described_class.new(1, 0, 1) + @v1_0_1_b1 = described_class.new(1, 0, 1, '-b1') + @v1_0_1_rc1 = described_class.new(1, 0, 1, '-rc1') + @v1_0_1_rc2 = described_class.new(1, 0, 1, '-rc2') + @v1_1_0 = described_class.new(1, 1, 0) + @v1_1_0_beta1 = described_class.new(1, 1, 0, '-beta1') + @v2_0_0 = described_class.new(2, 0, 0) + @v13_10_1_1574_89 = described_class.parse("v13.10.1~beta.1574.gf6ea9389", parse_suffix: true) + @v13_10_1_1575_89 = described_class.parse("v13.10.1~beta.1575.gf6ea9389", parse_suffix: true) + @v13_10_1_1575_90 = described_class.parse("v13.10.1~beta.1575.gf6ea9390", parse_suffix: true) end describe '>' do it { expect(@v2_0_0).to be > @v1_1_0 } it { expect(@v1_1_0).to be > @v1_0_1 } + it { expect(@v1_0_1_b1).to be > @v1_0_0 } + it { expect(@v1_0_1_rc1).to be > @v1_0_0 } + it { expect(@v1_0_1_rc1).to be > @v1_0_1_b1 } + it { expect(@v1_0_1_rc2).to be > @v1_0_1_rc1 } + it { expect(@v1_0_1).to be > @v1_0_1_rc1 } + it { expect(@v1_0_1).to be > @v1_0_1_rc2 } it { expect(@v1_0_1).to be > @v1_0_0 } it { expect(@v1_0_0).to be > @v0_1_0 } + it { expect(@v1_1_0_beta1).to be > @v1_0_1_rc2 } + it { expect(@v1_1_0).to be > @v1_1_0_beta1 } it { expect(@v0_1_0).to be > @v0_0_1 } end describe '>=' do - it { expect(@v2_0_0).to be >= Gitlab::VersionInfo.new(2, 0, 0) } + it { expect(@v2_0_0).to be >= described_class.new(2, 0, 0) } it { expect(@v2_0_0).to be >= @v1_1_0 } + it { expect(@v1_0_1_rc2).to be >= @v1_0_1_rc1 } end describe '<' do @@ -31,64 +47,115 @@ it { expect(@v0_1_0).to be < @v1_0_0 } it { expect(@v1_0_0).to be < @v1_0_1 } it { expect(@v1_0_1).to be < @v1_1_0 } + it { expect(@v1_0_0).to be < @v1_0_1_rc2 } + it { expect(@v1_0_1_rc1).to be < @v1_0_1 } + it { expect(@v1_0_1_rc1).to be < @v1_0_1_rc2 } + it { expect(@v1_0_1_rc2).to be < @v1_0_1 } it { expect(@v1_1_0).to be < @v2_0_0 } + it { expect(@v13_10_1_1574_89).to be < @v13_10_1_1575_89 } + it { expect(@v13_10_1_1575_89).to be < @v13_10_1_1575_90 } end describe '<=' do - it { expect(@v0_0_1).to be <= Gitlab::VersionInfo.new(0, 0, 1) } + it { expect(@v0_0_1).to be <= described_class.new(0, 0, 1) } it { expect(@v0_0_1).to be <= @v0_1_0 } + it { expect(@v1_0_1_b1).to be <= @v1_0_1_rc1 } + it { expect(@v1_0_1_rc1).to be <= @v1_0_1_rc2 } + it { expect(@v1_1_0_beta1).to be <= @v1_1_0 } end describe '==' do - it { expect(@v0_0_1).to eq(Gitlab::VersionInfo.new(0, 0, 1)) } - it { expect(@v0_1_0).to eq(Gitlab::VersionInfo.new(0, 1, 0)) } - it { expect(@v1_0_0).to eq(Gitlab::VersionInfo.new(1, 0, 0)) } + it { expect(@v0_0_1).to eq(described_class.new(0, 0, 1)) } + it { expect(@v0_1_0).to eq(described_class.new(0, 1, 0)) } + it { expect(@v1_0_0).to eq(described_class.new(1, 0, 0)) } + it { expect(@v1_0_1_rc1).to eq(described_class.new(1, 0, 1, '-rc1')) } end describe '!=' do it { expect(@v0_0_1).not_to eq(@v0_1_0) } + it { expect(@v1_0_1_rc1).not_to eq(@v1_0_1_rc2) } end describe '.unknown' do it { expect(@unknown).not_to be @v0_0_1 } - it { expect(@unknown).not_to be Gitlab::VersionInfo.new } + it { expect(@unknown).not_to be described_class.new } it { expect {@unknown > @v0_0_1}.to raise_error(ArgumentError) } it { expect {@unknown < @v0_0_1}.to raise_error(ArgumentError) } end describe '.parse' do - it { expect(Gitlab::VersionInfo.parse("1.0.0")).to eq(@v1_0_0) } - it { expect(Gitlab::VersionInfo.parse("1.0.0.1")).to eq(@v1_0_0) } - it { expect(Gitlab::VersionInfo.parse("1.0.0-ee")).to eq(@v1_0_0) } - it { expect(Gitlab::VersionInfo.parse("1.0.0-rc1")).to eq(@v1_0_0) } - it { expect(Gitlab::VersionInfo.parse("1.0.0-rc1-ee")).to eq(@v1_0_0) } - it { expect(Gitlab::VersionInfo.parse("git 1.0.0b1")).to eq(@v1_0_0) } - it { expect(Gitlab::VersionInfo.parse("git 1.0b1")).not_to be_valid } + it { expect(described_class.parse("1.0.0")).to eq(@v1_0_0) } + it { expect(described_class.parse("1.0.0.1")).to eq(@v1_0_0) } + it { expect(described_class.parse("1.0.0-ee")).to eq(@v1_0_0) } + it { expect(described_class.parse("1.0.0-rc1")).to eq(@v1_0_0) } + it { expect(described_class.parse("1.0.0-rc1-ee")).to eq(@v1_0_0) } + it { expect(described_class.parse("git 1.0.0b1")).to eq(@v1_0_0) } + it { expect(described_class.parse("git 1.0b1")).not_to be_valid } + + context 'with parse_suffix: true' do + let(:versions) do + <<-VERSIONS.lines + 0.0.1 + 0.1.0 + 1.0.0 + 1.0.1-b1 + 1.0.1-rc1 + 1.0.1-rc2 + 1.0.1 + 1.1.0-beta1 + 1.1.0 + 2.0.0 + v13.10.0-pre + v13.10.0-rc1 + v13.10.0-rc2 + v13.10.0 + v13.10.1~beta.1574.gf6ea9389 + v13.10.1~beta.1575.gf6ea9389 + v13.10.1-rc1 + v13.10.1-rc2 + v13.10.1 + VERSIONS + end + + let(:parsed_versions) do + versions.map(&:strip).map { |version| described_class.parse(version, parse_suffix: true) } + end + + it 'versions are returned in a correct order' do + expect(parsed_versions.shuffle.sort).to eq(parsed_versions) + end + end end describe '.to_s' do it { expect(@v1_0_0.to_s).to eq("1.0.0") } + it { expect(@v1_0_1_rc1.to_s).to eq("1.0.1-rc1") } it { expect(@unknown.to_s).to eq("Unknown") } end describe '.hash' do - it { expect(Gitlab::VersionInfo.parse("1.0.0").hash).to eq(@v1_0_0.hash) } - it { expect(Gitlab::VersionInfo.parse("1.0.0.1").hash).to eq(@v1_0_0.hash) } - it { expect(Gitlab::VersionInfo.parse("1.0.1b1").hash).to eq(@v1_0_1.hash) } + it { expect(described_class.parse("1.0.0").hash).to eq(@v1_0_0.hash) } + it { expect(described_class.parse("1.0.0.1").hash).to eq(@v1_0_0.hash) } + it { expect(described_class.parse("1.0.1b1").hash).to eq(@v1_0_1.hash) } + it { expect(described_class.parse("1.0.1-rc1", parse_suffix: true).hash).to eq(@v1_0_1_rc1.hash) } end describe '.eql?' do - it { expect(Gitlab::VersionInfo.parse("1.0.0").eql?(@v1_0_0)).to be_truthy } - it { expect(Gitlab::VersionInfo.parse("1.0.0.1").eql?(@v1_0_0)).to be_truthy } + it { expect(described_class.parse("1.0.0").eql?(@v1_0_0)).to be_truthy } + it { expect(described_class.parse("1.0.0.1").eql?(@v1_0_0)).to be_truthy } + it { expect(@v1_0_1_rc1.eql?(@v1_0_1_rc1)).to be_truthy } + it { expect(@v1_0_1_rc1.eql?(@v1_0_1_rc2)).to be_falsey } + it { expect(@v1_0_1_rc1.eql?(@v1_0_1)).to be_falsey } it { expect(@v1_0_1.eql?(@v1_0_0)).to be_falsey } it { expect(@v1_1_0.eql?(@v1_0_0)).to be_falsey } it { expect(@v1_0_0.eql?(@v1_0_0)).to be_truthy } - it { expect([@v1_0_0, @v1_1_0, @v1_0_0].uniq).to eq [@v1_0_0, @v1_1_0] } + it { expect([@v1_0_0, @v1_1_0, @v1_0_0, @v1_0_1_rc1, @v1_0_1_rc1].uniq).to eq [@v1_0_0, @v1_1_0, @v1_0_1_rc1] } end describe '.same_minor_version?' do it { expect(@v0_1_0.same_minor_version?(@v0_0_1)).to be_falsey } it { expect(@v1_0_1.same_minor_version?(@v1_0_0)).to be_truthy } + it { expect(@v1_0_1_rc1.same_minor_version?(@v1_0_0)).to be_truthy } it { expect(@v1_0_0.same_minor_version?(@v1_0_1)).to be_truthy } it { expect(@v1_1_0.same_minor_version?(@v1_0_0)).to be_falsey } it { expect(@v2_0_0.same_minor_version?(@v1_0_0)).to be_falsey } @@ -98,5 +165,6 @@ it { expect(@v0_1_0.without_patch).to eq(@v0_1_0) } it { expect(@v1_0_0.without_patch).to eq(@v1_0_0) } it { expect(@v1_0_1.without_patch).to eq(@v1_0_0) } + it { expect(@v1_0_1_rc1.without_patch).to eq(@v1_0_0) } end end