# HG changeset patch
# User Balasankar "Balu" C <balasankar@gitlab.com>
# Date 1684934486 -19800
#      Wed May 24 18:51:26 2023 +0530
# Node ID 65fe06b881d29ca5ec3a5d6754a8d067afee803e
# Parent  ea165a3cdc9db44649cce76db8b49e1d5f09fe5b
Move most of the latest tag detection logic to Ruby

Signed-off-by: Balasankar "Balu" C <balasankar@gitlab.com>

diff --git a/lib/gitlab/build/info.rb b/lib/gitlab/build/info.rb
--- a/lib/gitlab/build/info.rb
+++ b/lib/gitlab/build/info.rb
@@ -9,8 +9,6 @@
 
 module Build
   class Info
-    TagsNotFoundError = Class.new(StandardError)
-
     DEPLOYER_OS_MAPPING = {
       'AUTO_DEPLOY_ENVIRONMENT' => 'ubuntu-xenial',
       'PATCH_DEPLOY_ENVIRONMENT' => 'ubuntu-bionic',
@@ -72,6 +70,10 @@
         "#{semver}-#{Gitlab::BuildIteration.new.build_iteration}"
       end
 
+      def sorted_tags_for_edition
+        `git -c versionsort.prereleaseSuffix=rc tag -l '#{Info.tag_match_pattern}' --sort=-v:refname`.split("\n")
+      end
+
       # TODO, merge latest_tag with latest_stable_tag
       # TODO, add tests, needs a repo clone
       def latest_tag
@@ -79,9 +81,16 @@
           return fact_from_file
         end
 
-        version = branch_name.delete_suffix('-stable').tr('-', '.') if Build::Check.on_stable_branch?
+        tags = sorted_tags_for_edition
+
+        return if tags.empty?
 
-        `git -c versionsort.prereleaseSuffix=rc tag -l '#{version}#{Info.tag_match_pattern}' --sort=-v:refname | head -1`.chomp
+        version = branch_name.delete_suffix('-stable').tr('-', '.') if Build::Check.on_stable_branch?
+        output = tags.find { |t| t.start_with?(version) } if version
+
+        # If no tags corresponding to the stable branch version was found, we
+        # fall back to the latest available tag
+        output || tags.first
       end
 
       def latest_stable_tag(level: 1)
@@ -89,21 +98,33 @@
           return fact_from_file
         end
 
+        # Exclude RC tags so that we only have stable tags.
+        stable_tags = sorted_tags_for_edition.reject { |t| t.include?('rc') }
+
+        return if stable_tags.empty?
+
         version = branch_name.delete_suffix('-stable').tr('-', '.') if Build::Check.on_stable_branch?
 
+        results = stable_tags.select { |t| t.start_with?(version) } if version
+
+        # If no tags corresponding to the stable branch version was found, we
+        # fall back to the latest available stable tag
+        output = if results.nil? || results.empty?
+                   stable_tags
+                 else
+                   results
+                 end
+
         # Level decides tag at which position you want. Level one gives you
         # latest stable tag, two gives you the one just before it and so on.
-        output = `git -c versionsort.prereleaseSuffix=rc tag -l '#{version}#{Info.tag_match_pattern}' --sort=-v:refname | awk '!/rc/' | head -#{level}`&.split("\n")&.last
-
-        # If no tags exist that start with the specified version, we need to
-        # fall back to the available latest stable tag. For that, we run the
-        # same command without the version in the filter argument.
-        raise TagsNotFoundError if output.nil?
-
-        output
-      rescue TagsNotFoundError
-        puts "No tags found in #{version}.x series. Falling back to latest available tag."
-        `git -c versionsort.prereleaseSuffix=rc tag -l '#{Info.tag_match_pattern}' --sort=-v:refname | awk '!/rc/' | head -#{level}`.split("\n").last
+        # Since arrays start from 0, we subtract 1 from the specified level to
+        # get the index. If the specified level is more than the number of
+        # tags, we return the last tag.
+        if level >= output.length
+          output.last
+        else
+          output[level - 1]
+        end
       end
 
       def docker_tag
diff --git a/spec/lib/gitlab/build/info_spec.rb b/spec/lib/gitlab/build/info_spec.rb
--- a/spec/lib/gitlab/build/info_spec.rb
+++ b/spec/lib/gitlab/build/info_spec.rb
@@ -7,6 +7,11 @@
     stub_default_package_version
     stub_env_var('GITLAB_ALTERNATIVE_REPO', nil)
     stub_env_var('ALTERNATIVE_PRIVATE_TOKEN', nil)
+
+    ce_tags = "16.1.1+ce.0\n16.0.0+rc42.ce.0\n15.11.1+ce.0\n15.11.0+ce.0\n15.10.0+ce.0\n15.10.0+rc42.ce.0"
+    ee_tags = "16.1.1+ee.0\n16.0.0+rc42.ee.0\n15.11.1+ee.0\n15.11.0+ee.0\n15.10.0+ee.0\n15.10.0+rc42.ee.0"
+    allow(described_class).to receive(:`).with(/git -c versionsort.*ce/).and_return(ce_tags)
+    allow(described_class).to receive(:`).with(/git -c versionsort.*ee/).and_return(ee_tags)
   end
 
   describe '.package' do
@@ -86,115 +91,221 @@
     end
   end
 
-  # Specs for latest_tag and for latest_stable_tag are not really useful since
-  # we are stubbing out shell out to git. However, they are showing what we
-  # expect to see.
   describe '.latest_tag' do
-    context 'when running against stable branches' do
+    context 'on CE edition' do
       before do
         stub_is_ee(false)
-        stub_env_var('CI_COMMIT_BRANCH', '14-10-stable')
-        allow(described_class).to receive(:`).with(/git describe --exact-match/).and_return('12.121.12+rc7.ce.0')
-        allow(described_class).to receive(:`).with(/git -c versionsort.prereleaseSuffix=rc tag -l /).and_return('12.121.12+rc7.ce.0')
       end
 
-      it 'calls the shell command with correct arguments' do
-        expect(described_class).to receive(:`).with("git -c versionsort.prereleaseSuffix=rc tag -l '14.10*[+.]ce.*' --sort=-v:refname | head -1")
+      context 'on stable branch' do
+        context 'when tags already exist in the stable version series' do
+          before do
+            stub_env_var('CI_COMMIT_BRANCH', '15-10-stable')
+          end
+
+          it 'returns the latest tag in the stable version series' do
+            expect(described_class.latest_tag).to eq('15.10.0+ce.0')
+          end
+        end
+
+        context 'when tags does not exist in the stable version series' do
+          before do
+            stub_env_var('CI_COMMIT_BRANCH', '16-5-stable')
+          end
 
-        described_class.latest_tag
+          it 'returns the latest available tag' do
+            expect(described_class.latest_tag).to eq('16.1.1+ce.0')
+          end
+        end
+
+        context 'when latest tag in the series is an RC tag' do
+          before do
+            stub_env_var('CI_COMMIT_BRANCH', '16-0-stable')
+          end
+
+          it 'returns the RC tag' do
+            expect(described_class.latest_tag).to eq('16.0.0+rc42.ce.0')
+          end
+        end
+      end
+
+      context 'on feature branch' do
+        before do
+          stub_env_var('CI_COMMIT_BRANCH', 'my-feature-branch')
+        end
+
+        it 'returns the latest available tag' do
+          expect(described_class.latest_tag).to eq('16.1.1+ce.0')
+        end
       end
     end
 
-    describe 'for CE' do
+    context 'on EE edition' do
       before do
-        stub_env_var('CI_COMMIT_BRANCH', 'foo-feature-branch')
-        stub_is_ee(false)
-        allow(described_class).to receive(:`).with("git describe --exact-match 2>/dev/null").and_return('12.121.12+rc7.ce.0')
-        allow(described_class).to receive(:`).with("git -c versionsort.prereleaseSuffix=rc tag -l '*[+.]ce.*' --sort=-v:refname | head -1").and_return('12.121.12+rc7.ce.0')
-      end
-
-      it 'returns the version of correct edition' do
-        expect(described_class.latest_tag).to eq('12.121.12+rc7.ce.0')
+        stub_is_ee(true)
       end
 
-      it 'calls the shell command with correct arguments' do
-        expect(described_class).to receive(:`).with("git -c versionsort.prereleaseSuffix=rc tag -l '*[+.]ce.*' --sort=-v:refname | head -1")
+      context 'on stable branch' do
+        context 'when tags already exist in the stable version series' do
+          before do
+            stub_env_var('CI_COMMIT_BRANCH', '15-10-stable')
+          end
+
+          it 'returns the latest tag in the stable version series' do
+            expect(described_class.latest_tag).to eq('15.10.0+ee.0')
+          end
+        end
 
-        described_class.latest_tag
-      end
-    end
+        context 'when tags does not exist in the stable version series' do
+          before do
+            stub_env_var('CI_COMMIT_BRANCH', '16-5-stable')
+          end
+
+          it 'returns the latest available tag' do
+            expect(described_class.latest_tag).to eq('16.1.1+ee.0')
+          end
+        end
 
-    describe 'for EE' do
-      before do
-        stub_env_var('CI_COMMIT_BRANCH', 'foo-feature-branch')
-        stub_is_ee(true)
-        allow(described_class).to receive(:`).with("git -c versionsort.prereleaseSuffix=rc tag -l '*[+.]ee.*' --sort=-v:refname | head -1").and_return('12.121.12+rc7.ee.0')
+        context 'when latest tag in the series is an RC tag' do
+          before do
+            stub_env_var('CI_COMMIT_BRANCH', '16-0-stable')
+          end
+
+          it 'returns the RC tag' do
+            expect(described_class.latest_tag).to eq('16.0.0+rc42.ee.0')
+          end
+        end
       end
 
-      it 'returns the version of correct edition' do
-        expect(described_class.latest_tag).to eq('12.121.12+rc7.ee.0')
+      context 'on feature branch' do
+        before do
+          stub_env_var('CI_COMMIT_BRANCH', 'my-feature-branch')
+        end
+
+        it 'returns the latest available tag' do
+          expect(described_class.latest_tag).to eq('16.1.1+ee.0')
+        end
       end
     end
   end
 
   describe '.latest_stable_tag' do
-    describe 'for CE' do
+    context 'on CE edition' do
       before do
-        stub_env_var('CI_COMMIT_BRANCH', 'foo-feature-branch')
-        stub_is_ee(nil)
-        allow(described_class).to receive(:`).with("git describe --exact-match 2>/dev/null").and_return('12.121.12+ce.0')
-        allow(described_class).to receive(:`).with("git -c versionsort.prereleaseSuffix=rc tag -l '*[+.]ce.*' --sort=-v:refname | awk '!/rc/' | head -1").and_return('12.121.12+ce.0')
+        stub_is_ee(false)
       end
 
-      it 'returns the version of correct edition' do
-        expect(described_class.latest_stable_tag).to eq('12.121.12+ce.0')
-      end
-    end
+      context 'on stable branch' do
+        context 'when tags already exist in the stable version series' do
+          before do
+            stub_env_var('CI_COMMIT_BRANCH', '15-10-stable')
+          end
+
+          it 'returns the latest tag in the stable version series' do
+            expect(described_class.latest_stable_tag).to eq('15.10.0+ce.0')
+          end
+        end
 
-    describe 'for EE' do
-      before do
-        stub_env_var('CI_COMMIT_BRANCH', 'foo-feature-branch')
-        stub_is_ee(true)
-        allow(described_class).to receive(:`).with("git -c versionsort.prereleaseSuffix=rc tag -l '*[+.]ee.*' --sort=-v:refname | awk '!/rc/' | head -1").and_return('12.121.12+ee.0')
+        context 'when tags does not exist in the stable version series' do
+          before do
+            stub_env_var('CI_COMMIT_BRANCH', '16-5-stable')
+          end
+
+          it 'returns the latest available tag' do
+            expect(described_class.latest_stable_tag).to eq('16.1.1+ce.0')
+          end
+        end
+
+        context 'when latest tag in the series is an RC tag' do
+          before do
+            stub_env_var('CI_COMMIT_BRANCH', '16-0-stable')
+          end
+
+          it 'skips the RC tag and returns the latest available tag' do
+            expect(described_class.latest_stable_tag).to eq('16.1.1+ce.0')
+          end
+        end
       end
 
-      it 'returns the version of correct edition' do
-        expect(described_class.latest_stable_tag).to eq('12.121.12+ee.0')
+      context 'on feature branch' do
+        before do
+          stub_env_var('CI_COMMIT_BRANCH', 'my-feature-branch')
+        end
+
+        it 'returns the latest available tag' do
+          expect(described_class.latest_stable_tag).to eq('16.1.1+ce.0')
+        end
       end
     end
 
-    describe 'on stable branches' do
+    context 'on EE edition' do
       before do
-        stub_env_var('CI_COMMIT_BRANCH', '15-11-stable')
-        stub_is_ee(false)
+        stub_is_ee(true)
       end
 
-      context 'when no tags exist in the stable branch' do
-        before do
-          allow(described_class).to receive(:`).with("git -c versionsort.prereleaseSuffix=rc tag -l '15.11*[+.]ce.*' --sort=-v:refname | awk '!/rc/' | head -1").and_return(nil)
+      context 'on stable branch' do
+        context 'when tags already exist in the stable version series' do
+          before do
+            stub_env_var('CI_COMMIT_BRANCH', '15-10-stable')
+          end
+
+          it 'returns the latest tag in the stable version series' do
+            expect(described_class.latest_stable_tag).to eq('15.10.0+ee.0')
+          end
         end
 
-        it 'returns the latest available stable tag' do
-          # Twice because we are calling the method two times - once to check
-          # the return value and another time to check if the message is
-          # printed
-          expect(described_class).to receive(:`).with("git -c versionsort.prereleaseSuffix=rc tag -l '15.11*[+.]ce.*' --sort=-v:refname | awk '!/rc/' | head -1").twice
-          expect(described_class).to receive(:`).with("git -c versionsort.prereleaseSuffix=rc tag -l '*[+.]ce.*' --sort=-v:refname | awk '!/rc/' | head -1").twice.and_return('15.10.3+ce.0')
+        context 'when tags does not exist in the stable version series' do
+          before do
+            stub_env_var('CI_COMMIT_BRANCH', '16-5-stable')
+          end
 
-          expect(described_class.latest_stable_tag).to eq('15.10.3+ce.0')
-          expect { described_class.latest_stable_tag }.to output(/No tags found in 15.11.x series/).to_stdout
+          it 'returns the latest available tag' do
+            expect(described_class.latest_stable_tag).to eq('16.1.1+ee.0')
+          end
+        end
+
+        context 'when latest tag in the series is an RC tag' do
+          before do
+            stub_env_var('CI_COMMIT_BRANCH', '16-0-stable')
+          end
+
+          it 'skips the RC tag and returns the latest available tag' do
+            expect(described_class.latest_stable_tag).to eq('16.1.1+ee.0')
+          end
         end
       end
 
-      context 'when tags exist in the stable branch' do
+      context 'on feature branch' do
         before do
-          allow(described_class).to receive(:`).with("git -c versionsort.prereleaseSuffix=rc tag -l '15.11*[+.]ce.*' --sort=-v:refname | awk '!/rc/' | head -1").and_return('15.11.0+ce.0')
+          stub_env_var('CI_COMMIT_BRANCH', 'my-feature-branch')
         end
 
-        it 'returns the latest available stable tag' do
-          expect(described_class).to receive(:`).with("git -c versionsort.prereleaseSuffix=rc tag -l '15.11*[+.]ce.*' --sort=-v:refname | awk '!/rc/' | head -1")
-          expect(described_class).not_to receive(:`).with("git -c versionsort.prereleaseSuffix=rc tag -l '*[+.]ce.*' --sort=-v:refname | awk '!/rc/' | head -1")
-          expect(described_class.latest_stable_tag).to eq('15.11.0+ce.0')
+        it 'returns the latest available tag' do
+          expect(described_class.latest_stable_tag).to eq('16.1.1+ee.0')
+        end
+      end
+    end
+
+    context 'when a level is specified' do
+      context 'when level is less than total number of tags' do
+        before do
+          stub_is_ee(true)
+          stub_env_var('CI_COMMIT_BRANCH', 'my-feature-branch')
+        end
+
+        it 'returns recent tag at specified position' do
+          expect(described_class.latest_stable_tag(level: 2)).to eq('15.11.1+ee.0')
+        end
+      end
+
+      context 'when level is more than total number of tags' do
+        before do
+          stub_is_ee(true)
+          stub_env_var('CI_COMMIT_BRANCH', 'my-feature-branch')
+        end
+
+        it 'returns last tag' do
+          expect(described_class.latest_stable_tag(level: 6)).to eq('15.10.0+ee.0')
         end
       end
     end