diff --git a/.gitlab/ci/setup.gitlab-ci.yml b/.gitlab/ci/setup.gitlab-ci.yml index c3cc646ba5885f1a19a8bd7922aa96546fa30632..30c2e23c1ad25c501b56ba116812b346af6b39cd 100644 --- a/.gitlab/ci/setup.gitlab-ci.yml +++ b/.gitlab/ci/setup.gitlab-ci.yml @@ -66,7 +66,7 @@ set-pipeline-name: # We use > instead of | because we want the files to be space-separated. FILES_TO_DOWNLOAD: > scripts/utils.sh - scripts/pipeline/set-pipeline-name + scripts/pipeline/set_pipeline_name.rb image: ${GITLAB_DEPENDENCY_PROXY_ADDRESS}ruby:${RUBY_VERSION}-alpine3.16 stage: prepare before_script: @@ -75,7 +75,7 @@ set-pipeline-name: script: - source scripts/utils.sh - install_gitlab_gem - - chmod u+x scripts/pipeline/set-pipeline-name && scripts/pipeline/set-pipeline-name + - chmod u+x scripts/pipeline/set_pipeline_name.rb && scripts/pipeline/set_pipeline_name.rb allow_failure: exit_codes: - 3 diff --git a/scripts/pipeline/set-pipeline-name b/scripts/pipeline/set-pipeline-name deleted file mode 100755 index 29d8949d51967b951519bce320740cdd28270fca..0000000000000000000000000000000000000000 --- a/scripts/pipeline/set-pipeline-name +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require 'gitlab' - -class SetPipelineName - DOCS = ['docs-lint markdown', 'docs-lint links'].freeze - RSPEC_PREDICTIVE = ['rspec:predictive:trigger', 'rspec-ee:predictive:trigger'].freeze - CODE = ['retrieve-tests-metadata'].freeze - QA_GDK = ['e2e:test-on-gdk'].freeze - REVIEW_APP = ['start-review-app-pipeline'].freeze - # TODO: Please remove `trigger-omnibus-and-follow-up-e2e` and `follow-up-e2e:package-and-test-ee` - # after 2025-04-08 in this project - # - # `trigger-omnibus-and-follow-up-e2e` was renamed to `follow-up:trigger-omnibus` on 2024-04-08 via - # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147908/diffs?pin=c11467759d7eae77ed84e02a5445e21704c8d8e5#c11467759d7eae77ed84e02a5445e21704c8d8e5_105_104 - # - # `follow-up-e2e:package-and-test-ee` was renamed to `follow-up:e2e:package-and-test-ee` on 2024-04-08 via - # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147908/diffs?pin=c11467759d7eae77ed84e02a5445e21704c8d8e5#c11467759d7eae77ed84e02a5445e21704c8d8e5_136_137 - QA = [ - 'e2e:package-and-test-ce', - 'e2e:package-and-test-ee', - 'follow-up-e2e:package-and-test-ee', - 'follow-up:e2e:package-and-test-ee', - 'follow-up:trigger-omnibus', - 'trigger-omnibus-and-follow-up-e2e' - ].freeze - # Ordered by expected duration, DESC - PIPELINE_TYPES_ORDERED = %w[qa review-app qa-gdk code rspec-predictive docs].freeze - - def initialize - @pipeline_types = Set.new - end - - def execute - if ENV['CI_PIPELINE_NAME'].match?(/\[types: .+\]/) - puts "Pipeline name '#{ENV['CI_PIPELINE_NAME']}' already has types in its name." - return - end - - begin - Gitlab.pipeline_bridges(ENV['CI_PROJECT_ID'], ENV['CI_PIPELINE_ID']).auto_paginate do |job| - @pipeline_types.merge(pipeline_types_for(job)) - end - - Gitlab.pipeline_jobs(ENV['CI_PROJECT_ID'], ENV['CI_PIPELINE_ID']).auto_paginate do |job| - @pipeline_types.merge(pipeline_types_for(job)) - end - rescue Gitlab::Error::Error => error - puts "GitLab error: #{error}" - exit_allow_to_fail - end - - pipeline_name = "#{ENV['CI_PIPELINE_NAME']} [types: #{sorted_pipeline_types.join(',')}]" - - puts "Found pipeline types: #{pipeline_types.to_a}" - puts "New pipeline name: #{pipeline_name}" - - set_pipeline_name(pipeline_name) - end - - private - - attr_accessor :pipeline_types - - def pipeline_types_for(job) - pipeline_types = Set.new - pipeline_types << 'rspec-predictive' if RSPEC_PREDICTIVE.include?(job.name) - pipeline_types << 'qa-gdk' if QA_GDK.include?(job.name) - pipeline_types << 'review-app' if REVIEW_APP.include?(job.name) - pipeline_types << 'qa' if QA.include?(job.name) - pipeline_types << 'docs' if DOCS.include?(job.name) - pipeline_types << 'code' if CODE.include?(job.name) - pipeline_types - end - - def sorted_pipeline_types - pipeline_types.sort_by { |type| PIPELINE_TYPES_ORDERED.index(type) } - end - - def set_pipeline_name(pipeline_name) - # TODO: Create an issue in the gitlab gem to add this one - uri = URI("#{ENV['CI_API_V4_URL']}/projects/#{ENV['CI_PROJECT_ID']}/pipelines/#{ENV['CI_PIPELINE_ID']}/metadata") - Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http| - request = Net::HTTP::Put.new uri - request['JOB-TOKEN'] = ENV['CI_JOB_TOKEN'] - request.body = "name=#{pipeline_name}" - response = http.request request - - if response.code != '200' - puts "Failed to set pipeline name: #{response.body}" - exit_allow_to_fail - end - end - end - - # Exit with a different error code, so that we can allow the CI job to fail - def exit_allow_to_fail - exit 3 - end -end - -if $PROGRAM_NAME == __FILE__ - Gitlab.configure do |config| - config.endpoint = ENV['CI_API_V4_URL'] - config.private_token = ENV['PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE'] - end - - SetPipelineName.new.execute -end diff --git a/scripts/pipeline/set_pipeline_name.rb b/scripts/pipeline/set_pipeline_name.rb new file mode 100755 index 0000000000000000000000000000000000000000..fd7a7d2f21cab6214abbe83d0e468389a4ea5bd2 --- /dev/null +++ b/scripts/pipeline/set_pipeline_name.rb @@ -0,0 +1,158 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# We need to take some precautions when using the `gitlab` gem in this project. +# +# See https://docs.gitlab.com/ee/development/pipelines/internals.html#using-the-gitlab-ruby-gem-in-the-canonical-project. +require 'gitlab' unless Object.const_defined?(:Gitlab) +require 'net/http' + +class SetPipelineName + DOCS = ['docs-lint markdown', 'docs-lint links'].freeze + RSPEC_PREDICTIVE = ['rspec:predictive:trigger', 'rspec-ee:predictive:trigger'].freeze + CODE = ['retrieve-tests-metadata'].freeze + QA_GDK = ['e2e:test-on-gdk'].freeze + REVIEW_APP = ['start-review-app-pipeline'].freeze + # TODO: Please remove `trigger-omnibus-and-follow-up-e2e` and `follow-up-e2e:package-and-test-ee` + # after 2025-04-08 in this project + # + # `trigger-omnibus-and-follow-up-e2e` was renamed to `follow-up:trigger-omnibus` on 2024-04-08 via + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147908/diffs?pin=c11467759d7eae77ed84e02a5445e21704c8d8e5#c11467759d7eae77ed84e02a5445e21704c8d8e5_105_104 + # + # `follow-up-e2e:package-and-test-ee` was renamed to `follow-up:e2e:package-and-test-ee` on 2024-04-08 via + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147908/diffs?pin=c11467759d7eae77ed84e02a5445e21704c8d8e5#c11467759d7eae77ed84e02a5445e21704c8d8e5_136_137 + QA = [ + 'e2e:package-and-test-ce', + 'e2e:package-and-test-ee', + 'follow-up-e2e:package-and-test-ee', + 'follow-up:e2e:package-and-test-ee', + 'follow-up:trigger-omnibus', + 'trigger-omnibus-and-follow-up-e2e' + ].freeze + # Ordered by expected duration, DESC + PIPELINE_TYPES_ORDERED = %w[qa review-app qa-gdk code rspec-predictive docs].freeze + + # We need an access token that isn't CI_JOB_TOKEN because we are querying + # the pipelines API to fetch jobs and bridge jobs. + # We are still using CI_JOB_TOKEN to update the pipeline name. + # + # See https://docs.gitlab.com/ee/ci/jobs/ci_job_token.html for more info. + def initialize(api_endpoint:, gitlab_access_token:) + @api_endpoint = api_endpoint + @gitlab_access_token = gitlab_access_token + end + + def gitlab + @gitlab ||= Gitlab.client( + endpoint: @api_endpoint, + private_token: @gitlab_access_token + ) + end + + def execute + # If we already added metadata to the pipeline name, we discard it, and recompute it. + # + # This is in case we retry a CI job that runs this script. + pipeline_name = ENV['CI_PIPELINE_NAME'].sub(/ \[.*\]\z/, '') + + pipeline_suffixes = {} + pipeline_suffixes[:tier] = pipeline_tier || 'N/A' + pipeline_suffixes[:types] = pipeline_types.join(',') + + pipeline_suffix = pipeline_suffixes.map { |key, value| "#{key}:#{value}" }.join(', ') + pipeline_name += " [#{pipeline_suffix}]" + + puts "New pipeline name: #{pipeline_name}" + + set_pipeline_name(pipeline_name) + rescue Gitlab::Error::Error => error + puts "GitLab error: #{error}" + allow_to_fail_return_code + end + + private + + def pipeline_tier + return unless ENV['CI_MERGE_REQUEST_LABELS'] + + # The first pipeline of any MR won't have any tier label, unless the label was added in the MR description + # before creating the MR. This is a known limitation. + # + # Fetching the labels from the API instead of relying on ENV['CI_MERGE_REQUEST_LABELS'] + # would solve this problem, but it would also mean that we would update the tier information + # based on the merge request labels at the time of retrying the job, which isn't what we want. + merge_request_labels = ENV['CI_MERGE_REQUEST_LABELS'].split(',') + puts "Labels from the MR: #{merge_request_labels}" + + tier_label = merge_request_labels.find { |label| label.start_with?('pipeline::tier-') } + return if tier_label.nil? + + tier_label[/\d+\z/] + end + + def pipeline_types + types = Set.new + + gitlab.pipeline_bridges(ENV['CI_PROJECT_ID'], ENV['CI_PIPELINE_ID']).auto_paginate do |job| + types.merge(pipeline_types_for(job)) + end + + gitlab.pipeline_jobs(ENV['CI_PROJECT_ID'], ENV['CI_PIPELINE_ID']).auto_paginate do |job| + types.merge(pipeline_types_for(job)) + end + + types.sort_by { |type| PIPELINE_TYPES_ORDERED.index(type) } + end + + def pipeline_types_for(job) + types = Set.new + types << 'rspec-predictive' if RSPEC_PREDICTIVE.include?(job.name) + types << 'qa-gdk' if QA_GDK.include?(job.name) + types << 'review-app' if REVIEW_APP.include?(job.name) + types << 'qa' if QA.include?(job.name) + types << 'docs' if DOCS.include?(job.name) + types << 'code' if CODE.include?(job.name) + types + end + + def set_pipeline_name(pipeline_name) + # TODO: Replace with the following once a version of the gem is above 4.19.0: + # + # Gitlab.update_pipeline_metadata(ENV['CI_PROJECT_ID'], ENV['CI_PIPELINE_ID'], name: pipeline_name) + # + # New endpoint added in https://github.com/NARKOZ/gitlab/pull/685 (merged on 2024-04-30) + # Latest release was made on 2022-07-10: https://github.com/NARKOZ/gitlab/releases/tag/v4.19.0 + uri = URI("#{ENV['CI_API_V4_URL']}/projects/#{ENV['CI_PROJECT_ID']}/pipelines/#{ENV['CI_PIPELINE_ID']}/metadata") + success = false + error = nil + Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http| + request = Net::HTTP::Put.new uri + request['JOB-TOKEN'] = ENV['CI_JOB_TOKEN'] + request.body = "name=#{pipeline_name}" + response = http.request request + + if response.code == '200' + success = true + else + error = response.body + end + end + + return 0 if success + + puts "Failed to set pipeline name: #{error}" + allow_to_fail_return_code + end + + # Exit with a different error code, so that we can allow the CI job to fail + def allow_to_fail_return_code + 3 + end +end + +if $PROGRAM_NAME == __FILE__ + exit SetPipelineName.new( + api_endpoint: ENV['CI_API_V4_URL'], + gitlab_access_token: ENV['PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE'] + ).execute +end diff --git a/spec/scripts/pipeline/set_pipeline_name_spec.rb b/spec/scripts/pipeline/set_pipeline_name_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..86b4befa2532a6c7b83d4165ff76c0b9e217ff7a --- /dev/null +++ b/spec/scripts/pipeline/set_pipeline_name_spec.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +require_relative '../../support/webmock' +require_relative '../../../scripts/pipeline/set_pipeline_name' + +RSpec.describe SetPipelineName, feature_category: :tooling do + include StubENV + + let(:instance) { described_class.new(api_endpoint: 'gitlab.test', gitlab_access_token: 'xxx') } + let(:original_pipeline_name) { "Ruby 3.2 MR" } + let(:project_id) { '123' } + let(:merge_request_iid) { '1234' } + let(:pipeline_id) { '5678' } + let(:merge_request_labels) { ['Engineering Productivity', 'type::feature', 'pipeline::tier-3'] } + + let(:put_url) { "https://gitlab.test/api/v4/projects/#{project_id}/pipelines/#{pipeline_id}/metadata" } + + let(:jobs) { ['docs-lint markdown'] } + let(:bridges) { ['rspec:predictive:trigger'] } + + # We need to take some precautions when using the `gitlab` gem in this project. + # + # See https://docs.gitlab.com/ee/development/pipelines/internals.html#using-the-gitlab-ruby-gem-in-the-canonical-project. + # + # rubocop:disable RSpec/VerifiedDoubles -- See the disclaimer above + before do + stub_env( + 'CI_API_V4_URL' => 'https://gitlab.test/api/v4', + 'CI_MERGE_REQUEST_IID' => merge_request_iid, + 'CI_PIPELINE_ID' => pipeline_id, + 'CI_PIPELINE_NAME' => original_pipeline_name, + 'CI_PROJECT_ID' => project_id, + 'PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE' => 'xxx', + 'CI_MERGE_REQUEST_LABELS' => merge_request_labels.empty? ? nil : merge_request_labels.join(',') + ) + + stub_request(:put, put_url).to_return(status: 200, body: 'OK') + + # Gitlab client stubbing + client = double('GitLab') + allow(instance).to receive(:gitlab).and_return(client) + allow(client).to yield_jobs(:pipeline_jobs, jobs) + allow(client).to yield_jobs(:pipeline_bridges, bridges) + + # Ensure we don't output to stdout while running tests + allow(instance).to receive(:puts) + end + + def yield_jobs(api_method, jobs) + messages = receive_message_chain(api_method, :auto_paginate) + + jobs.inject(messages) do |stub, job_name| + stub.and_yield(double(name: job_name)) + end + end + # rubocop:enable RSpec/VerifiedDoubles + + describe '#execute' do + context 'when the pipeline is not from a merge request' do + let(:merge_request_iid) { nil } + let(:merge_request_labels) { [] } + + it 'does not add a pipeline tier' do + instance.execute + + expect(WebMock).to have_requested(:put, put_url).with { |req| req.body.include?('tier:N/A') } + end + + it 'changes the pipeline types' do + instance.execute + + # Why not a block with do..end? https://github.com/bblimke/webmock/issues/174#issuecomment-34908908 + expect(WebMock).to have_requested(:put, put_url).with { |req| + req.body.include?('types:rspec-predictive,docs') + } + end + end + + context 'when the pipeline is from a merge request' do + it 'adds a pipeline tier' do + instance.execute + + expect(WebMock).to have_requested(:put, put_url).with { |req| req.body.include?('tier:3') } + end + + it 'adds the pipeline types' do + instance.execute + + # Why not a block with do..end? https://github.com/bblimke/webmock/issues/174#issuecomment-34908908 + expect(WebMock).to have_requested(:put, put_url).with { |req| + req.body.include?('types:rspec-predictive,docs') + } + end + + context 'when the merge request does not have a pipeline tier label' do + let(:merge_request_labels) { ['Engineering Productivity', 'type::feature'] } + + it 'adds the N/A pipeline tier' do + instance.execute + + expect(WebMock).to have_requested(:put, put_url).with { |req| req.body.include?('tier:N/A') } + end + end + end + end + + context 'when we could not update the pipeline name' do + before do + stub_request(:put, put_url).to_return(status: 502, body: 'NOT OK') + end + + it 'displays an error message' do + allow(instance).to receive(:puts).and_call_original + + expect { instance.execute }.to output(/Failed to set pipeline name: NOT OK/).to_stdout + end + + it 'returns an error code of 3' do + expect(instance.execute).to eq(3) + end + end +end diff --git a/spec/support/webmock.rb b/spec/support/webmock.rb index 491fc7ea64daa359b287f7ff7766847b52fe4580..59c61692aa42e4c0e244f98ab042382e8d5cf553 100644 --- a/spec/support/webmock.rb +++ b/spec/support/webmock.rb @@ -24,7 +24,7 @@ def webmock_allowed_hosts hosts << Gitlab.config.webpack.dev_server.host end - if ViteRuby.env['VITE_ENABLED'] == "true" + if Object.const_defined?(:ViteRuby) && ViteRuby.env['VITE_ENABLED'] == "true" hosts << ViteRuby.instance.config.host hosts << ViteRuby.env['VITE_HMR_HOST'] end