From 882ad7490a83e5278fafd0f2c79df063d5292341 Mon Sep 17 00:00:00 2001 From: Jennifer Li <jli@gitlab.com> Date: Tue, 30 Jul 2024 11:10:16 -0700 Subject: [PATCH] Remove CI dependency validation danger Remove spec --- danger/pipeline/Dangerfile | 7 +- .../plugins/ci_jobs_dependency_validation.rb | 9 - .../ci_jobs_dependency_validation_spec.rb | 607 ------------------ .../danger/ci_jobs_dependency_validation.rb | 297 --------- 4 files changed, 1 insertion(+), 919 deletions(-) delete mode 100644 danger/plugins/ci_jobs_dependency_validation.rb delete mode 100644 spec/tooling/danger/ci_jobs_dependency_validation_spec.rb delete mode 100644 tooling/danger/ci_jobs_dependency_validation.rb diff --git a/danger/pipeline/Dangerfile b/danger/pipeline/Dangerfile index 090faf620ee12..0f936d827dc54 100644 --- a/danger/pipeline/Dangerfile +++ b/danger/pipeline/Dangerfile @@ -17,9 +17,4 @@ Please consider the effect of the changes in this merge request on the following Please consider communicating these changes to the broader team following the [communication guideline for pipeline changes](https://about.gitlab.com/handbook/engineering/quality/engineering-productivity/#pipeline-changes) MSG -if helper.has_ci_changes? - markdown(PIPELINE_CHANGES_MESSAGE) - - dependency_validation_message = ci_jobs_dependency_validation.output_message - markdown(dependency_validation_message) unless dependency_validation_message.empty? -end +markdown(PIPELINE_CHANGES_MESSAGE) if helper.has_ci_changes? diff --git a/danger/plugins/ci_jobs_dependency_validation.rb b/danger/plugins/ci_jobs_dependency_validation.rb deleted file mode 100644 index 0d35eb62ed285..0000000000000 --- a/danger/plugins/ci_jobs_dependency_validation.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -require_relative '../../tooling/danger/ci_jobs_dependency_validation' - -module Danger - class CiJobsDependencyValidation < ::Danger::Plugin - include Tooling::Danger::CiJobsDependencyValidation - end -end diff --git a/spec/tooling/danger/ci_jobs_dependency_validation_spec.rb b/spec/tooling/danger/ci_jobs_dependency_validation_spec.rb deleted file mode 100644 index c5e645ffee31a..0000000000000 --- a/spec/tooling/danger/ci_jobs_dependency_validation_spec.rb +++ /dev/null @@ -1,607 +0,0 @@ -# frozen_string_literal: true - -require 'gitlab/dangerfiles/spec_helper' -require 'fast_spec_helper' -require 'webmock/rspec' - -require_relative '../../../tooling/danger/ci_jobs_dependency_validation' - -RSpec.describe Tooling::Danger::CiJobsDependencyValidation, feature_category: :tooling do - include_context 'with dangerfile' - - let(:ci) { true } - let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) } - - let(:rules_base) do - [ - { - 'if' => '$CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE == "detached"', - 'changes' => ["doc/index.md"] - } - ] - end - - let(:job_config_base) { { 'rules' => rules_base, 'needs' => [] } } - let(:new_condition) { { 'if' => '$NEW_VAR == "true"' } } - - let(:rules_with_new_condition) { [*rules_base, new_condition] } - - let(:validated_jobs_base) do - described_class::VALIDATED_JOB_NAMES.index_with { job_config_base } - end - - let(:master_merged_yaml) do - YAML.dump({ - 'job1' => job_config_base - }) - end - - let(:query) do - { - content_ref: 'merged_result_commit_sha', - dry_run_ref: 'feature_branch', - include_jobs: true, - dry_run: true - } - end - - subject(:ci_jobs_dependency_validation) { fake_danger.new(helper: fake_helper) } - - before do - stub_env('CI_COMMIT_SHA', 'merged_result_commit_sha') - - allow(ci_jobs_dependency_validation).to receive_message_chain(:gitlab, :api, :get).with("/projects/1/ci/lint", - query: {}) do - { 'merged_yaml' => master_merged_yaml } - end - - allow(ci_jobs_dependency_validation).to receive_message_chain(:gitlab, :api, :get).with("/projects/1/ci/lint", - query: query) do - { 'merged_yaml' => source_branch_merged_yaml } - end - - allow(ci_jobs_dependency_validation.helper).to receive(:ci?).and_return(ci) - allow(ci_jobs_dependency_validation.helper).to receive(:has_ci_changes?).and_return(true) - allow(ci_jobs_dependency_validation.helper).to receive(:mr_source_branch).and_return('feature_branch') - allow(ci_jobs_dependency_validation.helper).to receive(:mr_target_branch).and_return('master') - allow(ci_jobs_dependency_validation.helper).to receive(:mr_source_project_id).and_return('1') - allow(ci_jobs_dependency_validation.helper).to receive(:mr_target_project_id).and_return('1') - allow($stdout).to receive(:puts) - end - - describe '#output_message' do - shared_examples 'output message' do |warning| - it 'outputs messages' do - if warning - expect(ci_jobs_dependency_validation).to receive(:warn).with(described_class::FAILED_VALIDATION_WARNING) - else - expect(ci_jobs_dependency_validation).not_to receive(:warn) - end - - expect(ci_jobs_dependency_validation.output_message).to eq(expected_message) - end - end - - context 'when not in ci environment' do - let(:ci) { false } - let(:expected_message) { '' } - - it_behaves_like 'output message' - end - - context 'when in ci environment' do - context 'with no ci changes' do - let(:expected_message) { '' } - - before do - allow(ci_jobs_dependency_validation.helper).to receive(:has_ci_changes?).and_return(false) - end - - it_behaves_like 'output message' - end - - context 'with api fails to retrieve jobs from target branch' do - let(:error_msg) { '404 not found' } - - before do - allow( - ci_jobs_dependency_validation - ).to receive_message_chain(:gitlab, :api, :get).with("/projects/1/ci/lint", query: {}).and_raise(error_msg) - end - - it 'warns validation is skipped and outputs empty message' do - expect(ci_jobs_dependency_validation).to receive(:warn).with( - "#{described_class::SKIPPED_VALIDATION_WARNING}: #{error_msg}" - ) - expect { expect(ci_jobs_dependency_validation.output_message).to eq('') }.tap do |expectation| - expectation - .to output(<<~MSG).to_stdout - Initializing 0 jobs from master ci config... - MSG - end - end - end - - context 'with api fails to retrieve jobs from source branch' do - let(:error_msg) { '404 not found' } - - before do - allow( - ci_jobs_dependency_validation - ).to receive_message_chain(:gitlab, :api, :get).with("/projects/1/ci/lint", query: query).and_raise(error_msg) - end - - it 'warns validation is skipped and outputs empty message' do - expect(ci_jobs_dependency_validation).to receive(:warn).with( - "#{described_class::SKIPPED_VALIDATION_WARNING}: #{error_msg}" - ) - - expect { expect(ci_jobs_dependency_validation.output_message).to eq('') }.tap do |expectation| - expectation - .to output(<<~MSG).to_stdout - Initializing 1 jobs from master ci config... - Initializing 0 jobs from feature_branch ci config... - MSG - end - end - end - - context 'with api returns nil for merged yaml' do - let(:source_branch_merged_yaml) { nil } - - it 'warns validation is skipped and outputs empty message' do - expect(ci_jobs_dependency_validation).to receive(:warn).with( - "#{described_class::SKIPPED_VALIDATION_WARNING}: no implicit conversion of nil into String" - ) - - expect(ci_jobs_dependency_validation.output_message).to eq('') - end - end - - context 'when target branch jobs is empty' do - let(:source_branch_merged_yaml) { YAML.dump({}) } - let(:expected_message) { '' } - - it_behaves_like 'output message' - end - - context 'when source branch jobs is empty' do - let(:master_merged_yaml) { YAML.dump({}) } - let(:expected_message) { '' } - - it_behaves_like 'output message' - end - - context 'when jobs do not have dependencies' do - let(:source_branch_merged_yaml) { YAML.dump(validated_jobs_base) } - - let(:expected_message) { described_class::VALIDATION_PASSED_OUTPUT } - - it_behaves_like 'output message' - end - - context 'when needed jobs are missing is source branch' do - let(:source_branch_merged_yaml) do - YAML.dump({ 'job1' => job_config_base.merge({ 'rules' => rules_with_new_condition }) }) - end - - let(:expected_message) do - warning_details = described_class::VALIDATED_JOB_NAMES.map do |job_name| - <<~MARKDOWN - - :warning: Unable to find job `#{job_name}` in branch `feature_branch`. - If this job has been removed, please delete it from `Tooling::Danger::CiJobsDependencyValidation::VALIDATED_JOB_NAMES`. - Validation skipped. - MARKDOWN - end.join("\n") - - <<~MARKDOWN.chomp - ### CI Jobs Dependency Validation - - | name | validation status | - | ------ | --------------- | - | `setup-test-env` | :warning: Skipped | - | `compile-test-assets` | :warning: Skipped | - | `retrieve-tests-metadata` | :warning: Skipped | - | `build-gdk-image` | :warning: Skipped | - | `build-assets-image` | :warning: Skipped | - | `build-qa-image` | :warning: Skipped | - | `e2e-test-pipeline-generate` | :warning: Skipped | - - #{warning_details} - MARKDOWN - end - - it_behaves_like 'output message', true - end - - context 'when job1 in branch needs one other job to run' do - let(:job_name) { 'job1' } - let(:needed_job_name) { 'setup-test-env' } - - let(:source_branch_merged_yaml) do - YAML.dump(validated_jobs_base.merge( - { - job_name => { - 'rules' => rules_with_new_condition, - 'needs' => [needed_job_name] - } - } - )) - end - - context 'with a hidden job' do - let(:job_name) { '.job1' } - let(:expected_message) { described_class::VALIDATION_PASSED_OUTPUT } - - it_behaves_like 'output message' - end - - context 'with a global keyword' do - let(:job_name) { 'default' } - let(:expected_message) { described_class::VALIDATION_PASSED_OUTPUT } - - it_behaves_like 'output message' - end - - context 'when the dependent job config has not changed (identical in master and in branch)' do - let(:master_merged_yaml) { source_branch_merged_yaml } - let(:expected_message) { described_class::VALIDATION_PASSED_OUTPUT } - - it_behaves_like 'output message' - end - - context 'when VALIDATED_JOB_NAMES does not contain the needed job' do - let(:needed_job_name) { 'not-recognized' } - let(:expected_message) { described_class::VALIDATION_PASSED_OUTPUT } - - it_behaves_like 'output message' - end - - context 'when VALIDATED_JOB_NAMES contains the needed job and dependent job config changed' do - context 'when the added rule is also present in its needed job' do - let(:source_branch_merged_yaml) do - YAML.dump(validated_jobs_base.merge({ - job_name => job_config_base.merge({ - 'rules' => rules_with_new_condition, - 'needs' => [needed_job_name] - }), - needed_job_name => { 'rules' => rules_with_new_condition, 'needs' => [] } - })) - end - - let(:expected_message) { described_class::VALIDATION_PASSED_OUTPUT } - - it_behaves_like 'output message' - end - - context 'when an added rule is missing in its needed job' do - let(:expected_message) do - <<~MARKDOWN - ### CI Jobs Dependency Validation - - | name | validation status | - | ------ | --------------- | - | `setup-test-env` | 🚨 Failed (1) | - | `compile-test-assets` | :white_check_mark: Passed | - | `retrieve-tests-metadata` | :white_check_mark: Passed | - | `build-gdk-image` | :white_check_mark: Passed | - | `build-assets-image` | :white_check_mark: Passed | - | `build-qa-image` | :white_check_mark: Passed | - | `e2e-test-pipeline-generate` | :white_check_mark: Passed | - - - 🚨 **These rule changes do not match with rules for `setup-test-env`:** - - <details><summary>Click to expand details</summary> - - `job1`: - - - Added rules: - - ```yaml - - if: $NEW_VAR == "true" - ``` - - - Removed rules: - - `N/A` - - Here are the rules for `setup-test-env`: - - ```yaml - - if: $CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE - == "detached" - changes: - - doc/index.md - ``` - - </details> - - To avoid CI config errors, please verify if the same rule addition/removal should be applied to `setup-test-env`. - If not, please add a comment to explain why. - MARKDOWN - end - - it_behaves_like 'output message', true - end - end - end - - context 'when job configs are malformatted' do - let(:source_branch_merged_yaml) do - YAML.dump(validated_jobs_base.merge( - { - 'job1' => 'not a hash', - 'job2' => ['array'], - 'job3' => { 'key' => 'missing needs and rules' } - } - )) - end - - let(:expected_message) { described_class::VALIDATION_PASSED_OUTPUT } - - it_behaves_like 'output message' - end - - context 'when dependent job has a rule that is not a hash' do - let(:source_branch_merged_yaml) do - YAML.dump( - validated_jobs_base.merge({ - 'job1' => { - 'rules' => ['this is a malformatted rule'], - 'needs' => 'this is a malformatted needs' - } - }) - ) - end - - let(:expected_message) { described_class::VALIDATION_PASSED_OUTPUT } - - it_behaves_like 'output message' - end - - context 'when dependent job have an added rule but condition reads "when: never"' do - let(:new_condition) { { 'if' => "$NEW_VAR == true", 'when' => 'never' } } - let(:source_branch_merged_yaml) do - YAML.dump( - validated_jobs_base.merge({ - 'job1' => { - 'rules' => [new_condition], - 'needs' => ['setup-test-env'] - } - }) - ) - end - - let(:expected_message) { described_class::VALIDATION_PASSED_OUTPUT } - - it_behaves_like 'output message' - end - - context 'when dependent job has removed a rule with "when: never"' do - let(:needed_job_rules) { rules_base } - let(:master_merged_yaml) do - YAML.dump( - validated_jobs_base.merge({ - 'job1' => { - 'rules' => [*rules_base, { 'if' => 'true', 'when' => 'never' }], - 'needs' => ['setup-test-env'] - } - }) - ) - end - - let(:source_branch_merged_yaml) do - YAML.dump( - validated_jobs_base.merge({ - 'job1' => { - 'rules' => rules_base, - 'needs' => ['setup-test-env'] - }, - 'setup-test-env' => { - 'rules' => needed_job_rules - } - }) - ) - end - - context 'when needed_job does not have this negative rule' do - let(:expected_message) { ':white_check_mark: No warnings found in ci job dependencies.' } - - it_behaves_like 'output message' - end - - context 'when needed_job still has this negative rule' do - let(:needed_job_rules) { [*rules_base, { 'if' => 'true', 'when' => 'never' }] } - let(:expected_message) do - <<~MARKDOWN - ### CI Jobs Dependency Validation - - | name | validation status | - | ------ | --------------- | - | `setup-test-env` | 🚨 Failed (1) | - | `compile-test-assets` | :white_check_mark: Passed | - | `retrieve-tests-metadata` | :white_check_mark: Passed | - | `build-gdk-image` | :white_check_mark: Passed | - | `build-assets-image` | :white_check_mark: Passed | - | `build-qa-image` | :white_check_mark: Passed | - | `e2e-test-pipeline-generate` | :white_check_mark: Passed | - - - 🚨 **These rule changes do not match with rules for `setup-test-env`:** - - <details><summary>Click to expand details</summary> - - `job1`: - - - Added rules: - - `N/A` - - - Removed rules: - - ```yaml - - if: 'true' - when: never - ``` - - Here are the rules for `setup-test-env`: - - ```yaml - - if: $CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE - == "detached" - changes: - - doc/index.md - - if: 'true' - when: never - ``` - - </details> - - To avoid CI config errors, please verify if the same rule addition/removal should be applied to `setup-test-env`. - If not, please add a comment to explain why. - MARKDOWN - end - - it_behaves_like 'output message', true - end - end - - context 'when dependent job have modified rules, but its attributes have nested arrays' do - let(:source_branch_merged_yaml) do - YAML.dump( - validated_jobs_base.merge({ - 'job1' => { - 'rules' => [{ 'if' => 'true', 'when' => 'always' }, [new_condition]], - 'needs' => ['setup-test-env', %w[compile-test-assets retrieve-tests-metadata]] - } - }) - ) - end - - let(:message_preview) do - <<~MARKDOWN - ### CI Jobs Dependency Validation - - | name | validation status | - | ------ | --------------- | - | `setup-test-env` | 🚨 Failed (1) | - | `compile-test-assets` | 🚨 Failed (1) | - | `retrieve-tests-metadata` | 🚨 Failed (1) | - | `build-gdk-image` | :white_check_mark: Passed | - | `build-assets-image` | :white_check_mark: Passed | - | `build-qa-image` | :white_check_mark: Passed | - | `e2e-test-pipeline-generate` | :white_check_mark: Passed | - - MARKDOWN - end - - let(:expected_message) do - %w[setup-test-env compile-test-assets retrieve-tests-metadata].map do |job_name| - <<~MARKDOWN - - 🚨 **These rule changes do not match with rules for `#{job_name}`:** - - <details><summary>Click to expand details</summary> - - `job1`: - - - Added rules: - - ```yaml - - if: 'true' - when: always - - if: $NEW_VAR == "true" - ``` - - - Removed rules: - - `N/A` - - Here are the rules for `#{job_name}`: - - ```yaml - - if: $CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE - == "detached" - changes: - - doc/index.md - ``` - - </details> - - To avoid CI config errors, please verify if the same rule addition/removal should be applied to `#{job_name}`. - If not, please add a comment to explain why. - - MARKDOWN - end.join('').prepend(message_preview).chomp - end - - it_behaves_like 'output message', true - end - end - end - - describe '#fetch_jobs_yaml' do - context 'with api returns error' do - before do - allow( - ci_jobs_dependency_validation - ).to receive_message_chain(:gitlab, :api, :get).with("/projects/1/ci/lint", query: {}).and_raise('error') - end - - it 'returns empty object' do - expect(ci_jobs_dependency_validation).to receive(:warn).with( - "#{described_class::SKIPPED_VALIDATION_WARNING}: error" - ) - expect(ci_jobs_dependency_validation.send(:fetch_jobs_yaml, '1', 'master')).to eq({}) - end - end - - context 'with returned payload missing merged_yaml' do - before do - allow( - ci_jobs_dependency_validation - ).to receive_message_chain(:gitlab, :api, :get).with( - "/projects/1/ci/lint", query: {} - ).and_return({ 'errors' => ['error'] }) - end - - it 'returns empty object' do - expect(ci_jobs_dependency_validation).to receive(:warn).with( - "#{described_class::SKIPPED_VALIDATION_WARNING}: error" - ) - expect(ci_jobs_dependency_validation.send(:fetch_jobs_yaml, '1', 'master')).to eq({}) - end - end - - context 'with returned payload has merged_yaml and also has errors' do - before do - allow( - ci_jobs_dependency_validation - ).to receive_message_chain(:gitlab, :api, :get).with( - "/projects/1/ci/lint", query: {} - ).and_return({ 'errors' => ['error'], 'merged_yaml' => master_merged_yaml }) - end - - it 'returns the yaml and disregard the errors' do - expect(ci_jobs_dependency_validation.send(:fetch_jobs_yaml, '1', 'master')).to eq( - YAML.load(master_merged_yaml) - ) - end - end - - context 'with returned merged_yaml cannot be parsed' do - before do - allow( - ci_jobs_dependency_validation - ).to receive_message_chain(:gitlab, :api, :get).with("/projects/1/ci/lint", query: {}).and_return( - { 'merged_yaml' => 'date: 2024-04-04' } - ) - end - - it 'returns empty object' do - expect(ci_jobs_dependency_validation).to receive(:warn).with( - "#{described_class::SKIPPED_VALIDATION_WARNING}: Tried to load unspecified class: Date" - ) - expect(ci_jobs_dependency_validation.send(:fetch_jobs_yaml, '1', 'master')).to eq({}) - end - end - end -end diff --git a/tooling/danger/ci_jobs_dependency_validation.rb b/tooling/danger/ci_jobs_dependency_validation.rb deleted file mode 100644 index db5f2b04ba557..0000000000000 --- a/tooling/danger/ci_jobs_dependency_validation.rb +++ /dev/null @@ -1,297 +0,0 @@ -# frozen_string_literal: true - -module Tooling - module Danger - module CiJobsDependencyValidation - VALIDATED_JOB_NAMES = %w[ - setup-test-env - compile-test-assets - retrieve-tests-metadata - build-gdk-image - build-assets-image - build-qa-image - e2e-test-pipeline-generate - ].freeze - GLOBAL_KEYWORDS = %w[workflow variables stages default].freeze - DEFAULT_BRANCH_NAME = 'master' - FAILED_VALIDATION_WARNING = 'Please review warnings in the *CI Jobs Dependency Validation* section below.' - SKIPPED_VALIDATION_WARNING = 'Job dependency validation is skipped due to error fetching merged CI yaml' - VALIDATION_PASSED_OUTPUT = ':white_check_mark: No warnings found in ci job dependencies.' - - Job = Struct.new(:name, :rules, :needs, keyword_init: true) do - def self.parse_rules_from_yaml(name, jobs_yaml) - attribute_values(jobs_yaml, name, 'rules').filter_map do |rule| - rule.is_a?(Hash) ? rule.slice('if', 'changes', 'when') : rule - end - end - - def self.parse_needs_from_yaml(name, jobs_yaml) - attribute_values(jobs_yaml, name, 'needs').map { |need| need.is_a?(Hash) ? need['job'] : need } - end - - def self.attribute_values(jobs_yaml, name, attribute) - return [] if jobs_yaml.nil? || jobs_yaml.empty? || !jobs_yaml[name].is_a?(Hash) - - values = jobs_yaml.dig(name, attribute) - values.nil? ? [] : Array(values).flatten - end - - def self.ignore?(job_name) - GLOBAL_KEYWORDS.include?(job_name) || job_name.start_with?('.') - end - - def dependent_jobs(jobs) - jobs.select do |job| - !Job.ignore?(job.name) && job.needs.include?(name) - end - end - end - - def output_message - return '' if !helper.ci? || !helper.has_ci_changes? || target_branch_jobs.empty? || source_branch_jobs.empty? - - validation_statuses = VALIDATED_JOB_NAMES.to_h do |job_name| - [job_name, { skipped: false, failed: 0 }] - end - - output = VALIDATED_JOB_NAMES.filter_map do |needed_job_name| - validate(needed_job_name, validation_statuses) - end.join("\n").chomp - - return VALIDATION_PASSED_OUTPUT if output == '' - - warn FAILED_VALIDATION_WARNING - - <<~MARKDOWN - ### CI Jobs Dependency Validation - - | name | validation status | - | ------ | --------------- | - #{construct_summary_table(validation_statuses)} - - #{output} - MARKDOWN - end - - private - - def target_branch_jobs - @target_branch_jobs ||= build_jobs_from_yaml(target_branch_jobs_yaml, target_branch) - end - - def source_branch_jobs - @source_branch_jobs ||= build_jobs_from_yaml(source_branch_jobs_yaml, source_branch) - end - - def target_branch_jobs_yaml - @target_branch_jobs_yaml ||= fetch_jobs_yaml(target_project_id, target_branch) - end - - def source_branch_jobs_yaml - @source_branch_jobs_yaml ||= fetch_jobs_yaml(source_project_id, source_branch) - end - - def fetch_jobs_yaml(project_id, ref) - api_response = gitlab.api.get(lint_path(project_id), query: query_params(ref)) - - raise api_response['errors'].first if api_response['merged_yaml'].nil? && api_response['errors']&.any? - - YAML.load(api_response['merged_yaml'], aliases: true) - rescue StandardError => e - warn "#{SKIPPED_VALIDATION_WARNING}: #{e.message}" - {} - end - - def build_jobs_from_yaml(jobs_yaml, ref) - puts "Initializing #{jobs_yaml.keys.count} jobs from #{ref} ci config..." - - jobs_yaml.filter_map do |job_name, _job_data| - next if Job.ignore?(job_name) - - Job.new( - name: job_name, - rules: Job.parse_rules_from_yaml(job_name, jobs_yaml), - needs: Job.parse_needs_from_yaml(job_name, jobs_yaml) - ) - end - end - - def query_params(ref) - ref_query_params = { - content_ref: merged_result_commit_sha, - dry_run_ref: ref, - include_jobs: true, - dry_run: true - } - - ref == DEFAULT_BRANCH_NAME ? {} : ref_query_params - end - - def validate(needed_job_name, validation_statuses) - needed_job_in_source_branch = source_branch_jobs.find { |job| job.name == needed_job_name } - needed_job_in_target_branch = target_branch_jobs.find { |job| job.name == needed_job_name } - - if needed_job_in_source_branch.nil? - validation_statuses[needed_job_name][:skipped] = true - - return <<~MARKDOWN - - :warning: Unable to find job `#{needed_job_name}` in branch `#{source_branch}`. - If this job has been removed, please delete it from `Tooling::Danger::CiJobsDependencyValidation::VALIDATED_JOB_NAMES`. - Validation skipped. - MARKDOWN - end - - failures = validation_failures( - needed_job_in_source_branch: needed_job_in_source_branch, - needed_job_in_target_branch: needed_job_in_target_branch - ) - - failed_count = failures.count - - return if failed_count == 0 - - validation_statuses[needed_job_name][:failed] = failed_count - - <<~MSG - - 🚨 **These rule changes do not match with rules for `#{needed_job_name}`:** - - <details><summary>Click to expand details</summary> - - #{failures.join("\n")} - Here are the rules for `#{needed_job_name}`: - - ```yaml - #{dump_yaml(needed_job_in_source_branch.rules)} - ``` - - </details> - - To avoid CI config errors, please verify if the same rule addition/removal should be applied to `#{needed_job_name}`. - If not, please add a comment to explain why. - MSG - end - - def construct_summary_table(validation_statuses) - validation_statuses.map do |job_name, statuses| - skipped, failed_count = statuses.values_at(:skipped, :failed) - - summary = if skipped - ":warning: Skipped" - elsif failed_count == 0 - ":white_check_mark: Passed" - else - "🚨 Failed (#{failed_count})" - end - - <<~MARKDOWN.chomp - | `#{job_name}` | #{summary} | - MARKDOWN - end.join("\n") - end - - def validation_failures(needed_job_in_source_branch:, needed_job_in_target_branch:) - dependent_jobs_new = needed_job_in_source_branch&.dependent_jobs(source_branch_jobs) || [] - dependent_jobs_old = needed_job_in_target_branch&.dependent_jobs(target_branch_jobs) || [] - - (dependent_jobs_new - dependent_jobs_old).filter_map do |dependent_job_with_rule_change| - dependent_job_old = dependent_jobs_old.find do |target_branch_job| - target_branch_job.name == dependent_job_with_rule_change.name - end - - new_rules = dependent_job_with_rule_change.rules - old_rules = dependent_job_old&.rules - - added_rules_to_report = rules_missing_in_needed_job( - needed_job: needed_job_in_source_branch, - rules: dependent_job_old.nil? ? new_rules : new_rules - old_rules # added rules - ) - - removed_rules_to_report = removed_negative_rules_present_in_needed_job( - needed_job: needed_job_in_source_branch, - rules: dependent_job_old.nil? ? [] : old_rules - new_rules # removed rules - ) - - next if added_rules_to_report.empty? && removed_rules_to_report.empty? - - <<~MARKDOWN - `#{dependent_job_with_rule_change.name}`: - - - Added rules: - - #{report_yaml_markdown(added_rules_to_report)} - - - Removed rules: - - #{report_yaml_markdown(removed_rules_to_report)} - MARKDOWN - end - end - - def report_yaml_markdown(rules_to_report) - return '`N/A`' unless rules_to_report.any? - - <<~MARKDOWN.chomp - ```yaml - #{dump_yaml(rules_to_report)} - ``` - MARKDOWN - end - - def dump_yaml(yaml) - YAML.dump(yaml).delete_prefix("---\n").chomp - end - - # Limitation: missing rules in needed jobs does not always mean the config is invalid. - # needed_jobs can have very generic rules, for example - # - rule-for-job1: - # - <<: *if-merge-request-targeting-stable-branch - # - rule-for-needed_job: - # - <<: *if-merge-request-targeting-all-branches - # The above config is still valid, but danger will still print a warning because the exact rule is missing. - # We will have to manually identify that this config is fine and the warning should be ignored. - def rules_missing_in_needed_job(rules:, needed_job:) - return [] if rules.empty? - - rules.select do |rule| - !needed_job.rules.include?(rule) && !negative_rule?(rule) - end - end - - def removed_negative_rules_present_in_needed_job(rules:, needed_job:) - return [] if rules.empty? - - rules.select do |rule| - needed_job.rules.include?(rule) && negative_rule?(rule) - end - end - - def negative_rule?(rule) - rule.is_a?(Hash) && rule['when'] == 'never' - end - - def lint_path(project_id) - "/projects/#{project_id}/ci/lint" - end - - def source_project_id - helper.mr_source_project_id - end - - def target_project_id - helper.mr_target_project_id - end - - def source_branch - helper.mr_source_branch - end - - def merged_result_commit_sha - ENV['CI_COMMIT_SHA'] # so we validate the merged results commit - end - - def target_branch - helper.mr_target_branch - end - end - end -end -- GitLab