From 0b8694bd986b7bded5c47ff2b0bb73dcaee26362 Mon Sep 17 00:00:00 2001 From: Furkan Ayhan Date: Tue, 28 Jun 2022 14:17:49 +0300 Subject: [PATCH 01/15] Add "compare" to rules:changes This is the final step of improving the "rules:changes" CI config. In "compare" you can write the ref that you want to compare with for the changed files. The changes are behind a feature flag "ci_rules_changes_compare". --- .../development/ci_rules_changes_compare.yml | 8 ++ doc/ci/yaml/index.md | 56 ++++++++++++++ lib/gitlab/ci/build/rules.rb | 4 +- lib/gitlab/ci/build/rules/rule/clause.rb | 1 + .../ci/build/rules/rule/clause/changes.rb | 30 +++++++- .../ci/config/entry/rules/rule/changes.rb | 3 +- lib/gitlab/ci/pipeline/seed/build.rb | 12 ++- lib/gitlab/ci/pipeline/seed/stage.rb | 3 +- .../build/rules/rule/clause/changes_spec.rb | 60 ++++++++++++++- .../config/entry/rules/rule/changes_spec.rb | 25 +++++++ .../gitlab/ci/config/entry/rules/rule_spec.rb | 6 ++ .../lib/gitlab/ci/pipeline/seed/build_spec.rb | 20 +++++ .../ci/create_pipeline_service_spec.rb | 74 +++++++++++++++++++ 13 files changed, 294 insertions(+), 8 deletions(-) create mode 100644 config/feature_flags/development/ci_rules_changes_compare.yml diff --git a/config/feature_flags/development/ci_rules_changes_compare.yml b/config/feature_flags/development/ci_rules_changes_compare.yml new file mode 100644 index 00000000000000..c70e44bb3b1785 --- /dev/null +++ b/config/feature_flags/development/ci_rules_changes_compare.yml @@ -0,0 +1,8 @@ +--- +name: ci_rules_changes_compare +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90968 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/366412 +milestone: '15.2' +type: development +group: group::pipeline authoring +default_enabled: false diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index 6d8ca7fe8cf059..3f1e786298b63f 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -3244,6 +3244,62 @@ docker build: - [Jobs or pipelines can run unexpectedly when using `rules: changes`](../jobs/job_control.md#jobs-or-pipelines-run-unexpectedly-when-using-changes). +##### `rules:changes:paths` + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90171) in GitLab 15.2. + +`rules:changes:paths` is an alias for `rules:changes`. + +**Keyword type**: Job keyword. You can use it only as part of a job. + +**Possible inputs**: + +- An array of file paths. + +**Example of `rules:changes:paths`**: + +```yaml +docker build: + script: docker build -t my-image:$CI_COMMIT_REF_SLUG . + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + changes: + paths: + - Dockerfile + when: manual + allow_failure: true +``` + +##### `rules:changes:compare` + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/293645) in GitLab 15.2 [with a flag](../../administration/feature_flags.md) named `ci_rules_changes_compare`. Disabled by default. + +Use `rules:changes:compare` to specify what ref to compare for changes to specific files. +You must combine it with [`rules:changes:paths`](#ruleschangespaths). + +**Keyword type**: Job keyword. You can use it only as part of a job. + +**Possible inputs**: + +- A branch name; 'branch1', 'refs/heads/branch1'. +- A tag name; 'tag1', 'refs/tags/tag1'. +- A commit sha; '2fg31ga14b'. + +**Example of `rules:changes:compare`**: + +```yaml +docker build: + script: docker build -t my-image:$CI_COMMIT_REF_SLUG . + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + changes: + paths: + - Dockerfile + compare: 'refs/heads/branch1' + when: manual + allow_failure: true +``` + #### `rules:exists` > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24021) in GitLab 12.4. diff --git a/lib/gitlab/ci/build/rules.rb b/lib/gitlab/ci/build/rules.rb index 2d4f9cf635b53c..dee95534b07554 100644 --- a/lib/gitlab/ci/build/rules.rb +++ b/lib/gitlab/ci/build/rules.rb @@ -6,7 +6,7 @@ module Build class Rules include ::Gitlab::Utils::StrongMemoize - Result = Struct.new(:when, :start_in, :allow_failure, :variables) do + Result = Struct.new(:when, :start_in, :allow_failure, :variables, :errors) do def build_attributes { when: self.when, @@ -38,6 +38,8 @@ def evaluate(pipeline, context) else Result.new('never') end + rescue Rule::Clause::ParseError => e + Result.new('never', nil, nil, nil, [e.message]) end private diff --git a/lib/gitlab/ci/build/rules/rule/clause.rb b/lib/gitlab/ci/build/rules/rule/clause.rb index 6d4bbbb8c21c94..503f2a8736114c 100644 --- a/lib/gitlab/ci/build/rules/rule/clause.rb +++ b/lib/gitlab/ci/build/rules/rule/clause.rb @@ -11,6 +11,7 @@ class Rules::Rule::Clause # Used for job's inclusion rules configuration. # UnknownClauseError = Class.new(StandardError) + ParseError = Class.new(StandardError) def self.fabricate(type, value) "#{self}::#{type.to_s.camelize}".safe_constantize&.new(value) diff --git a/lib/gitlab/ci/build/rules/rule/clause/changes.rb b/lib/gitlab/ci/build/rules/rule/clause/changes.rb index 1bcd87c9d93f4a..ed66bf12f22810 100644 --- a/lib/gitlab/ci/build/rules/rule/clause/changes.rb +++ b/lib/gitlab/ci/build/rules/rule/clause/changes.rb @@ -11,10 +11,14 @@ def initialize(globs) end def satisfied_by?(pipeline, context) - return true unless pipeline&.modified_paths + modified_paths = find_modified_paths(pipeline, context) + + # This condition can lead to two things; + # 1. pipeline is nil, 2. it falls back to the `else` case in `pipeline.modified_paths` + return true unless modified_paths expanded_globs = expand_globs(context) - pipeline.modified_paths.any? do |path| + modified_paths.any? do |path| expanded_globs.any? do |glob| File.fnmatch?(glob, path, File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB) end @@ -40,6 +44,28 @@ def paths end end end + + def find_modified_paths(pipeline, context) + return unless pipeline + return pipeline.modified_paths unless ::Feature.enabled?(:ci_rules_changes_compare, pipeline.project) + + compare_to_ref = find_compare_to_ref(pipeline) + + if compare_to_ref + pipeline.project.repository.diff_stats(compare_to_ref, context.sha).paths + else + pipeline.modified_paths + end + end + + def find_compare_to_ref(pipeline) + return if @globs[:compare].blank? + + commit = pipeline.project.commit(@globs[:compare]) + raise Rules::Rule::Clause::ParseError, 'rules:changes:compare is not a valid ref' unless commit + + commit.sha + end end end end diff --git a/lib/gitlab/ci/config/entry/rules/rule/changes.rb b/lib/gitlab/ci/config/entry/rules/rule/changes.rb index a56b928450abca..00aad9a85e3aed 100644 --- a/lib/gitlab/ci/config/entry/rules/rule/changes.rb +++ b/lib/gitlab/ci/config/entry/rules/rule/changes.rb @@ -30,7 +30,7 @@ class ComplexChanges < ::Gitlab::Config::Entry::Node include ::Gitlab::Config::Entry::Validatable include ::Gitlab::Config::Entry::Attributable - ALLOWED_KEYS = %i[paths].freeze + ALLOWED_KEYS = %i[paths compare].freeze REQUIRED_KEYS = %i[paths].freeze attributes ALLOWED_KEYS @@ -43,6 +43,7 @@ class ComplexChanges < ::Gitlab::Config::Entry::Node validates :paths, array_of_strings: true, length: { maximum: 50, too_long: "has too many entries (maximum %{count})" } + validates :compare, type: String, allow_nil: true # compare_to instead? end end end diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb index 901208f325a68a..93106b96af20a7 100644 --- a/lib/gitlab/ci/pipeline/seed/build.rb +++ b/lib/gitlab/ci/pipeline/seed/build.rb @@ -54,9 +54,11 @@ def included? end def errors - return unless included? - strong_memoize(:errors) do + # We check rules errors before checking "included?" because rules affects its inclusion status. + next rules_errors if rules_errors + next unless included? + [needs_errors, variable_expansion_errors].compact.flatten end end @@ -168,6 +170,12 @@ def rules_result end end + def rules_errors + strong_memoize(:rules_errors) do + ["Failed to parse rule for #{name}: #{rules_result.errors.join(', ')}"] if rules_result.errors.present? + end + end + def evaluate_context strong_memoize(:evaluate_context) do Gitlab::Ci::Build::Context::Build.new(@pipeline, @seed_attributes) diff --git a/lib/gitlab/ci/pipeline/seed/stage.rb b/lib/gitlab/ci/pipeline/seed/stage.rb index bc56fe9bef93bb..80623eb95a2639 100644 --- a/lib/gitlab/ci/pipeline/seed/stage.rb +++ b/lib/gitlab/ci/pipeline/seed/stage.rb @@ -36,7 +36,8 @@ def seeds def errors strong_memoize(:errors) do - seeds.flat_map(&:errors).compact + # We use "@builds" instead of "seeds" because "seeds" hides the errors from non-included builds. + @builds.flat_map(&:errors).compact end end diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb index 3892b88598a5e8..bbcf4913e5094b 100644 --- a/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb +++ b/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb @@ -4,7 +4,7 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes do describe '#satisfied_by?' do - subject { described_class.new(globs).satisfied_by?(pipeline, context) } + subject(:satisfied_by) { described_class.new(globs).satisfied_by?(pipeline, context) } context 'a glob matching rule' do using RSpec::Parameterized::TableSyntax @@ -92,5 +92,63 @@ it { is_expected.to be_truthy } end end + + context 'when using compare' do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + + let(:compare_sha) { project.commit.sha } + let(:context) { instance_double(Gitlab::Ci::Build::Context::Base, sha: compare_sha) } + + context 'when glob matches' do + let(:globs) { { paths: ['README.md'], compare: 'pages-deploy' } } + + it { is_expected.to be_truthy } + + context 'when the FF ci_rules_changes_compare is disabled' do + before do + stub_feature_flags(ci_rules_changes_compare: false) + end + + it { is_expected.to be_truthy } + end + end + + context 'when glob does not match' do + let(:globs) { { paths: ['xyz.md'], compare: 'pages-deploy' } } + + it { is_expected.to be_falsey } + end + + context 'when compare is a tag' do + let(:globs) { { paths: ['README.md'], compare: 'v1.1.1' } } + + it { is_expected.to be_truthy } + end + + context 'when compare is a sha' do + let(:globs) { { paths: ['README.md'], compare: '189a6c92' } } + + it { is_expected.to be_truthy } + end + + context 'when compare is not a valid ref' do + let(:globs) { { paths: ['README.md'], compare: 'xyz' } } + + it 'raises ParseError' do + expect { satisfied_by }.to raise_error( + ::Gitlab::Ci::Build::Rules::Rule::Clause::ParseError, 'rules:changes:compare is not a valid ref' + ) + end + + context 'when the FF ci_rules_changes_compare is disabled' do + before do + stub_feature_flags(ci_rules_changes_compare: false) + end + + it { is_expected.to be_truthy } + end + end + end end end diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb index 295561b3c4d26a..4f59f8fb567c13 100644 --- a/spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb @@ -119,6 +119,23 @@ end end end + + context 'with paths and compare' do + let(:config) { { paths: %w[app/ lib/], compare: 'branch1' } } + + it { is_expected.to be_valid } + + context 'when compare is not a string' do + let(:config) { { paths: %w[app/ lib/], compare: 1 } } + + it { is_expected.not_to be_valid } + + it 'returns information about errors' do + expect(entry.errors) + .to include(/should be a string/) + end + end + end end describe '#value' do @@ -137,5 +154,13 @@ it { is_expected.to eq(config) } end + + context 'with paths and compare' do + let(:config) do + { paths: ['app/', 'lib/'], compare: 'branch1' } + end + + it { is_expected.to eq(config) } + end end end diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb index 93f4a66bfb6c53..4605abac9a8b6e 100644 --- a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb @@ -418,6 +418,12 @@ it { is_expected.to eq(config) } end + + context 'when using changes with paths and compare' do + let(:config) { { changes: { paths: %w[app/ lib/ spec/ other/* paths/**/*.rb], compare: 'branch1' } } } + + it { is_expected.to eq(config) } + end end context 'when default value has been provided' do diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 040f3ab5830f18..377ef9a0ebfd5f 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -958,6 +958,26 @@ expect(seed_build.attributes).to include(when: 'never') end end + + context 'with invalid rules raising error' do + let(:rule_set) do + [ + { changes: { paths: ['README.md'], compare: 'invalid-ref' }, when: 'never' } + ] + end + + it { is_expected.not_to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'never') + end + + it "returns an error" do + expect(seed_build.errors).to contain_exactly( + "Failed to parse rule for rspec: rules:changes:compare is not a valid ref" + ) + end + end end end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 9cef7f7dadb111..34c57bb0c9c56d 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -2152,6 +2152,80 @@ def find_job(name) end end + context 'with changes: paths and compare' do + let(:config) do + <<-EOY + job1: + script: exit 0 + rules: + - changes: + paths: [README.md] + compare: #{compare_ref} + + job2: + script: exit 0 + EOY + end + + context 'when there is no such ref' do + let(:compare_ref) { 'invalid-branch' } + + it 'returns an error' do + expect(pipeline.errors.full_messages).to eq([ + 'Failed to parse rule for job1: rules:changes:compare is not a valid ref' + ]) + end + + context 'when the FF ci_rules_changes_compare is not enabled' do + before do + stub_feature_flags(ci_rules_changes_compare: false) + end + + it 'ignores compare' do + expect(build_names).to contain_exactly('job2') + end + end + end + + context 'when the ref exists' do + context 'when the rule matches' do + let(:compare_ref) { 'pages-deploy' } + + it 'creates job1 and job2' do + expect(build_names).to contain_exactly('job1', 'job2') + end + + context 'when the FF ci_rules_changes_compare is not enabled' do + before do + stub_feature_flags(ci_rules_changes_compare: false) + end + + it 'ignores compare' do + expect(build_names).to contain_exactly('job2') + end + end + end + + context 'when the rule does not match' do + let(:compare_ref) { 'feature' } + + it 'does not create job1' do + expect(build_names).to contain_exactly('job2') + end + + context 'when the FF ci_rules_changes_compare is not enabled' do + before do + stub_feature_flags(ci_rules_changes_compare: false) + end + + it 'ignores compare' do + expect(build_names).to contain_exactly('job2') + end + end + end + end + end + context 'with mixed if: and changes: rules' do let(:config) do <<-EOY -- GitLab From bf7c6060754249e98fa14d60371b53ff6f4eec29 Mon Sep 17 00:00:00 2001 From: Marcel Amirault Date: Wed, 29 Jun 2022 12:02:26 +0000 Subject: [PATCH 02/15] Apply 2 suggestion(s) to 1 file(s) --- doc/ci/yaml/index.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index 3f1e786298b63f..4abd8e0631c485 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -3274,16 +3274,16 @@ docker build: > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/293645) in GitLab 15.2 [with a flag](../../administration/feature_flags.md) named `ci_rules_changes_compare`. Disabled by default. -Use `rules:changes:compare` to specify what ref to compare for changes to specific files. -You must combine it with [`rules:changes:paths`](#ruleschangespaths). +Use `rules:changes:compare` to specify which ref to compare against for changes to the files +listed under [`rules:changes:paths`](#ruleschangespaths). -**Keyword type**: Job keyword. You can use it only as part of a job. +**Keyword type**: Job keyword. You can use it only as part of a job, and it must be combined with `rules:changes:paths`. **Possible inputs**: -- A branch name; 'branch1', 'refs/heads/branch1'. -- A tag name; 'tag1', 'refs/tags/tag1'. -- A commit sha; '2fg31ga14b'. +- A branch name, like `main`, `branch1`, or `refs/heads/branch1`. +- A tag name, like `tag1` or `refs/tags/tag1`. +- A commit SHA, like `2fg31ga14b`. **Example of `rules:changes:compare`**: -- GitLab From d438193b231e77fe4c6ccb37f81b1ef7dc3e4f47 Mon Sep 17 00:00:00 2001 From: Furkan Ayhan Date: Wed, 29 Jun 2022 15:33:46 +0300 Subject: [PATCH 03/15] Change compare to compare_to --- doc/ci/yaml/index.md | 8 +++---- .../ci/build/rules/rule/clause/changes.rb | 6 ++--- .../ci/config/entry/rules/rule/changes.rb | 4 ++-- .../build/rules/rule/clause/changes_spec.rb | 22 +++++++++---------- .../config/entry/rules/rule/changes_spec.rb | 12 +++++----- .../gitlab/ci/config/entry/rules/rule_spec.rb | 4 ++-- .../lib/gitlab/ci/pipeline/seed/build_spec.rb | 4 ++-- .../ci/create_pipeline_service_spec.rb | 12 +++++----- 8 files changed, 36 insertions(+), 36 deletions(-) diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index 4abd8e0631c485..334110a0dd0d56 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -3270,11 +3270,11 @@ docker build: allow_failure: true ``` -##### `rules:changes:compare` +##### `rules:changes:compare_to` > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/293645) in GitLab 15.2 [with a flag](../../administration/feature_flags.md) named `ci_rules_changes_compare`. Disabled by default. -Use `rules:changes:compare` to specify which ref to compare against for changes to the files +Use `rules:changes:compare_to` to specify which ref to compare against for changes to the files listed under [`rules:changes:paths`](#ruleschangespaths). **Keyword type**: Job keyword. You can use it only as part of a job, and it must be combined with `rules:changes:paths`. @@ -3285,7 +3285,7 @@ listed under [`rules:changes:paths`](#ruleschangespaths). - A tag name, like `tag1` or `refs/tags/tag1`. - A commit SHA, like `2fg31ga14b`. -**Example of `rules:changes:compare`**: +**Example of `rules:changes:compare_to`**: ```yaml docker build: @@ -3295,7 +3295,7 @@ docker build: changes: paths: - Dockerfile - compare: 'refs/heads/branch1' + compare_to: 'refs/heads/branch1' when: manual allow_failure: true ``` diff --git a/lib/gitlab/ci/build/rules/rule/clause/changes.rb b/lib/gitlab/ci/build/rules/rule/clause/changes.rb index ed66bf12f22810..aace242526993c 100644 --- a/lib/gitlab/ci/build/rules/rule/clause/changes.rb +++ b/lib/gitlab/ci/build/rules/rule/clause/changes.rb @@ -59,10 +59,10 @@ def find_modified_paths(pipeline, context) end def find_compare_to_ref(pipeline) - return if @globs[:compare].blank? + return if @globs[:compare_to].blank? - commit = pipeline.project.commit(@globs[:compare]) - raise Rules::Rule::Clause::ParseError, 'rules:changes:compare is not a valid ref' unless commit + commit = pipeline.project.commit(@globs[:compare_to]) + raise Rules::Rule::Clause::ParseError, 'rules:changes:compare_to is not a valid ref' unless commit commit.sha end diff --git a/lib/gitlab/ci/config/entry/rules/rule/changes.rb b/lib/gitlab/ci/config/entry/rules/rule/changes.rb index 00aad9a85e3aed..107e7c228af0ed 100644 --- a/lib/gitlab/ci/config/entry/rules/rule/changes.rb +++ b/lib/gitlab/ci/config/entry/rules/rule/changes.rb @@ -30,7 +30,7 @@ class ComplexChanges < ::Gitlab::Config::Entry::Node include ::Gitlab::Config::Entry::Validatable include ::Gitlab::Config::Entry::Attributable - ALLOWED_KEYS = %i[paths compare].freeze + ALLOWED_KEYS = %i[paths compare_to].freeze REQUIRED_KEYS = %i[paths].freeze attributes ALLOWED_KEYS @@ -43,7 +43,7 @@ class ComplexChanges < ::Gitlab::Config::Entry::Node validates :paths, array_of_strings: true, length: { maximum: 50, too_long: "has too many entries (maximum %{count})" } - validates :compare, type: String, allow_nil: true # compare_to instead? + validates :compare_to, type: String, allow_nil: true end end end diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb index bbcf4913e5094b..7bddd07c1e8c6c 100644 --- a/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb +++ b/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb @@ -93,7 +93,7 @@ end end - context 'when using compare' do + context 'when using compare_to' do let_it_be(:project) { create(:project, :repository) } let_it_be(:pipeline) { create(:ci_pipeline, project: project) } @@ -101,13 +101,13 @@ let(:context) { instance_double(Gitlab::Ci::Build::Context::Base, sha: compare_sha) } context 'when glob matches' do - let(:globs) { { paths: ['README.md'], compare: 'pages-deploy' } } + let(:globs) { { paths: ['README.md'], compare_to: 'pages-deploy' } } it { is_expected.to be_truthy } context 'when the FF ci_rules_changes_compare is disabled' do before do - stub_feature_flags(ci_rules_changes_compare: false) + stub_feature_flags(ci_rules_changes_compare_to: false) end it { is_expected.to be_truthy } @@ -115,25 +115,25 @@ end context 'when glob does not match' do - let(:globs) { { paths: ['xyz.md'], compare: 'pages-deploy' } } + let(:globs) { { paths: ['xyz.md'], compare_to: 'pages-deploy' } } it { is_expected.to be_falsey } end - context 'when compare is a tag' do - let(:globs) { { paths: ['README.md'], compare: 'v1.1.1' } } + context 'when compare_to is a tag' do + let(:globs) { { paths: ['README.md'], compare_to: 'v1.1.1' } } it { is_expected.to be_truthy } end - context 'when compare is a sha' do - let(:globs) { { paths: ['README.md'], compare: '189a6c92' } } + context 'when compare_to is a sha' do + let(:globs) { { paths: ['README.md'], compare_to: '189a6c92' } } it { is_expected.to be_truthy } end - context 'when compare is not a valid ref' do - let(:globs) { { paths: ['README.md'], compare: 'xyz' } } + context 'when compare_to is not a valid ref' do + let(:globs) { { paths: ['README.md'], compare_to: 'xyz' } } it 'raises ParseError' do expect { satisfied_by }.to raise_error( @@ -143,7 +143,7 @@ context 'when the FF ci_rules_changes_compare is disabled' do before do - stub_feature_flags(ci_rules_changes_compare: false) + stub_feature_flags(ci_rules_changes_compare_to: false) end it { is_expected.to be_truthy } diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb index 4f59f8fb567c13..7bf85cb0d73b3e 100644 --- a/spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb @@ -120,13 +120,13 @@ end end - context 'with paths and compare' do - let(:config) { { paths: %w[app/ lib/], compare: 'branch1' } } + context 'with paths and compare_to' do + let(:config) { { paths: %w[app/ lib/], compare_to: 'branch1' } } it { is_expected.to be_valid } - context 'when compare is not a string' do - let(:config) { { paths: %w[app/ lib/], compare: 1 } } + context 'when compare_to is not a string' do + let(:config) { { paths: %w[app/ lib/], compare_to: 1 } } it { is_expected.not_to be_valid } @@ -155,9 +155,9 @@ it { is_expected.to eq(config) } end - context 'with paths and compare' do + context 'with paths and compare_to' do let(:config) do - { paths: ['app/', 'lib/'], compare: 'branch1' } + { paths: ['app/', 'lib/'], compare_to: 'branch1' } end it { is_expected.to eq(config) } diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb index 4605abac9a8b6e..10b4cba3bbb13f 100644 --- a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb @@ -419,8 +419,8 @@ it { is_expected.to eq(config) } end - context 'when using changes with paths and compare' do - let(:config) { { changes: { paths: %w[app/ lib/ spec/ other/* paths/**/*.rb], compare: 'branch1' } } } + context 'when using changes with paths and compare_to' do + let(:config) { { changes: { paths: %w[app/ lib/ spec/ other/* paths/**/*.rb], compare_to: 'branch1' } } } it { is_expected.to eq(config) } end diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 377ef9a0ebfd5f..15ef744a474ffe 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -962,7 +962,7 @@ context 'with invalid rules raising error' do let(:rule_set) do [ - { changes: { paths: ['README.md'], compare: 'invalid-ref' }, when: 'never' } + { changes: { paths: ['README.md'], compare_to: 'invalid-ref' }, when: 'never' } ] end @@ -974,7 +974,7 @@ it "returns an error" do expect(seed_build.errors).to contain_exactly( - "Failed to parse rule for rspec: rules:changes:compare is not a valid ref" + "Failed to parse rule for rspec: rules:changes:compare_to is not a valid ref" ) end end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 34c57bb0c9c56d..fc51ba222ab210 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -2152,7 +2152,7 @@ def find_job(name) end end - context 'with changes: paths and compare' do + context 'with changes: paths and compare_to' do let(:config) do <<-EOY job1: @@ -2160,7 +2160,7 @@ def find_job(name) rules: - changes: paths: [README.md] - compare: #{compare_ref} + compare_to: #{compare_ref} job2: script: exit 0 @@ -2172,7 +2172,7 @@ def find_job(name) it 'returns an error' do expect(pipeline.errors.full_messages).to eq([ - 'Failed to parse rule for job1: rules:changes:compare is not a valid ref' + 'Failed to parse rule for job1: rules:changes:compare_to is not a valid ref' ]) end @@ -2181,7 +2181,7 @@ def find_job(name) stub_feature_flags(ci_rules_changes_compare: false) end - it 'ignores compare' do + it 'ignores compare_to' do expect(build_names).to contain_exactly('job2') end end @@ -2200,7 +2200,7 @@ def find_job(name) stub_feature_flags(ci_rules_changes_compare: false) end - it 'ignores compare' do + it 'ignores compare_to' do expect(build_names).to contain_exactly('job2') end end @@ -2218,7 +2218,7 @@ def find_job(name) stub_feature_flags(ci_rules_changes_compare: false) end - it 'ignores compare' do + it 'ignores compare_to' do expect(build_names).to contain_exactly('job2') end end -- GitLab From 0d0374da5e7eb51bbde40a9fa73865b801a2f0ab Mon Sep 17 00:00:00 2001 From: Marcel Amirault Date: Tue, 12 Jul 2022 14:41:29 +0000 Subject: [PATCH 04/15] Updates documentation --- doc/ci/yaml/index.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index 334110a0dd0d56..87b838ea3319df 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -3270,6 +3270,9 @@ docker build: allow_failure: true ``` +In this example, the `docker build` job is only included when the `Dockerfile` has changed +and the pipeline source is a merge request event. + ##### `rules:changes:compare_to` > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/293645) in GitLab 15.2 [with a flag](../../administration/feature_flags.md) named `ci_rules_changes_compare`. Disabled by default. @@ -3300,6 +3303,9 @@ docker build: allow_failure: true ``` +In this example, the `docker build` job is only included when the `Dockerfile` has changed +relative to `refs/heads/branch1` and the pipeline source is a merge request event. + #### `rules:exists` > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/24021) in GitLab 12.4. -- GitLab From 262df989fa880cbea13e595a1c8a7eb2c3445cc1 Mon Sep 17 00:00:00 2001 From: lauraMon Date: Wed, 13 Jul 2022 17:39:36 +0200 Subject: [PATCH 05/15] Adds a spec for workflow:rules --- .../ci/create_pipeline_service_spec.rb | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index fc51ba222ab210..52abb7655cf6e0 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -2152,6 +2152,83 @@ def find_job(name) end end + context 'with workflow:rules and changes:paths and compare_to' do + let(:config) do + <<-EOY + workflow: + rules: + - changes: + paths: [README.md] + compare_to: #{compare_ref} + + job1: + script: exit 0 + EOY + end + + context 'when there is no such ref' do + let(:compare_ref) { 'invalid-branch' } + + it 'returns an error' do + expect(pipeline.errors.full_messages).to eq([ + 'Pipeline filtered out by workflow rules.' + ]) + end + + context 'when the FF ci_rules_changes_compare is not enabled' do + before do + stub_feature_flags(ci_rules_changes_compare: false) + end + + it 'ignores compare_to' do + expect(build_names).to be_empty + expect(pipeline).not_to be_created_successfully + end + end + end + + context 'when the ref exists' do + context 'when the rule matches' do + let(:compare_ref) { 'pages-deploy' } + + it 'creates job1' do + expect(build_names).to contain_exactly('job1') + end + + context 'when the FF ci_rules_changes_compare is not enabled' do + before do + stub_feature_flags(ci_rules_changes_compare: false) + end + + it 'ignores compare_to' do + expect(pipeline).not_to be_created_successfully + expect(build_names).to be_empty + end + end + end + + context 'when the rule does not match' do + let(:compare_ref) { 'feature' } + + it 'does not create job1' do + expect(pipeline).not_to be_created_successfully + expect(build_names).to be_empty + end + + context 'when the FF ci_rules_changes_compare is not enabled' do + before do + stub_feature_flags(ci_rules_changes_compare: false) + end + + it 'ignores compare_to' do + expect(pipeline).not_to be_created_successfully + expect(build_names).to be_empty + end + end + end + end + end + context 'with changes: paths and compare_to' do let(:config) do <<-EOY -- GitLab From 108a9484f54b871aa295a6ce5462317caca8a065 Mon Sep 17 00:00:00 2001 From: lauraMon Date: Thu, 14 Jul 2022 12:56:27 +0200 Subject: [PATCH 06/15] Updates find_compare_to_ref return conditional --- lib/gitlab/ci/build/rules/rule/clause/changes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/ci/build/rules/rule/clause/changes.rb b/lib/gitlab/ci/build/rules/rule/clause/changes.rb index aace242526993c..404ff474a31733 100644 --- a/lib/gitlab/ci/build/rules/rule/clause/changes.rb +++ b/lib/gitlab/ci/build/rules/rule/clause/changes.rb @@ -59,7 +59,7 @@ def find_modified_paths(pipeline, context) end def find_compare_to_ref(pipeline) - return if @globs[:compare_to].blank? + return unless @globs.include?(:compare_to) commit = pipeline.project.commit(@globs[:compare_to]) raise Rules::Rule::Clause::ParseError, 'rules:changes:compare_to is not a valid ref' unless commit -- GitLab From 852a03415354489b6cf688b3827de7a0391f61f7 Mon Sep 17 00:00:00 2001 From: lauraMon Date: Thu, 14 Jul 2022 14:01:52 +0200 Subject: [PATCH 07/15] Extract method in lib to modified_paths_since --- app/models/ci/pipeline.rb | 4 ++++ .../ci/build/rules/rule/clause/changes.rb | 2 +- spec/models/ci/pipeline_spec.rb | 19 +++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 791bae172718e0..63e50a08c4ce86 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -1152,6 +1152,10 @@ def modified_paths end end + def modified_paths_since(compare_to_ref) + project.repository.diff_stats(compare_to_ref, sha).paths + end + def all_worktree_paths strong_memoize(:all_worktree_paths) do project.repository.ls_files(sha) diff --git a/lib/gitlab/ci/build/rules/rule/clause/changes.rb b/lib/gitlab/ci/build/rules/rule/clause/changes.rb index 404ff474a31733..06e3cb033ca1bd 100644 --- a/lib/gitlab/ci/build/rules/rule/clause/changes.rb +++ b/lib/gitlab/ci/build/rules/rule/clause/changes.rb @@ -52,7 +52,7 @@ def find_modified_paths(pipeline, context) compare_to_ref = find_compare_to_ref(pipeline) if compare_to_ref - pipeline.project.repository.diff_stats(compare_to_ref, context.sha).paths + pipeline.modified_paths_since(compare_to_ref) else pipeline.modified_paths end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 081fa6cbbaeb31..14100ebdaaa022 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -2162,6 +2162,25 @@ def create_build(name, *traits, queued_at: current, started_from: 0, **opts) end end + describe '#modified_paths_since' do + subject (:modified_paths_since) { pipeline.modified_paths_since(compare_to_ref) } + + let(:compare_to_ref) { 'master' } + let(:project) { create(:project, :repository) } + let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.head_commit.sha) } + + it 'returns path' do + diff_stats = double(:diff_stats, paths: ['README.md']) + + expect(project.repository) + .to receive(:diff_stats).with(compare_to_ref, pipeline.sha) + .and_return(diff_stats) + + expect(modified_paths_since).to match_array(['README.md']) + end + end + + describe '#all_worktree_paths' do let(:files) { { 'main.go' => '', 'mocks/mocks.go' => '' } } let(:project) { create(:project, :custom_repo, files: files) } -- GitLab From fff4af82cddb0b447a7d1ab657b4cab12fd0c871 Mon Sep 17 00:00:00 2001 From: lauraMon Date: Thu, 14 Jul 2022 14:12:03 +0200 Subject: [PATCH 08/15] Fixes typos and removes comments --- lib/gitlab/ci/build/rules/rule/clause/changes.rb | 2 -- lib/gitlab/ci/pipeline/seed/stage.rb | 1 - spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb | 6 +++--- spec/lib/gitlab/ci/pipeline/seed/build_spec.rb | 6 +++--- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/gitlab/ci/build/rules/rule/clause/changes.rb b/lib/gitlab/ci/build/rules/rule/clause/changes.rb index 06e3cb033ca1bd..8ce3ef8a49f677 100644 --- a/lib/gitlab/ci/build/rules/rule/clause/changes.rb +++ b/lib/gitlab/ci/build/rules/rule/clause/changes.rb @@ -13,8 +13,6 @@ def initialize(globs) def satisfied_by?(pipeline, context) modified_paths = find_modified_paths(pipeline, context) - # This condition can lead to two things; - # 1. pipeline is nil, 2. it falls back to the `else` case in `pipeline.modified_paths` return true unless modified_paths expanded_globs = expand_globs(context) diff --git a/lib/gitlab/ci/pipeline/seed/stage.rb b/lib/gitlab/ci/pipeline/seed/stage.rb index 80623eb95a2639..7cf6466cf4b25f 100644 --- a/lib/gitlab/ci/pipeline/seed/stage.rb +++ b/lib/gitlab/ci/pipeline/seed/stage.rb @@ -36,7 +36,6 @@ def seeds def errors strong_memoize(:errors) do - # We use "@builds" instead of "seeds" because "seeds" hides the errors from non-included builds. @builds.flat_map(&:errors).compact end end diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb index 7bddd07c1e8c6c..f0cfffb4b9d6e4 100644 --- a/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb +++ b/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb @@ -107,7 +107,7 @@ context 'when the FF ci_rules_changes_compare is disabled' do before do - stub_feature_flags(ci_rules_changes_compare_to: false) + stub_feature_flags(ci_rules_changes_compare: false) end it { is_expected.to be_truthy } @@ -137,13 +137,13 @@ it 'raises ParseError' do expect { satisfied_by }.to raise_error( - ::Gitlab::Ci::Build::Rules::Rule::Clause::ParseError, 'rules:changes:compare is not a valid ref' + ::Gitlab::Ci::Build::Rules::Rule::Clause::ParseError, 'rules:changes:compare_to is not a valid ref' ) end context 'when the FF ci_rules_changes_compare is disabled' do before do - stub_feature_flags(ci_rules_changes_compare_to: false) + stub_feature_flags(ci_rules_changes_compare: false) end it { is_expected.to be_truthy } diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 15ef744a474ffe..890ba51157a43a 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -769,7 +769,7 @@ with_them do it { is_expected.not_to be_included } - it 'correctly populates when:' do + it 'still correctly populates when:' do expect(seed_build.attributes).to include(when: 'never') end end @@ -972,9 +972,9 @@ expect(seed_build.attributes).to include(when: 'never') end - it "returns an error" do + it 'returns an error' do expect(seed_build.errors).to contain_exactly( - "Failed to parse rule for rspec: rules:changes:compare_to is not a valid ref" + 'Failed to parse rule for rspec: rules:changes:compare_to is not a valid ref' ) end end -- GitLab From 36e6d6db1f9b9ad800b91aa341c54af461654b50 Mon Sep 17 00:00:00 2001 From: lauraMon Date: Thu, 14 Jul 2022 14:53:35 +0200 Subject: [PATCH 09/15] Uses spec_helper rather than fast_ --- spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb | 2 +- spec/models/ci/pipeline_spec.rb | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb index 7bf85cb0d73b3e..64f0a64074c215 100644 --- a/spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/rules/rule/changes_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'spec_helper' RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule::Changes do let(:factory) do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 14100ebdaaa022..20649428f9abcb 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -2163,7 +2163,7 @@ def create_build(name, *traits, queued_at: current, started_from: 0, **opts) end describe '#modified_paths_since' do - subject (:modified_paths_since) { pipeline.modified_paths_since(compare_to_ref) } + subject(:modified_paths_since) { pipeline.modified_paths_since(compare_to_ref) } let(:compare_to_ref) { 'master' } let(:project) { create(:project, :repository) } @@ -2180,7 +2180,6 @@ def create_build(name, *traits, queued_at: current, started_from: 0, **opts) end end - describe '#all_worktree_paths' do let(:files) { { 'main.go' => '', 'mocks/mocks.go' => '' } } let(:project) { create(:project, :custom_repo, files: files) } -- GitLab From 2191eb6dd56305c62e357a7370dc1c438535678b Mon Sep 17 00:00:00 2001 From: Furkan Ayhan Date: Wed, 20 Jul 2022 14:02:20 +0300 Subject: [PATCH 10/15] Simplify the docs --- doc/ci/yaml/index.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index 87b838ea3319df..3fbc6a40a4b9fd 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -3266,8 +3266,6 @@ docker build: changes: paths: - Dockerfile - when: manual - allow_failure: true ``` In this example, the `docker build` job is only included when the `Dockerfile` has changed @@ -3275,7 +3273,7 @@ and the pipeline source is a merge request event. ##### `rules:changes:compare_to` -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/293645) in GitLab 15.2 [with a flag](../../administration/feature_flags.md) named `ci_rules_changes_compare`. Disabled by default. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/293645) in GitLab 15.3 [with a flag](../../administration/feature_flags.md) named `ci_rules_changes_compare`. Disabled by default. Use `rules:changes:compare_to` to specify which ref to compare against for changes to the files listed under [`rules:changes:paths`](#ruleschangespaths). @@ -3299,8 +3297,6 @@ docker build: paths: - Dockerfile compare_to: 'refs/heads/branch1' - when: manual - allow_failure: true ``` In this example, the `docker build` job is only included when the `Dockerfile` has changed -- GitLab From c77e4febb9237e0cdb10de0ec127b013a5955f58 Mon Sep 17 00:00:00 2001 From: Furkan Ayhan Date: Wed, 20 Jul 2022 14:02:39 +0300 Subject: [PATCH 11/15] Add a failing test case for modified_paths_since to change --- app/models/ci/pipeline.rb | 1 + spec/models/ci/pipeline_spec.rb | 24 ++++++++++++++---------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 63e50a08c4ce86..25b83a87953199 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -1153,6 +1153,7 @@ def modified_paths end def modified_paths_since(compare_to_ref) + # TODO: project.repository.diff_stats(project.repository.merge_base(compare_to_ref, sha), sha).paths project.repository.diff_stats(compare_to_ref, sha).paths end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 20649428f9abcb..f72174f1ea316b 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -2163,20 +2163,24 @@ def create_build(name, *traits, queued_at: current, started_from: 0, **opts) end describe '#modified_paths_since' do - subject(:modified_paths_since) { pipeline.modified_paths_since(compare_to_ref) } + let(:project) do + create(:project, :custom_repo, + files: { 'file1.txt' => 'file 1' }) + end - let(:compare_to_ref) { 'master' } - let(:project) { create(:project, :repository) } - let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.head_commit.sha) } + let(:user) { project.owner } + let(:main_branch) { project.default_branch } + let(:new_branch) { 'feature_x' } + let(:pipeline) { build(:ci_pipeline, project: project, sha: new_branch) } - it 'returns path' do - diff_stats = double(:diff_stats, paths: ['README.md']) + subject(:modified_paths_since) { pipeline.modified_paths_since(main_branch) } - expect(project.repository) - .to receive(:diff_stats).with(compare_to_ref, pipeline.sha) - .and_return(diff_stats) + it 'returns only file2.txt' do + project.repository.add_branch(user, new_branch, main_branch) + project.repository.create_file(user, 'file2.txt', 'file 2', message: 'Create file2.txt', branch_name: new_branch) + project.repository.update_file(user, 'file1.txt', 'file 1 v2', message: 'Update file1.txt', branch_name: main_branch) - expect(modified_paths_since).to match_array(['README.md']) + expect(modified_paths_since).to match_array(['file2.txt']) end end -- GitLab From 15c80230b5f26487437f52f0f73156f537ada52c Mon Sep 17 00:00:00 2001 From: Furkan Ayhan Date: Thu, 21 Jul 2022 14:09:07 +0300 Subject: [PATCH 12/15] Update milestone of FF --- config/feature_flags/development/ci_rules_changes_compare.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/feature_flags/development/ci_rules_changes_compare.yml b/config/feature_flags/development/ci_rules_changes_compare.yml index c70e44bb3b1785..b405c2ab919a4e 100644 --- a/config/feature_flags/development/ci_rules_changes_compare.yml +++ b/config/feature_flags/development/ci_rules_changes_compare.yml @@ -2,7 +2,7 @@ name: ci_rules_changes_compare introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90968 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/366412 -milestone: '15.2' +milestone: '15.3' type: development group: group::pipeline authoring default_enabled: false -- GitLab From 6c6dfd47384fefadc88852d039c74848a4fc4411 Mon Sep 17 00:00:00 2001 From: Furkan Ayhan Date: Thu, 21 Jul 2022 14:10:15 +0300 Subject: [PATCH 13/15] Fix modified_paths_since with merge_base --- app/models/ci/pipeline.rb | 3 +-- spec/models/ci/pipeline_spec.rb | 40 +++++++++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 25b83a87953199..839c2580ed79f8 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -1153,8 +1153,7 @@ def modified_paths end def modified_paths_since(compare_to_ref) - # TODO: project.repository.diff_stats(project.repository.merge_base(compare_to_ref, sha), sha).paths - project.repository.diff_stats(compare_to_ref, sha).paths + project.repository.diff_stats(project.repository.merge_base(compare_to_ref, sha), sha).paths end def all_worktree_paths diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index f72174f1ea316b..b1ddc26b1dc11b 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -2175,12 +2175,44 @@ def create_build(name, *traits, queued_at: current, started_from: 0, **opts) subject(:modified_paths_since) { pipeline.modified_paths_since(main_branch) } - it 'returns only file2.txt' do + before do project.repository.add_branch(user, new_branch, main_branch) - project.repository.create_file(user, 'file2.txt', 'file 2', message: 'Create file2.txt', branch_name: new_branch) - project.repository.update_file(user, 'file1.txt', 'file 1 v2', message: 'Update file1.txt', branch_name: main_branch) + end + + context 'when no change in the new branch' do + it 'returns an empty array' do + expect(modified_paths_since).to be_empty + end + end - expect(modified_paths_since).to match_array(['file2.txt']) + context 'when adding a new file' do + before do + project.repository.create_file(user, 'file2.txt', 'file 2', message: 'Create file2.txt', branch_name: new_branch) + end + + it 'returns the new file path' do + expect(modified_paths_since).to eq(['file2.txt']) + end + + context 'and when updating an existing file' do + before do + project.repository.update_file(user, 'file1.txt', 'file 1 updated', message: 'Update file1.txt', branch_name: new_branch) + end + + it 'returns the new and updated file paths' do + expect(modified_paths_since).to eq(['file1.txt', 'file2.txt']) + end + end + end + + context 'when updating an existing file' do + before do + project.repository.update_file(user, 'file1.txt', 'file 1 updated', message: 'Update file1.txt', branch_name: new_branch) + end + + it 'returns the updated file path' do + expect(modified_paths_since).to eq(['file1.txt']) + end end end -- GitLab From d4a98d8ef3d371d41799ebbea26619347cb5cdfd Mon Sep 17 00:00:00 2001 From: Furkan Ayhan Date: Fri, 22 Jul 2022 13:46:30 +0300 Subject: [PATCH 14/15] Refactor some methods --- app/models/ci/pipeline.rb | 4 ++-- lib/gitlab/ci/build/rules/rule/clause/changes.rb | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 839c2580ed79f8..77704ac02d293f 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -1152,8 +1152,8 @@ def modified_paths end end - def modified_paths_since(compare_to_ref) - project.repository.diff_stats(project.repository.merge_base(compare_to_ref, sha), sha).paths + def modified_paths_since(compare_to_sha) + project.repository.diff_stats(project.repository.merge_base(compare_to_sha, sha), sha).paths end def all_worktree_paths diff --git a/lib/gitlab/ci/build/rules/rule/clause/changes.rb b/lib/gitlab/ci/build/rules/rule/clause/changes.rb index 8ce3ef8a49f677..29e43198635a93 100644 --- a/lib/gitlab/ci/build/rules/rule/clause/changes.rb +++ b/lib/gitlab/ci/build/rules/rule/clause/changes.rb @@ -11,7 +11,7 @@ def initialize(globs) end def satisfied_by?(pipeline, context) - modified_paths = find_modified_paths(pipeline, context) + modified_paths = find_modified_paths(pipeline) return true unless modified_paths @@ -43,20 +43,20 @@ def paths end end - def find_modified_paths(pipeline, context) + def find_modified_paths(pipeline) return unless pipeline return pipeline.modified_paths unless ::Feature.enabled?(:ci_rules_changes_compare, pipeline.project) - compare_to_ref = find_compare_to_ref(pipeline) + compare_to_sha = find_compare_to_sha(pipeline) - if compare_to_ref - pipeline.modified_paths_since(compare_to_ref) + if compare_to_sha + pipeline.modified_paths_since(compare_to_sha) else pipeline.modified_paths end end - def find_compare_to_ref(pipeline) + def find_compare_to_sha(pipeline) return unless @globs.include?(:compare_to) commit = pipeline.project.commit(@globs[:compare_to]) -- GitLab From 2fe4248ba738e90dc1b63b5afd106a84cf17acfc Mon Sep 17 00:00:00 2001 From: Furkan Ayhan Date: Fri, 22 Jul 2022 13:47:11 +0300 Subject: [PATCH 15/15] Fix and add tests --- .../build/rules/rule/clause/changes_spec.rb | 80 +++++--- .../ci/create_pipeline_service_spec.rb | 185 ++++++++---------- 2 files changed, 145 insertions(+), 120 deletions(-) diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb index f0cfffb4b9d6e4..fa92d1a830d940 100644 --- a/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb +++ b/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb @@ -4,6 +4,8 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes do describe '#satisfied_by?' do + let(:context) { instance_double(Gitlab::Ci::Build::Context::Base) } + subject(:satisfied_by) { described_class.new(globs).satisfied_by?(pipeline, context) } context 'a glob matching rule' do @@ -94,46 +96,80 @@ end context 'when using compare_to' do - let_it_be(:project) { create(:project, :repository) } - let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:project) do + create(:project, :custom_repo, + files: { 'README.md' => 'readme' }) + end - let(:compare_sha) { project.commit.sha } - let(:context) { instance_double(Gitlab::Ci::Build::Context::Base, sha: compare_sha) } + let_it_be(:user) { project.owner } - context 'when glob matches' do - let(:globs) { { paths: ['README.md'], compare_to: 'pages-deploy' } } + before_all do + project.repository.add_branch(user, 'feature_1', 'master') - it { is_expected.to be_truthy } + project.repository.create_file( + user, 'file1.txt', 'file 1', message: 'Create file1.txt', branch_name: 'feature_1' + ) + project.repository.add_tag(user, 'tag_1', 'feature_1') - context 'when the FF ci_rules_changes_compare is disabled' do - before do - stub_feature_flags(ci_rules_changes_compare: false) - end + project.repository.create_file( + user, 'file2.txt', 'file 2', message: 'Create file2.txt', branch_name: 'feature_1' + ) + project.repository.add_branch(user, 'feature_2', 'feature_1') - it { is_expected.to be_truthy } - end + project.repository.update_file( + user, 'file2.txt', 'file 2 updated', message: 'Update file2.txt', branch_name: 'feature_2' + ) end - context 'when glob does not match' do - let(:globs) { { paths: ['xyz.md'], compare_to: 'pages-deploy' } } + context 'when compare_to is branch or tag' do + using RSpec::Parameterized::TableSyntax + + where(:pipeline_ref, :compare_to, :paths, :ff, :result) do + 'feature_1' | 'master' | ['file1.txt'] | true | true + 'feature_1' | 'master' | ['README.md'] | true | false + 'feature_1' | 'master' | ['xyz.md'] | true | false + 'feature_2' | 'master' | ['file1.txt'] | true | true + 'feature_2' | 'master' | ['file2.txt'] | true | true + 'feature_2' | 'feature_1' | ['file1.txt'] | true | false + 'feature_2' | 'feature_1' | ['file1.txt'] | false | true + 'feature_2' | 'feature_1' | ['file2.txt'] | true | true + 'feature_1' | 'tag_1' | ['file1.txt'] | true | false + 'feature_1' | 'tag_1' | ['file1.txt'] | false | true + 'feature_1' | 'tag_1' | ['file2.txt'] | true | true + 'feature_2' | 'tag_1' | ['file2.txt'] | true | true + end - it { is_expected.to be_falsey } - end + with_them do + let(:globs) { { paths: paths, compare_to: compare_to } } - context 'when compare_to is a tag' do - let(:globs) { { paths: ['README.md'], compare_to: 'v1.1.1' } } + let(:pipeline) do + build(:ci_pipeline, project: project, ref: pipeline_ref, sha: project.commit(pipeline_ref).sha) + end - it { is_expected.to be_truthy } + before do + stub_feature_flags(ci_rules_changes_compare: ff) + end + + it { is_expected.to eq(result) } + end end context 'when compare_to is a sha' do - let(:globs) { { paths: ['README.md'], compare_to: '189a6c92' } } + let(:globs) { { paths: ['file2.txt'], compare_to: project.commit('tag_1').sha } } + + let(:pipeline) do + build(:ci_pipeline, project: project, ref: 'feature_2', sha: project.commit('feature_2').sha) + end it { is_expected.to be_truthy } end context 'when compare_to is not a valid ref' do - let(:globs) { { paths: ['README.md'], compare_to: 'xyz' } } + let(:globs) { { paths: ['file1.txt'], compare_to: 'xyz' } } + + let(:pipeline) do + build(:ci_pipeline, project: project, ref: 'feature_2', sha: project.commit('feature_2').sha) + end it 'raises ParseError' do expect { satisfied_by }.to raise_error( diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 52abb7655cf6e0..9e1fb46558e5bd 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -18,6 +18,7 @@ # rubocop:disable Metrics/ParameterLists def execute_service( source: :push, + before: '00000000', after: project.commit.id, ref: ref_name, trigger_request: nil, @@ -29,7 +30,7 @@ def execute_service( target_sha: nil, save_on_errors: true) params = { ref: ref, - before: '00000000', + before: before, after: after, variables_attributes: variables_attributes, push_options: push_options, @@ -2152,47 +2153,48 @@ def find_job(name) end end - context 'with workflow:rules and changes:paths and compare_to' do - let(:config) do - <<-EOY - workflow: - rules: - - changes: - paths: [README.md] - compare_to: #{compare_ref} + context 'with changes: paths and compare_to' do + before_all do + project.repository.add_branch(user, 'feature_1', 'master') - job1: - script: exit 0 - EOY + project.repository.create_file( + user, 'file1.txt', 'file 1', message: 'Create file1.txt', branch_name: 'feature_1' + ) + + project.repository.add_branch(user, 'feature_2', 'feature_1') + + project.repository.create_file( + user, 'file2.txt', 'file 2', message: 'Create file2.txt', branch_name: 'feature_2' + ) end - context 'when there is no such ref' do - let(:compare_ref) { 'invalid-branch' } + let(:changed_file) { 'file2.txt' } + let(:ref_name) { 'feature_2' } - it 'returns an error' do - expect(pipeline.errors.full_messages).to eq([ - 'Pipeline filtered out by workflow rules.' - ]) - end + let(:response) { execute_service(ref: ref_name, before: nil, after: project.commit(ref_name).sha) } - context 'when the FF ci_rules_changes_compare is not enabled' do - before do - stub_feature_flags(ci_rules_changes_compare: false) - end + context 'for jobs rules' do + let(:config) do + <<-EOY + job1: + script: exit 0 + rules: + - changes: + paths: [#{changed_file}] + compare_to: #{compare_to} - it 'ignores compare_to' do - expect(build_names).to be_empty - expect(pipeline).not_to be_created_successfully - end + job2: + script: exit 0 + EOY end - end - context 'when the ref exists' do - context 'when the rule matches' do - let(:compare_ref) { 'pages-deploy' } + context 'when there is no such compare_to ref' do + let(:compare_to) { 'invalid-branch' } - it 'creates job1' do - expect(build_names).to contain_exactly('job1') + it 'returns an error' do + expect(pipeline.errors.full_messages).to eq([ + 'Failed to parse rule for job1: rules:changes:compare_to is not a valid ref' + ]) end context 'when the FF ci_rules_changes_compare is not enabled' do @@ -2200,76 +2202,71 @@ def find_job(name) stub_feature_flags(ci_rules_changes_compare: false) end - it 'ignores compare_to' do - expect(pipeline).not_to be_created_successfully - expect(build_names).to be_empty + it 'ignores compare_to and changes is always true' do + expect(build_names).to contain_exactly('job1', 'job2') end end end - context 'when the rule does not match' do - let(:compare_ref) { 'feature' } - - it 'does not create job1' do - expect(pipeline).not_to be_created_successfully - expect(build_names).to be_empty - end + context 'when the compare_to ref exists' do + let(:compare_to) { 'feature_1'} - context 'when the FF ci_rules_changes_compare is not enabled' do - before do - stub_feature_flags(ci_rules_changes_compare: false) + context 'when the rule matches' do + it 'creates job1 and job2' do + expect(build_names).to contain_exactly('job1', 'job2') end - it 'ignores compare_to' do - expect(pipeline).not_to be_created_successfully - expect(build_names).to be_empty + context 'when the FF ci_rules_changes_compare is not enabled' do + before do + stub_feature_flags(ci_rules_changes_compare: false) + end + + it 'ignores compare_to and changes is always true' do + expect(build_names).to contain_exactly('job1', 'job2') + end end end - end - end - end - context 'with changes: paths and compare_to' do - let(:config) do - <<-EOY - job1: - script: exit 0 - rules: - - changes: - paths: [README.md] - compare_to: #{compare_ref} + context 'when the rule does not match' do + let(:changed_file) { 'file1.txt' } - job2: - script: exit 0 - EOY - end + it 'does not create job1' do + expect(build_names).to contain_exactly('job2') + end - context 'when there is no such ref' do - let(:compare_ref) { 'invalid-branch' } + context 'when the FF ci_rules_changes_compare is not enabled' do + before do + stub_feature_flags(ci_rules_changes_compare: false) + end - it 'returns an error' do - expect(pipeline.errors.full_messages).to eq([ - 'Failed to parse rule for job1: rules:changes:compare_to is not a valid ref' - ]) + it 'ignores compare_to and changes is always true' do + expect(build_names).to contain_exactly('job1', 'job2') + end + end + end end + end - context 'when the FF ci_rules_changes_compare is not enabled' do - before do - stub_feature_flags(ci_rules_changes_compare: false) - end + context 'for workflow rules' do + let(:config) do + <<-EOY + workflow: + rules: + - changes: + paths: [#{changed_file}] + compare_to: #{compare_to} - it 'ignores compare_to' do - expect(build_names).to contain_exactly('job2') - end + job1: + script: exit 0 + EOY end - end - context 'when the ref exists' do - context 'when the rule matches' do - let(:compare_ref) { 'pages-deploy' } + let(:compare_to) { 'feature_1'} - it 'creates job1 and job2' do - expect(build_names).to contain_exactly('job1', 'job2') + context 'when the rule matches' do + it 'creates job1' do + expect(pipeline).to be_created_successfully + expect(build_names).to contain_exactly('job1') end context 'when the FF ci_rules_changes_compare is not enabled' do @@ -2277,27 +2274,19 @@ def find_job(name) stub_feature_flags(ci_rules_changes_compare: false) end - it 'ignores compare_to' do - expect(build_names).to contain_exactly('job2') + it 'ignores compare_to and changes is always true' do + expect(pipeline).to be_created_successfully + expect(build_names).to contain_exactly('job1') end end end context 'when the rule does not match' do - let(:compare_ref) { 'feature' } + let(:changed_file) { 'file1.txt' } it 'does not create job1' do - expect(build_names).to contain_exactly('job2') - end - - context 'when the FF ci_rules_changes_compare is not enabled' do - before do - stub_feature_flags(ci_rules_changes_compare: false) - end - - it 'ignores compare_to' do - expect(build_names).to contain_exactly('job2') - end + expect(pipeline).not_to be_created_successfully + expect(build_names).to be_empty end end end -- GitLab