diff --git a/lib/gitlab/ci/pipeline/seed/build/cache.rb b/lib/gitlab/ci/pipeline/seed/build/cache.rb index 7df84afe0f911b3ec127b4f087f1ed9399d7df51..f76683a662349d097212b0be0c4f6f39a791644e 100644 --- a/lib/gitlab/ci/pipeline/seed/build/cache.rb +++ b/lib/gitlab/ci/pipeline/seed/build/cache.rb @@ -62,9 +62,10 @@ def cache_key_digest end def hash_of_file_contents - return unless files.any? + expanded_paths = expand_file_patterns(files) + return unless expanded_paths.any? - blob_references = files.map { |path| [@pipeline.sha, path] } + blob_references = expanded_paths.map { |path| [@pipeline.sha, path] } content_hashes = @pipeline.project.repository .blobs_at(blob_references, blob_size_limit: 0) .map(&:id) @@ -90,6 +91,20 @@ def files_commits_files @key[:files_commits].to_a.select(&:present?).uniq end + def expand_file_patterns(patterns) + @expanded_patterns ||= begin + expanded = patterns.flat_map do |pattern| + if pattern.include?('*') + @pipeline.project.repository.search_files_by_wildcard_path(pattern, @pipeline.sha) + else + pattern + end + end.compact.uniq + + expanded.first(100) + end + end + def last_commit_id_for_path(path) @pipeline.project.repository.last_commit_id_for_path(@pipeline.sha, path) end diff --git a/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb index beee721c20c9c810fc62add33eb0ae0f5bd6e80d..696e999fef7a2007a7e251cfebc0764b9c25c3df 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb @@ -108,6 +108,20 @@ end end + context 'with only ./ relative paths' do + let(:config) do + { + key: { files: ['./VERSION'], prefix: 'relative' }, + paths: ['vendor/ruby'] + } + end + + it 'falls back to default key as relative paths are not supported' do + expect(subject[:key]).to eq('relative-default') + expect(subject[:paths]).to eq(['vendor/ruby']) + end + end + context 'with invalid file patterns' do where(:files, :expected_prefix) do [ @@ -144,6 +158,48 @@ end end end + + context 'with wildcard patterns' do + let(:config) do + { + key: { files: ['**/*.rb'], prefix: 'wildcard' }, + paths: ['vendor/ruby'] + } + end + + it 'expands wildcard patterns and builds cache key from matched files' do + expect(subject[:key]).to match(/^wildcard-[a-f0-9]+$/) + expect(subject[:paths]).to eq(['vendor/ruby']) + end + end + + context 'with mixed wildcard and non-wildcard patterns' do + let(:config) do + { + key: { files: ['VERSION', '*.gemspec'], prefix: 'mixed' }, + paths: ['vendor/ruby'] + } + end + + it 'expands wildcards and includes non-wildcard files' do + expect(subject[:key]).to match(/^mixed-[a-f0-9]+$/) + expect(subject[:paths]).to eq(['vendor/ruby']) + end + end + + context 'with wildcard pattern matching no files' do + let(:config) do + { + key: { files: ['**/nonexistent/*.txt'], prefix: 'empty' }, + paths: ['vendor/ruby'] + } + end + + it 'falls back to default key when no files match' do + expect(subject[:key]).to eq('empty-default') + expect(subject[:paths]).to eq(['vendor/ruby']) + end + end end context 'with cache:key:prefix' do