diff options
author | James Fargher <proglottis@gmail.com> | 2019-09-11 04:53:24 +0300 |
---|---|---|
committer | James Fargher <proglottis@gmail.com> | 2019-09-11 06:19:48 +0300 |
commit | 20f6f34f162584942dd3a31cd8168549078f5652 (patch) | |
tree | 6a921f740c338e24524d6345bb1ecc7a36774628 | |
parent | 895b6064f3568c306dda3180395b6589d1187828 (diff) |
Add file matching rule to flexible CI rulesjob_file_matching
This allows support for rules based on files in the repository:
```
job:
script:
- echo Dockerfile exists
rules:
- local:
- Dockerfile
```
-rw-r--r-- | changelogs/unreleased/job_file_matching.yml | 5 | ||||
-rw-r--r-- | lib/gitlab/ci/build/rules/rule/clause/local.rb | 54 | ||||
-rw-r--r-- | lib/gitlab/ci/config/entry/rules/rule.rb | 8 | ||||
-rw-r--r-- | spec/lib/gitlab/ci/build/rules/rule/clause/local_spec.rb | 30 | ||||
-rw-r--r-- | spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb | 32 |
5 files changed, 125 insertions, 4 deletions
diff --git a/changelogs/unreleased/job_file_matching.yml b/changelogs/unreleased/job_file_matching.yml new file mode 100644 index 00000000000..99723f76199 --- /dev/null +++ b/changelogs/unreleased/job_file_matching.yml @@ -0,0 +1,5 @@ +--- +title: Add file matching rule to flexible CI rules +merge_request: 32911 +author: +type: added diff --git a/lib/gitlab/ci/build/rules/rule/clause/local.rb b/lib/gitlab/ci/build/rules/rule/clause/local.rb new file mode 100644 index 00000000000..8865d1f2b33 --- /dev/null +++ b/lib/gitlab/ci/build/rules/rule/clause/local.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Build + class Rules::Rule::Clause::Local < Rules::Rule::Clause + MATCH_LIMIT = 10_000 + + def initialize(globs) + @globs = Array(globs) + end + + def satisfied_by?(pipeline, seed) + paths = if top_level_only? + pipeline.project.repository.tree(pipeline.sha).blobs.map(&:path) + else + pipeline.project.repository.ls_files(pipeline.sha) + end + + simple, complex = @globs.partition { |glob| simple_glob?(glob) } + + matches = 0 + simple.any? { |glob| paths.bsearch { |path| glob <=> path } } || + complex.any? do |glob| + paths.any? do |path| + matches += 1 + matches > MATCH_LIMIT || glob_match?(glob, path) + end + end + end + + private + + def glob_match?(glob, path) + File.fnmatch?(glob, path, File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB) + end + + def top_level_only? + @globs.all? { |glob| top_level_glob?(glob) } + end + + # matches glob patterns that only match files in the top level directory + def top_level_glob?(glob) + !glob.include?('/') && !glob.include?('**') + end + + # matches glob patterns that have no metacharacters for File#fnmatch? + def simple_glob?(glob) + !glob.include?('*') && !glob.include?('?') && !glob.include?('[') && !glob.include?('{') + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/rules/rule.rb b/lib/gitlab/ci/config/entry/rules/rule.rb index 1f2a34ec90e..3d5d0c7ae07 100644 --- a/lib/gitlab/ci/config/entry/rules/rule.rb +++ b/lib/gitlab/ci/config/entry/rules/rule.rb @@ -8,11 +8,11 @@ module Gitlab include ::Gitlab::Config::Entry::Validatable include ::Gitlab::Config::Entry::Attributable - CLAUSES = %i[if changes].freeze - ALLOWED_KEYS = %i[if changes when start_in].freeze + CLAUSES = %i[if changes local].freeze + ALLOWED_KEYS = %i[if changes local when start_in].freeze ALLOWED_WHEN = %w[on_success on_failure always never manual delayed].freeze - attributes :if, :changes, :when, :start_in + attributes :if, :changes, :local, :when, :start_in validations do validates :config, presence: true @@ -24,7 +24,7 @@ module Gitlab with_options allow_nil: true do validates :if, expression: true - validates :changes, array_of_strings: true + validates :changes, :local, array_of_strings: true validates :when, allowed_values: { in: ALLOWED_WHEN } end end diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/local_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/local_spec.rb new file mode 100644 index 00000000000..0b82373880a --- /dev/null +++ b/spec/lib/gitlab/ci/build/rules/rule/clause/local_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Build::Rules::Rule::Clause::Local do + describe 'satisfied_by?' do + using RSpec::Parameterized::TableSyntax + + where(:case_name, :globs, :files, :satisfied) do + 'simple top-level match' | ['Dockerfile'] | { 'Dockerfile' => '', 'Gemfile' => '' } | true + 'simple top-level no match' | ['Dockerfile'] | { 'Gemfile' => '' } | false + 'complex top-level match' | ['Docker*'] | { 'Dockerfile' => '', 'Gemfile' => '' } | true + 'complex top-level no match' | ['Docker*'] | { 'Gemfile' => '' } | false + 'simple nested match' | ['project/build.properties'] | { 'project/build.properties' => '' } | true + 'simple nested no match' | ['project/build.properties'] | { 'project/README.md' => '' } | false + 'complex nested match' | ['src/**/*.go'] | { 'src/gitlab.com/goproject/goproject.go' => '' } | true + 'complex nested no match' | ['src/**/*.go'] | { 'src/gitlab.com/goproject/README.md' => '' } | false + end + + with_them do + let(:project) { create(:project, :custom_repo, files: files) } + let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.head_commit.sha) } + subject { described_class.new(globs) } + + it 'checks if any files exist' do + expect(subject.satisfied_by?(pipeline, nil)).to eq(satisfied) + end + 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 c25344ec1a4..ba161d9c7b3 100644 --- a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb @@ -103,6 +103,32 @@ describe Gitlab::Ci::Config::Entry::Rules::Rule do end end + context 'when using a local: clause' do + let(:config) { { local: %w[app/ lib/ spec/ other/* paths/**/*.rb] } } + + it { is_expected.to be_valid } + end + + context 'when using a string as an invalid local: clause' do + let(:config) { { local: 'a regular string' } } + + it { is_expected.not_to be_valid } + + it 'reports an error about invalid policy' do + expect(subject.errors).to include(/should be an array of strings/) + end + end + + context 'when using a list as an invalid local: clause' do + let(:config) { { local: [1, 2] } } + + it { is_expected.not_to be_valid } + + it 'returns errors' do + expect(subject.errors).to include(/local should be an array of strings/) + end + end + context 'specifying a delayed job' do let(:config) { { if: '$THIS || $THAT', when: 'delayed', start_in: '15 minutes' } } @@ -198,6 +224,12 @@ describe Gitlab::Ci::Config::Entry::Rules::Rule do expect(entry.value).to eq(config) end end + + context 'when using a local: clause' do + let(:config) { { local: %w[app/ lib/ spec/ other/* paths/**/*.rb] } } + + it { is_expected.to eq(config) } + end end describe '.default' do |