-
### OpenTofu CI/CD template
diff --git a/doc/user/project/file_lock.md b/doc/user/project/file_lock.md
index 2731473aa05..3674237c3fc 100644
--- a/doc/user/project/file_lock.md
+++ b/doc/user/project/file_lock.md
@@ -27,7 +27,7 @@ said to have "released the lock".
GitLab supports two different modes of file locking:
-- [Exclusive file locks](#exclusive-file-locks) for binary files: done
+- [Exclusive file locks](../../topics/git/file_management.md#file-locks) for binary files: done
**through the command line** with Git LFS and `.gitattributes`, it prevents locked
files from being modified on any branch.
- [Default branch locks](#default-branch-file-and-directory-locks): done
@@ -44,156 +44,6 @@ users are prevented from modifying locked files by pushing, merging,
or any other means, and are shown an error like:
`'.gitignore' is locked by @Administrator`.
-## Exclusive file locks
-
-This process allows you to lock single files or file extensions and it is
-done through the command line. It doesn't require GitLab paid subscriptions.
-
-Git LFS is well known for tracking files to reduce the storage of
-Git repositories, but it can also be used for [locking files](https://github.com/git-lfs/git-lfs/wiki/File-Locking).
-This is the method used for Exclusive File Locks.
-
-### Install Git LFS
-
-Before getting started, make sure you have [Git LFS installed](../../topics/git/lfs/index.md) in your computer. Open a terminal window and run:
-
-```shell
-git-lfs --version
-```
-
-If it doesn't recognize this command, you must install it. There are
-several [installation methods](https://git-lfs.com/) that you can
-choose according to your OS. To install it with Homebrew:
-
-```shell
-brew install git-lfs
-```
-
-Once installed, **open your local repository in a terminal window** and
-install Git LFS in your repository. If you're sure that LFS is already installed,
-you can skip this step. If you're unsure, re-installing it does no harm:
-
-```shell
-git lfs install
-```
-
-For more information, see [Git Large File Storage (LFS)](../../topics/git/lfs/index.md).
-
-### Configure Exclusive File Locks
-
-You need the Maintainer role
-Exclusive File Locks for your project through the command line.
-
-The first thing to do before using File Locking is to tell Git LFS which
-kind of files are lockable. The following command stores PNG files
-in LFS and flag them as lockable:
-
-```shell
-git lfs track "*.png" --lockable
-```
-
-After executing the above command a file named `.gitattributes` is
-created or updated with the following content:
-
-```shell
-*.png filter=lfs diff=lfs merge=lfs -text lockable
-```
-
-You can also register a file type as lockable without using LFS (to be able, for example,
-to lock/unlock a file you need in a remote server that
-implements the LFS File Locking API). To do that you can edit the
-`.gitattributes` file manually:
-
-```shell
-*.pdf lockable
-```
-
-The `.gitattributes` file is key to the process and **must**
-be pushed to the remote repository for the changes to take effect.
-
-After a file type has been registered as lockable, Git LFS makes
-them read-only on the file system automatically. This means you
-must **lock the file** before [editing it](#edit-lockable-files).
-
-### Lock files
-
-By locking a file, you verify that no one else is editing it, and
-prevent anyone else from editing the file until you're done. On the other
-hand, when you unlock a file, you communicate that you've finished editing
-and allow other people to edit it.
-
-To lock or unlock a file with Exclusive File Locking, open a terminal window
-in your repository directory and run the commands as described below.
-
-To **lock** a file:
-
-```shell
-git lfs lock path/to/file.png
-```
-
-To **unlock** a file:
-
-```shell
-git lfs unlock path/to/file.png
-```
-
-You can also unlock by file ID (given by LFS when you [view locked files](#view-exclusively-locked-files)):
-
-```shell
-git lfs unlock --id=123
-```
-
-If for some reason you need to unlock a file that was not locked by
-yourself, you can use the `--force` flag as long as you have **Maintainer**
-permissions to the project:
-
-```shell
-git lfs unlock --id=123 --force
-```
-
-You can push files to GitLab whether they're locked or unlocked.
-
-NOTE:
-Although multi-branch file locks can be created and managed through the Git LFS
-command-line interface, file locks can be created for any file.
-
-### View exclusively-locked files
-
-To list all the files locked with LFS locally, open a terminal window in your
-repository and run:
-
-```shell
-git lfs locks
-```
-
-The output lists the locked files followed by the user who locked each of them
-and the files' IDs.
-
-On the repository file tree, GitLab displays an LFS badge for files
-tracked by Git LFS plus a padlock icon on exclusively-locked files:
-
-
-
-You can also [view and remove existing locks](#view-and-remove-existing-locks) from the GitLab UI.
-
-NOTE:
-When you rename an exclusively-locked file, the lock is lost. You must
-lock it again to keep it locked.
-
-### Edit lockable files
-
-After the file is [configured as lockable](#configure-exclusive-file-locks), it is set to read-only.
-Therefore, you need to lock it before editing it.
-
-Suggested workflow for shared projects:
-
-1. Lock the file.
-1. Edit the file.
-1. Commit your changes.
-1. Push to the repository.
-1. Get your changes reviewed, approved, and merged.
-1. Unlock the file.
-
## Default branch file and directory locks
DETAILS:
@@ -234,3 +84,8 @@ This list shows all the files locked either through LFS or GitLab UI.
Locks can be removed by their author, or any user
with at least the Maintainer role.
+
+## Related topics
+
+- [File management with Git](../../topics/git/file_management.md)
+- [File locks](../../topics/git/file_management.md#file-locks)
diff --git a/doc/user/project/repository/files/git_blame.md b/doc/user/project/repository/files/git_blame.md
index 5033a610387..49b7a63c5d6 100644
--- a/doc/user/project/repository/files/git_blame.md
+++ b/doc/user/project/repository/files/git_blame.md
@@ -55,35 +55,8 @@ To see earlier revisions of a specific line:
1. Select **View blame prior to this change** (**{doc-versions}**)
until you've found the changes you're interested in viewing.
-## Associated `git` command
-
-If you're running `git` from the command line, the equivalent command is
-`git blame `. For example, if you want to find `blame` information
-about a `README.md` file in the local directory:
-
-1. Run this command `git blame README.md`.
-1. If the line you want to see is not in the first page of results, press Space
- until you find the line you want.
-1. To exit out of the results, press Q.
-
-The `git blame` output in the CLI looks like this:
-
-```shell
-58233c4f1054c (Dan Rhodes 2022-05-13 07:02:20 +0000 1) ## Contributor License Agreement
-b87768f435185 (Jamie Hurewitz 2017-10-31 18:09:23 +0000 2)
-8e4c7f26317ff (Brett Walker 2023-10-20 17:53:25 +0000 3) Contributions to this repository are subject to the
-58233c4f1054c (Dan Rhodes 2022-05-13 07:02:20 +0000 4)
-```
-
-The output includes:
-
-- The SHA of the commit.
-- The name of the committer.
-- The date and time in UTC format.
-- The line number.
-- The contents of the line.
-
## Related topics
- [Git file blame REST API](../../../../api/repository_files.md#get-file-blame-from-repository)
- [Common Git commands](../../../../topics/git/commands.md)
+- [File management with Git](../../../../topics/git/file_management.md)
diff --git a/doc/user/project/repository/files/git_history.md b/doc/user/project/repository/files/git_history.md
index bb55b1ace83..70dc5074a30 100644
--- a/doc/user/project/repository/files/git_history.md
+++ b/doc/user/project/repository/files/git_history.md
@@ -31,7 +31,7 @@ GitLab retrieves the user name and email information from the
[Git configuration](https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration)
of the contributor when the user creates a commit.
-## View a file's Git history in the UI
+## View a file's Git history
To see a file's Git history in the UI:
@@ -40,34 +40,11 @@ To see a file's Git history in the UI:
1. Go to your desired file in the repository.
1. In the upper-right corner, select **History**.
-## In the CLI
-
-To see the history of a file from the command line, use the `git log ` command.
-For example, to see `history` information about the `CONTRIBUTING.md` file in the root
-of the `gitlab` repository, run this command:
-
-```shell
-$ git log CONTRIBUTING.md
-
-commit b350bf041666964c27834885e4590d90ad0bfe90
-Author: Nick Malcolm
-Date: Fri Dec 8 13:43:07 2023 +1300
-
- Update security contact and vulnerability disclosure info
-
-commit 8e4c7f26317ff4689610bf9d031b4931aef54086
-Author: Brett Walker
-Date: Fri Oct 20 17:53:25 2023 +0000
-
- Fix link to Code of Conduct
-
- and condense some of the verbiage
-```
-
## Related topics
- [Git blame](git_blame.md) for line-by-line information about a file
- [Common Git commands](../../../../topics/git/commands.md)
+- [File management with Git](../../../../topics/git/file_management.md)
## Troubleshooting
diff --git a/doc/user/project/repository/files/index.md b/doc/user/project/repository/files/index.md
index 43566caeb7e..06b742f089b 100644
--- a/doc/user/project/repository/files/index.md
+++ b/doc/user/project/repository/files/index.md
@@ -130,6 +130,7 @@ To change the default handling of a file or file type, create a
## Related topics
- [Repository files API](../../../../api/repository_files.md)
+- [File management with Git](../../../../topics/git/file_management.md)
## Troubleshooting
diff --git a/eslint.config.mjs b/eslint.config.mjs
index cce050a9ffe..4b018b7f4d9 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -30,8 +30,9 @@ const extendConfigs = [
// rewrite.
let jhConfigs = [];
if (existsSync(path.resolve(dirname, 'jh'))) {
- // eslint-disable-next-line import/no-unresolved, import/extensions
- jhConfigs = (await import('jh/eslint.config.js')).default;
+ const pathToJhConfig = path.resolve(dirname, 'jh/eslint.config.js')
+ // eslint-disable-next-line import/no-dynamic-require, no-unsanitized/method
+ jhConfigs = (await import(pathToJhConfig)).default;
}
const jestConfig = {
diff --git a/keeps/helpers/file_helper.rb b/keeps/helpers/file_helper.rb
index d6e57e196fa..c195a7cf020 100644
--- a/keeps/helpers/file_helper.rb
+++ b/keeps/helpers/file_helper.rb
@@ -11,6 +11,17 @@ module Keeps
@rewriter = Parser::Source::TreeRewriter.new(@source.buffer)
end
+ class << self
+ # Define a node matcher method in the +RuboCop::AST::Node+, which all other node types inherits from.
+ def def_node_matcher(method_name, pattern)
+ RuboCop::AST::NodePattern.new(pattern).def_node_matcher(RuboCop::AST::Node, method_name)
+
+ define_method method_name do
+ source.ast.public_send(method_name) # rubocop:disable GitlabSecurity/PublicSend -- it's used to evaluate the node matcher at instance level
+ end
+ end
+ end
+
def replace_method_content(method_name, content, strip_comments_from_file: false)
method = source.ast.each_node(:class).first.each_node(:def).find do |child|
child.method_name == method_name.to_sym
@@ -25,6 +36,12 @@ module Keeps
process
end
+ def replace_as_string(node, content)
+ rewriter.replace(node.loc.expression, content)
+
+ process
+ end
+
private
attr_reader :file, :source, :rewriter, :corrector
@@ -52,7 +69,7 @@ module Keeps
end
def process
- @process ||= rewriter.process.lstrip.gsub(/\n{3,}/, "\n\n")
+ rewriter.process.lstrip.gsub(/\n{3,}/, "\n\n")
end
end
end
diff --git a/keeps/update_workers_data_consistency.rb b/keeps/update_workers_data_consistency.rb
new file mode 100644
index 00000000000..843462fa751
--- /dev/null
+++ b/keeps/update_workers_data_consistency.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+
+require_relative '../rubocop/cop_todo'
+
+module Keeps
+ # This is an implementation of ::Gitlab::Housekeeper::Keep.
+ # This changes workers which have `data_consistency: :always` to `:sticky`.
+ #
+ # You can run it individually with:
+ #
+ # ```
+ # bundle exec gitlab-housekeeper -d -k Keeps::UpdateWorkersDataConsistency
+ # ```
+ class UpdateWorkersDataConsistency < ::Gitlab::Housekeeper::Keep
+ WORKER_REGEX = %r{app/workers/(.+).rb}
+ WORKERS_DATA_CONSISTENCY_PATH = '.rubocop_todo/sidekiq_load_balancing/worker_data_consistency.yml'
+ FALLBACK_FEATURE_CATEGORY = 'database'
+ LIMIT_TO = 5
+
+ def initialize(...)
+ ::Keeps::Helpers::FileHelper.def_node_matcher :data_consistency_node, <<~PATTERN
+ `(send nil? :data_consistency $(sym _) ...)
+ PATTERN
+
+ super
+ end
+
+ def each_change
+ workers_by_feature_category.deep_dup.each do |feature_category, workers|
+ remove_workers_from_list(workers.pluck(:path)) # rubocop:disable CodeReuse/ActiveRecord -- small dataset
+
+ workers.each do |worker|
+ file_helper = ::Keeps::Helpers::FileHelper.new(worker[:path])
+ node = file_helper.data_consistency_node
+ File.write(worker[:path], file_helper.replace_as_string(node, ':sticky'))
+ end
+
+ yield(build_change(feature_category, workers))
+ end
+ end
+
+ private
+
+ def workers_by_feature_category
+ worker_paths.each_with_object(Hash.new { |h, k| h[k] = [] }) do |worker_path, group|
+ next unless File.read(worker_path, mode: 'rb').include?('data_consistency :always')
+
+ worker_name = worker_path.match(WORKER_REGEX)[1].camelize
+
+ feature_category = worker_feature_category(worker_name)
+
+ next if group[feature_category].size >= LIMIT_TO
+
+ group[feature_category] << { path: worker_path, name: worker_name }
+ end
+ end
+
+ def build_change(feature_category, workers)
+ change = ::Gitlab::Housekeeper::Change.new
+ change.title = "Change data consistency for workers maintained by #{feature_category}".truncate(70, omission: '')
+ change.identifiers = workers.map { |worker| worker[:name].to_s }.prepend(feature_category)
+ change.labels = labels(feature_category)
+ change.reviewers = pick_reviewers(feature_category, change.identifiers)
+ change.changed_files = workers.pluck(:path).prepend(WORKERS_DATA_CONSISTENCY_PATH) # rubocop:disable CodeReuse/ActiveRecord -- small dataset
+
+ change.description = <<~MARKDOWN.chomp
+ ## What does this MR
+
+ It updates workers data consistency from `:always` to `:sticky` for workers maintained by `#{feature_category}`,
+ as a way to reduce database reads on the primary DB. Check https://gitlab.com/gitlab-org/gitlab/-/issues/462611.
+
+ To reduce resource saturation on the primary node, all workers should be changed to `sticky`, at minimum.
+
+ Since jobs are now enqueued along with the current database LSN, the replica (for `:sticky` or `:delayed`)
+ is guaranteed to be caught up to that point, or the job will be retried, or use the primary. Consider updating
+ the worker(s) to `delayed`, if it's applicable.
+
+ You can read more about the Sidekiq Workers `data_consistency` in
+ https://docs.gitlab.com/ee/development/sidekiq/worker_attributes.html#job-data-consistency-strategies.
+
+ You can use this [dashboard](https://log.gprd.gitlab.net/app/r/s/iyIUV) to monitor the worker query activity on
+ primary vs. replicas.
+
+ Currently, the `gitlab-housekeeper` is not always capable of updating all references, so you must check the diff
+ and pipeline failures to confirm if there are any issues.
+ MARKDOWN
+
+ change
+ end
+
+ def labels(feature_category)
+ group_labels = groups_helper.labels_for_feature_category(feature_category)
+
+ group_labels + %w[maintenance::scalability type::maintenance severity::3 priority::1]
+ end
+
+ def pick_reviewers(feature_category, identifiers)
+ groups_helper.pick_reviewer_for_feature_category(
+ feature_category,
+ identifiers,
+ fallback_feature_category: 'database'
+ )
+ end
+
+ def worker_feature_category(worker_name)
+ feature_category = workers_meta.find { |entry| entry[:worker_name].to_s == worker_name.to_s } || {}
+
+ feature_category.fetch(:feature_category, FALLBACK_FEATURE_CATEGORY).to_s
+ end
+
+ def workers_meta
+ @workers_meta ||= Gitlab::SidekiqConfig::QUEUE_CONFIG_PATHS.flat_map do |yaml_file|
+ YAML.safe_load_file(yaml_file, permitted_classes: [Symbol])
+ end
+ end
+
+ def remove_workers_from_list(paths_to_remove)
+ todo_helper = RuboCop::CopTodo.new('SidekiqLoadBalancing/WorkerDataConsistency')
+ todo_helper.add_files(worker_paths - paths_to_remove)
+
+ File.write(WORKERS_DATA_CONSISTENCY_PATH, todo_helper.to_yaml)
+ end
+
+ def worker_paths
+ @worker_paths ||= YAML.safe_load_file(WORKERS_DATA_CONSISTENCY_PATH).dig(
+ 'SidekiqLoadBalancing/WorkerDataConsistency',
+ 'Exclude'
+ )
+ end
+
+ def groups_helper
+ @groups_helper ||= ::Keeps::Helpers::Groups.new
+ end
+ end
+end
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index 7e6b21d6121..ad59429faa2 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -6,7 +6,7 @@ module API
before { authenticate! }
- feature_category :team_planning
+ feature_category :notifications
urgency :low
ISSUABLE_TYPES = {
diff --git a/lib/gitlab/ci/build/context/base.rb b/lib/gitlab/ci/build/context/base.rb
index 10e90e0348c..86f4aad933e 100644
--- a/lib/gitlab/ci/build/context/base.rb
+++ b/lib/gitlab/ci/build/context/base.rb
@@ -23,6 +23,12 @@ module Gitlab
end
end
+ def variables_hash_expanded
+ strong_memoize(:variables_hash_expanded) do
+ variables.sort_and_expand_all.to_hash
+ end
+ end
+
def project
pipeline.project
end
diff --git a/lib/gitlab/ci/build/rules/rule/clause/changes.rb b/lib/gitlab/ci/build/rules/rule/clause/changes.rb
index a65dd857f1a..bb4600bc1cc 100644
--- a/lib/gitlab/ci/build/rules/rule/clause/changes.rb
+++ b/lib/gitlab/ci/build/rules/rule/clause/changes.rb
@@ -17,7 +17,7 @@ module Gitlab
return true unless modified_paths
return false if modified_paths.empty?
- expanded_globs = expand_globs(context).uniq
+ expanded_globs = expand_globs(context, pipeline).uniq
return false if expanded_globs.empty?
cache_key = [
@@ -43,11 +43,17 @@ module Gitlab
end
end
- def expand_globs(context)
+ def expand_globs(context, pipeline)
return paths unless context
- paths.map do |glob|
- ExpandVariables.expand_existing(glob, -> { context.variables_hash })
+ if Feature.enabled?(:expand_nested_variables_in_job_rules_exists_and_changes, pipeline.project)
+ paths.map do |glob|
+ expand_value_nested(glob, context)
+ end
+ else
+ paths.map do |glob|
+ expand_value(glob, context)
+ end
end
end
@@ -70,12 +76,25 @@ module Gitlab
def find_compare_to_sha(pipeline, context)
return unless @globs.include?(:compare_to)
- compare_to = ExpandVariables.expand(@globs[:compare_to], -> { context.variables_hash })
+ compare_to = if Feature.enabled?(:expand_nested_variables_in_job_rules_exists_and_changes, pipeline.project)
+ expand_value_nested(@globs[:compare_to], context)
+ else
+ expand_value(@globs[:compare_to], context)
+ end
+
commit = pipeline.project.commit(compare_to)
raise Rules::Rule::Clause::ParseError, 'rules:changes:compare_to is not a valid ref' unless commit
commit.sha
end
+
+ def expand_value(value, context)
+ ExpandVariables.expand_existing(value, -> { context.variables_hash })
+ end
+
+ def expand_value_nested(value, context)
+ ExpandVariables.expand_existing(value, -> { context.variables_hash_expanded })
+ end
end
end
end
diff --git a/lib/gitlab/ci/build/rules/rule/clause/exists.rb b/lib/gitlab/ci/build/rules/rule/clause/exists.rb
index dcb368e145e..4501d822359 100644
--- a/lib/gitlab/ci/build/rules/rule/clause/exists.rb
+++ b/lib/gitlab/ci/build/rules/rule/clause/exists.rb
@@ -18,13 +18,13 @@ module Gitlab
@ref = clause[:ref]
end
- def satisfied_by?(_pipeline, context)
+ def satisfied_by?(pipeline, context)
# Return early to avoid redundant Gitaly calls
return false unless @globs.any?
- context = change_context(context) if @project_path
+ context = change_context(context, pipeline) if @project_path
- expanded_globs = expand_globs(context)
+ expanded_globs = expand_globs(context, pipeline)
top_level_only = expanded_globs.all?(&method(:top_level_glob?))
paths = worktree_paths(context, top_level_only)
@@ -42,9 +42,15 @@ module Gitlab
grouped.values_at(:exact, :extension, :pattern).map { |globs| Array(globs) }
end
- def expand_globs(context)
- @globs.map do |glob|
- expand_value(glob, context)
+ def expand_globs(context, pipeline)
+ if Feature.enabled?(:expand_nested_variables_in_job_rules_exists_and_changes, pipeline&.project)
+ @globs.map do |glob|
+ expand_value_nested(glob, context)
+ end
+ else
+ @globs.map do |glob|
+ expand_value(glob, context)
+ end
end
end
@@ -121,10 +127,10 @@ module Gitlab
glob.delete_prefix(WILDCARD_NESTED_PATTERN)
end
- def change_context(old_context)
+ def change_context(old_context, pipeline)
user = find_context_user(old_context)
- new_project = find_context_project(user, old_context)
- new_sha = find_context_sha(new_project, old_context)
+ new_project = find_context_project(user, old_context, pipeline)
+ new_sha = find_context_sha(new_project, old_context, pipeline)
Gitlab::Ci::Config::External::Context.new(
project: new_project,
@@ -138,8 +144,13 @@ module Gitlab
context.is_a?(Gitlab::Ci::Config::External::Context) ? context.user : context.pipeline.user
end
- def find_context_project(user, context)
- full_path = expand_value(@project_path, context)
+ def find_context_project(user, context, pipeline)
+ full_path = if Feature.enabled?(:expand_nested_variables_in_job_rules_exists_and_changes, pipeline.project)
+ expand_value_nested(@project_path, context)
+ else
+ expand_value(@project_path, context)
+ end
+
project = Project.find_by_full_path(full_path)
unless project
@@ -156,10 +167,16 @@ module Gitlab
project
end
- def find_context_sha(project, context)
+ def find_context_sha(project, context, pipeline)
return project.commit&.sha unless @ref
- ref = expand_value(@ref, context)
+ ref = if Feature.enabled?(:expand_nested_variables_in_job_rules_exists_and_changes,
+ pipeline.project)
+ expand_value_nested(@ref, context)
+ else
+ expand_value(@ref, context)
+ end
+
commit = project.commit(ref)
unless commit
@@ -184,6 +201,10 @@ module Gitlab
def expand_value(value, context)
ExpandVariables.expand_existing(value, -> { context.variables_hash })
end
+
+ def expand_value_nested(value, context)
+ ExpandVariables.expand_existing(value, -> { context.variables_hash_expanded })
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/external/context.rb b/lib/gitlab/ci/config/external/context.rb
index 00f1c0a4c09..61f93ee2353 100644
--- a/lib/gitlab/ci/config/external/context.rb
+++ b/lib/gitlab/ci/config/external/context.rb
@@ -61,6 +61,12 @@ module Gitlab
end
end
+ def variables_hash_expanded
+ strong_memoize(:variables_hash_expanded) do
+ variables.sort_and_expand_all.to_hash
+ end
+ end
+
def mutate(attrs = {})
self.class.new(**attrs) do |ctx|
ctx.pipeline = pipeline
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 41194841ba6..824fa153c0c 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -57513,6 +57513,12 @@ msgid_plural "Todos|Marked %d to-dos as done"
msgstr[0] ""
msgstr[1] ""
+msgid "Todos|Marked as done"
+msgstr ""
+
+msgid "Todos|Marked as undone"
+msgstr ""
+
msgid "Todos|Member access request"
msgstr ""
diff --git a/scripts/cells/application-settings-analysis.rb b/scripts/cells/application-settings-analysis.rb
index 3dbc66d09d5..daa33bc1392 100755
--- a/scripts/cells/application-settings-analysis.rb
+++ b/scripts/cells/application-settings-analysis.rb
@@ -145,6 +145,7 @@ class ApplicationSettingsAnalysis
help_page_support_url
help_page_text
home_page_url
+ identity_verification_settings
import_sources
importers
invisible_captcha_enabled
@@ -217,6 +218,7 @@ class ApplicationSettingsAnalysis
shared_runners_minutes
shared_runners_text
sidekiq_job_limiter_limit_bytes
+ sign_in_restrictions
signup_enabled
silent_admin_exports_enabled
slack_app_enabled
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 316b9842c6d..59fe8fff32d 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -612,47 +612,6 @@ RSpec.describe Projects::PipelinesController, feature_category: :continuous_inte
end
end
- describe 'GET dag' do
- let(:pipeline) { create(:ci_pipeline, project: project) }
-
- it_behaves_like 'the show page', 'dag'
- end
-
- describe 'GET dag.json' do
- let(:pipeline) { create(:ci_pipeline, project: project) }
- let(:build_stage) { create(:ci_stage, name: 'build', pipeline: pipeline) }
- let(:test_stage) { create(:ci_stage, name: 'test', pipeline: pipeline) }
-
- before do
- create_build(build_stage, 1, 'build')
- create_build(test_stage, 2, 'test', scheduling_type: 'dag').tap do |job|
- create(:ci_build_need, build: job, name: 'build')
- end
- end
-
- it 'returns the pipeline with DAG serialization' do
- get :dag, params: { namespace_id: project.namespace, project_id: project, id: pipeline }, format: :json
-
- expect(response).to have_gitlab_http_status(:ok)
-
- expect(json_response.fetch('stages')).not_to be_empty
-
- build_stage = json_response['stages'].first
- expect(build_stage.fetch('name')).to eq 'build'
- expect(build_stage.fetch('groups').first.fetch('jobs'))
- .to eq [{ 'name' => 'build', 'scheduling_type' => 'stage' }]
-
- test_stage = json_response['stages'].last
- expect(test_stage.fetch('name')).to eq 'test'
- expect(test_stage.fetch('groups').first.fetch('jobs'))
- .to eq [{ 'name' => 'test', 'scheduling_type' => 'dag', 'needs' => ['build'] }]
- end
-
- def create_build(stage, stage_idx, name, params = {})
- create(:ci_build, pipeline: pipeline, ci_stage: stage, stage_idx: stage_idx, name: name, **params)
- end
- end
-
describe 'GET builds' do
let(:pipeline) { create(:ci_pipeline, project: project) }
diff --git a/spec/features/dashboard/todos/todos_filtering_spec.rb b/spec/features/dashboard/todos/todos_filtering_spec.rb
index d6baff5ed4a..d0d83125225 100644
--- a/spec/features/dashboard/todos/todos_filtering_spec.rb
+++ b/spec/features/dashboard/todos/todos_filtering_spec.rb
@@ -4,7 +4,7 @@
require 'spec_helper'
-RSpec.describe 'Dashboard > User filters todos', :js, feature_category: :team_planning do
+RSpec.describe 'Dashboard > User filters todos', :js, feature_category: :notifications do
let(:user_1) { create(:user, username: 'user_1', name: 'user_1') }
let(:user_2) { create(:user, username: 'user_2', name: 'user_2') }
diff --git a/spec/features/dashboard/todos/todos_sorting_spec.rb b/spec/features/dashboard/todos/todos_sorting_spec.rb
index 18c0626c1f9..183c5fb804e 100644
--- a/spec/features/dashboard/todos/todos_sorting_spec.rb
+++ b/spec/features/dashboard/todos/todos_sorting_spec.rb
@@ -4,7 +4,7 @@
require 'spec_helper'
-RSpec.describe 'Dashboard > User sorts todos', feature_category: :team_planning do
+RSpec.describe 'Dashboard > User sorts todos', feature_category: :notifications do
let(:user) { create(:user) }
let(:project) { create(:project) }
diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb
index 8f8955e11ef..d7103169a87 100644
--- a/spec/features/dashboard/todos/todos_spec.rb
+++ b/spec/features/dashboard/todos/todos_spec.rb
@@ -4,7 +4,7 @@
require 'spec_helper'
-RSpec.describe 'Dashboard Todos', :js, feature_category: :team_planning do
+RSpec.describe 'Dashboard Todos', :js, feature_category: :notifications do
include DesignManagementTestHelpers
let_it_be(:user) { create(:user, username: 'john') }
diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb
index 251e7584f31..fdc604e12ba 100644
--- a/spec/features/issues/todo_spec.rb
+++ b/spec/features/issues/todo_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Manually create a todo item from issue', :js, feature_category: :team_planning do
+RSpec.describe 'Manually create a todo item from issue', :js, feature_category: :notifications do
let!(:project) { create(:project) }
let!(:issue) { create(:issue, project: project) }
let!(:user) { create(:user) }
diff --git a/spec/features/projects/active_tabs_spec.rb b/spec/features/projects/active_tabs_spec.rb
index f9995740724..a80a89bdc9c 100644
--- a/spec/features/projects/active_tabs_spec.rb
+++ b/spec/features/projects/active_tabs_spec.rb
@@ -168,15 +168,6 @@ RSpec.describe 'Project active tab', :js, feature_category: :groups_and_projects
it_behaves_like 'page has active sub tab', _('Pipelines')
end
- context 'Needs tab' do
- before do
- visit dag_project_pipeline_path(project, pipeline)
- end
-
- it_behaves_like 'page has active tab', _('Build')
- it_behaves_like 'page has active sub tab', _('Pipelines')
- end
-
context 'Builds tab' do
before do
visit builds_project_pipeline_path(project, pipeline)
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 6b791f5a1c1..300516bb0ee 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -1220,25 +1220,6 @@ RSpec.describe 'Pipeline', :js, feature_category: :continuous_integration do
end
end
- describe 'GET /:project/-/pipelines/:id/dag' do
- include_context 'pipeline builds'
-
- let_it_be(:project) { create(:project, :repository) }
-
- let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) }
-
- before do
- visit dag_project_pipeline_path(project, pipeline)
- end
-
- context 'page tabs' do
- it 'shows Pipeline and Jobs tabs with link' do
- expect(page).to have_link('Pipeline')
- expect(page).to have_link('Jobs')
- end
- end
- end
-
context 'when user sees pipeline flags in a pipeline detail page' do
let_it_be(:project) { create(:project, :repository) }
diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb
index 9ca8f83a82d..72c6f4d2336 100644
--- a/spec/finders/todos_finder_spec.rb
+++ b/spec/finders/todos_finder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe TodosFinder, feature_category: :team_planning do
+RSpec.describe TodosFinder, feature_category: :notifications do
describe '#execute' do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
diff --git a/spec/frontend/todos/components/todo_item_actions_spec.js b/spec/frontend/todos/components/todo_item_actions_spec.js
new file mode 100644
index 00000000000..a832fc30dc0
--- /dev/null
+++ b/spec/frontend/todos/components/todo_item_actions_spec.js
@@ -0,0 +1,63 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import TodoItemActions from '~/todos/components/todo_item_actions.vue';
+import { TODO_STATE_DONE, TODO_STATE_PENDING } from '~/todos/constants';
+
+describe('TodoItemActions', () => {
+ let wrapper;
+ const mockTodo = {
+ id: 'gid://gitlab/Todo/1',
+ state: TODO_STATE_PENDING,
+ };
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(TodoItemActions, {
+ propsData: {
+ todo: mockTodo,
+ ...props,
+ },
+ provide: {
+ currentTab: 0,
+ },
+ });
+ };
+
+ it('sets correct icon for pending todo action button', () => {
+ createComponent();
+ expect(wrapper.findComponent(GlButton).props('icon')).toBe('check');
+ });
+
+ it('sets correct icon for done todo action button', () => {
+ createComponent({ todo: { ...mockTodo, state: TODO_STATE_DONE } });
+ expect(wrapper.findComponent(GlButton).props('icon')).toBe('redo');
+ });
+
+ it('sets correct aria-label for pending todo', () => {
+ createComponent();
+ expect(wrapper.findComponent(GlButton).attributes('aria-label')).toBe('Mark as done');
+ });
+
+ it('sets correct aria-label for done todo', () => {
+ createComponent({ todo: { ...mockTodo, state: TODO_STATE_DONE } });
+ expect(wrapper.findComponent(GlButton).attributes('aria-label')).toBe('Undo');
+ });
+
+ describe('tooltipTitle', () => {
+ it('returns null when isLoading is true', () => {
+ createComponent();
+ // eslint-disable-next-line no-restricted-syntax
+ wrapper.setData({ isLoading: true });
+ expect(wrapper.vm.tooltipTitle).toBeNull();
+ });
+
+ it('returns "Mark as done" for pending todo', () => {
+ createComponent();
+ expect(wrapper.vm.tooltipTitle).toBe('Mark as done');
+ });
+
+ it('returns "Undo" for done todo', () => {
+ createComponent({ todo: { ...mockTodo, state: TODO_STATE_DONE } });
+ expect(wrapper.vm.tooltipTitle).toBe('Undo');
+ });
+ });
+});
diff --git a/spec/frontend/todos/components/todo_item_body_spec.js b/spec/frontend/todos/components/todo_item_body_spec.js
index 8c2734ec060..8150e9121b1 100644
--- a/spec/frontend/todos/components/todo_item_body_spec.js
+++ b/spec/frontend/todos/components/todo_item_body_spec.js
@@ -1,4 +1,3 @@
-// write jest specs in this file, for the component in the todo_item_body.vue file
import { shallowMount } from '@vue/test-utils';
import { GlLink, GlAvatar, GlAvatarLink } from '@gitlab/ui';
import TodoItemBody from '~/todos/components/todo_item_body.vue';
diff --git a/spec/frontend/todos/components/todo_item_spec.js b/spec/frontend/todos/components/todo_item_spec.js
new file mode 100644
index 00000000000..2835f4eda6f
--- /dev/null
+++ b/spec/frontend/todos/components/todo_item_spec.js
@@ -0,0 +1,70 @@
+import { shallowMount } from '@vue/test-utils';
+import TodoItem from '~/todos/components/todo_item.vue';
+import TodoItemTitle from '~/todos/components/todo_item_title.vue';
+import TodoItemBody from '~/todos/components/todo_item_body.vue';
+import TodoItemTimestamp from '~/todos/components/todo_item_timestamp.vue';
+import TodoItemActions from '~/todos/components/todo_item_actions.vue';
+import { TODO_STATE_DONE, TODO_STATE_PENDING } from '~/todos/constants';
+
+describe('TodoItem', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(TodoItem, {
+ propsData: {
+ currentUserId: '1',
+ todo: {
+ id: '1',
+ state: TODO_STATE_PENDING,
+ targetType: 'Issue',
+ targetUrl: '/project/issue/1',
+ },
+ ...props,
+ },
+ });
+ };
+
+ it('renders the component', () => {
+ createComponent();
+ expect(wrapper.exists()).toBe(true);
+ });
+
+ it('renders TodoItemTitle component', () => {
+ createComponent();
+ expect(wrapper.findComponent(TodoItemTitle).exists()).toBe(true);
+ });
+
+ it('renders TodoItemBody component', () => {
+ createComponent();
+ expect(wrapper.findComponent(TodoItemBody).exists()).toBe(true);
+ });
+
+ it('renders TodoItemTimestamp component', () => {
+ createComponent();
+ expect(wrapper.findComponent(TodoItemTimestamp).exists()).toBe(true);
+ });
+
+ it('renders TodoItemActions component', () => {
+ createComponent();
+ expect(wrapper.findComponent(TodoItemActions).exists()).toBe(true);
+ });
+
+ describe('computed properties', () => {
+ it('isDone returns true when todo state is done', () => {
+ createComponent({ todo: { state: TODO_STATE_DONE } });
+ expect(wrapper.vm.isDone).toBe(true);
+ });
+
+ it('isPending returns true when todo state is pending', () => {
+ createComponent({ todo: { state: TODO_STATE_PENDING } });
+ expect(wrapper.vm.isPending).toBe(true);
+ });
+ });
+
+ it('emits change event when TodoItemActions emits change', async () => {
+ createComponent();
+ const todoItemActions = wrapper.findComponent(TodoItemActions);
+ await todoItemActions.vm.$emit('change', '1', true);
+ expect(wrapper.emitted('change')).toEqual([['1', true]]);
+ });
+});
diff --git a/spec/graphql/resolvers/todos_resolver_spec.rb b/spec/graphql/resolvers/todos_resolver_spec.rb
index 7a2756f9544..7e0798a5cef 100644
--- a/spec/graphql/resolvers/todos_resolver_spec.rb
+++ b/spec/graphql/resolvers/todos_resolver_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Resolvers::TodosResolver, feature_category: :team_planning do
+RSpec.describe Resolvers::TodosResolver, feature_category: :notifications do
include GraphqlHelpers
include DesignManagementTestHelpers
diff --git a/spec/graphql/types/todo_type_spec.rb b/spec/graphql/types/todo_type_spec.rb
index a99d62f9593..8363e19354e 100644
--- a/spec/graphql/types/todo_type_spec.rb
+++ b/spec/graphql/types/todo_type_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe GitlabSchema.types['Todo'], feature_category: :team_planning do
+RSpec.describe GitlabSchema.types['Todo'], feature_category: :notifications do
let_it_be(:current_user) { create(:user) }
let_it_be(:author) { create(:user) }
diff --git a/spec/graphql/types/todoable_interface_spec.rb b/spec/graphql/types/todoable_interface_spec.rb
index 66a0e9a42bd..91a55c842c0 100644
--- a/spec/graphql/types/todoable_interface_spec.rb
+++ b/spec/graphql/types/todoable_interface_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Types::TodoableInterface, feature_category: :team_planning do
+RSpec.describe Types::TodoableInterface, feature_category: :notifications do
include GraphqlHelpers
it 'exposes the expected fields' do
diff --git a/spec/keeps/helpers/file_helper_spec.rb b/spec/keeps/helpers/file_helper_spec.rb
index 05391f5bf3d..6c35ed53da2 100644
--- a/spec/keeps/helpers/file_helper_spec.rb
+++ b/spec/keeps/helpers/file_helper_spec.rb
@@ -137,4 +137,38 @@ RSpec.describe Keeps::Helpers::FileHelper, feature_category: :tooling do
end
end
end
+
+ describe '#replace_as_string' do
+ let(:filename) { 'file.txt' }
+ let(:new_milestone) { '17.5' }
+ let(:parsed_file) do
+ <<~RUBY
+ # Migration type +class+
+ # frozen_string_literal: true
+
+ # See https://docs.gitlab.com/ee/development/migration_style_guide.html
+ # for more information on how to write migrations for GitLab.
+
+ =begin
+ This migration adds
+ a new column to project
+ =end
+ class AddColToProjects < Gitlab::Database::Migration[2.2]
+ milestone #{new_milestone} # Inline comment
+
+ def change
+ add_column :projects, :bool_col, :boolean, default: false, null: false # adds a new column
+ end
+ end# Another inline comment
+ RUBY
+ end
+
+ before do
+ described_class.def_node_matcher(:milestone_node, '`(send nil? :milestone $(str _) ...)')
+ end
+
+ it 'parses the file as expected' do
+ expect(helper.replace_as_string(helper.milestone_node, new_milestone)).to eq(parsed_file)
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/build/context/build_spec.rb b/spec/lib/gitlab/ci/build/context/build_spec.rb
index 1d7ef6d794a..3023eae70d7 100644
--- a/spec/lib/gitlab/ci/build/context/build_spec.rb
+++ b/spec/lib/gitlab/ci/build/context/build_spec.rb
@@ -168,4 +168,12 @@ RSpec.describe Gitlab::Ci::Build::Context::Build, feature_category: :pipeline_co
it_behaves_like 'variables collection'
end
+
+ describe '#variables_hash_expanded' do
+ subject { context.variables_hash_expanded }
+
+ it { expect(context.variables_hash_expanded).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) }
+
+ it_behaves_like 'variables collection'
+ 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 06273428ccf..ab9971475a0 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
@@ -158,10 +158,35 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes, feature_category
end
before do
- allow(context).to receive(:variables_hash).and_return(variables_hash)
+ allow(context).to receive(:variables_hash_expanded).and_return(variables_hash)
end
it { is_expected.to be_truthy }
+
+ context 'when the variable is nested' do
+ let(:variables_hash) do
+ { 'HELM_DIR' => 'he$SUFFIX', 'SUFFIX' => 'lm' }
+ end
+
+ let(:variables_hash_expanded) do
+ { 'HELM_DIR' => 'helm', 'SUFFIX' => 'lm' }
+ end
+
+ before do
+ allow(context).to receive(:variables_hash_expanded).and_return(variables_hash_expanded)
+ end
+
+ it { is_expected.to be_truthy }
+
+ context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
+ before do
+ stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
+ allow(context).to receive(:variables_hash).and_return(variables_hash)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
end
context 'when variable expansion does not match' do
@@ -169,7 +194,7 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes, feature_category
let(:modified_paths) { ['path/with/$in/it/file.txt'] }
before do
- allow(context).to receive(:variables_hash).and_return({})
+ allow(context).to receive(:variables_hash_expanded).and_return({})
end
it { is_expected.to be_truthy }
@@ -263,10 +288,54 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes, feature_category
let(:pipeline) { build(:ci_pipeline, project: project, ref: 'feature_2', sha: project.commit('feature_2').sha) }
before do
- allow(context).to receive(:variables_hash).and_return(variables_hash)
+ allow(context).to receive(:variables_hash_expanded).and_return(variables_hash)
end
it { is_expected.to be_truthy }
+
+ context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
+ before do
+ stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
+ allow(context).to receive(:variables_hash).and_return(variables_hash)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when the variable is nested' do
+ let(:context) { instance_double(Gitlab::Ci::Build::Context::Base) }
+ let(:variables_hash) do
+ { 'FEATURE_BRANCH_NAME_PREFIX' => 'feature_', 'NESTED_REF_VAR' => '${FEATURE_BRANCH_NAME_PREFIX}1' }
+ end
+
+ let(:variables_hash_expanded) do
+ { 'FEATURE_BRANCH_NAME_PREFIX' => 'feature_', 'NESTED_REF_VAR' => 'feature_1' }
+ end
+
+ let(:globs) { { paths: ['file2.txt'], compare_to: '$NESTED_REF_VAR' } }
+ let(:pipeline) do
+ build(:ci_pipeline, project: project, ref: 'feature_2', sha: project.commit('feature_2').sha)
+ end
+
+ before do
+ allow(context).to receive(:variables_hash_expanded).and_return(variables_hash_expanded)
+ end
+
+ it { is_expected.to be_truthy }
+
+ context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
+ before do
+ stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
+ allow(context).to receive(:variables_hash).and_return(variables_hash)
+ end
+
+ it 'raises ParseError' do
+ expect { satisfied_by }.to raise_error(
+ ::Gitlab::Ci::Build::Rules::Rule::Clause::ParseError, 'rules:changes:compare_to is not a valid ref'
+ )
+ end
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb
index 9e10907d154..1dae395babe 100644
--- a/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb
+++ b/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb
@@ -6,6 +6,7 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category:
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :small_repo, files: { 'subdir/my_file.txt' => '' }) }
let_it_be(:other_project) { create(:project, :small_repo, files: { 'file.txt' => '' }) }
+ let(:pipeline) { instance_double(Ci::Pipeline, project: project, sha: 'sha', user: user) }
let(:variables) do
Gitlab::Ci::Variables::Collection.new([
@@ -13,6 +14,7 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category:
{ key: 'FILE_TXT', value: 'file.txt' },
{ key: 'FULL_PATH_VALID', value: 'subdir/my_file.txt' },
{ key: 'FULL_PATH_INVALID', value: 'subdir/does_not_exist.txt' },
+ { key: 'NESTED_FULL_PATH_VALID', value: '$SUBDIR/my_file.txt' },
{ key: 'NEW_BRANCH', value: 'new_branch' },
{ key: 'MASKED_VAR', value: 'masked_value', masked: true }
])
@@ -29,7 +31,7 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category:
end
describe '#satisfied_by?' do
- subject(:satisfied_by?) { described_class.new(clause).satisfied_by?(nil, context) }
+ subject(:satisfied_by?) { described_class.new(clause).satisfied_by?(pipeline, context) }
before do
allow(context).to receive(:variables).and_return(variables)
@@ -65,6 +67,20 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category:
it { is_expected.to be_falsey }
end
+
+ context 'when the variable is nested and matches' do
+ let(:globs) { ['$NESTED_FULL_PATH_VALID'] }
+
+ it { is_expected.to be_truthy }
+
+ context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
+ before do
+ stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
end
context 'when a file path has a variable' do
@@ -114,6 +130,14 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category:
let(:globs) { ['$FILE_TXT'] }
it { is_expected.to be_truthy }
+
+ context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
+ before do
+ stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
+ end
+
+ it { is_expected.to be_truthy }
+ end
end
context 'when the project path is invalid' do
@@ -135,6 +159,19 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category:
"rules:exists:project `invalid/path/subdir` is not a valid project path"
)
end
+
+ context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
+ before do
+ stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
+ end
+
+ it 'raises an error' do
+ expect { satisfied_by? }.to raise_error(
+ Gitlab::Ci::Build::Rules::Rule::Clause::ParseError,
+ "rules:exists:project `invalid/path/subdir` is not a valid project path"
+ )
+ end
+ end
end
context 'when the project path contains a masked variable' do
@@ -165,6 +202,14 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category:
let(:ref) { '$NEW_BRANCH' }
it { is_expected.to be_truthy }
+
+ context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
+ before do
+ stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
+ end
+
+ it { is_expected.to be_truthy }
+ end
end
context 'when the ref is invalid' do
@@ -187,6 +232,20 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category:
"in project `#{other_project.full_path}`"
)
end
+
+ context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
+ before do
+ stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
+ end
+
+ it 'raises an error' do
+ expect { satisfied_by? }.to raise_error(
+ Gitlab::Ci::Build::Rules::Rule::Clause::ParseError,
+ "rules:exists:ref `invalid/ref/new_branch` is not a valid ref " \
+ "in project `#{other_project.full_path}`"
+ )
+ end
+ end
end
context 'when the ref contains a masked variable' do
diff --git a/spec/lib/gitlab/ci/config/external/rules_spec.rb b/spec/lib/gitlab/ci/config/external/rules_spec.rb
index 271a37ea584..8b6873744b9 100644
--- a/spec/lib/gitlab/ci/config/external/rules_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/rules_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::External::Rules, feature_category: :pipeline_composition do
let(:context) { double(variables_hash: {}) }
let(:rule_hashes) {}
- let(:pipeline) { instance_double(Ci::Pipeline, project_id: project.id, sha: 'sha') }
+ let(:pipeline) { instance_double(Ci::Pipeline, project: project, project_id: project.id, sha: 'sha') }
let_it_be(:project) { create(:project, :custom_repo, files: { 'file.txt' => 'file' }) }
subject(:rules) { described_class.new(rule_hashes) }
diff --git a/spec/models/ci/pipeline_creation/requests_spec.rb b/spec/models/ci/pipeline_creation/requests_spec.rb
index 91ff452272a..7438b297f28 100644
--- a/spec/models/ci/pipeline_creation/requests_spec.rb
+++ b/spec/models/ci/pipeline_creation/requests_spec.rb
@@ -77,36 +77,6 @@ RSpec.describe Ci::PipelineCreation::Requests, :clean_gitlab_redis_shared_state,
expect(described_class.pipeline_creating_for_merge_request?(merge_request)).to be_falsey
end
end
-
- context 'when delete_if_all_complete is true' do
- context 'when there are only finished creations for the merge request' do
- it 'deletes the MR pipeline creations key from Redis' do
- request_1 = described_class.start_for_merge_request(merge_request)
- request_2 = described_class.start_for_merge_request(merge_request)
- described_class.succeeded(request_1)
- described_class.failed(request_2)
-
- expect(described_class.pipeline_creating_for_merge_request?(merge_request, delete_if_all_complete: true))
- .to be_falsey
- expect(read(request_1)).to be_nil
- expect(read(request_2)).to be_nil
- end
- end
-
- context 'when there are unfinished creations for the merge request' do
- it 'does not delete the MR pipeline creations key from Redis' do
- request_1 = described_class.start_for_merge_request(merge_request)
- request_2 = described_class.start_for_merge_request(merge_request)
- described_class.start_for_merge_request(merge_request)
- described_class.succeeded(request_1)
-
- expect(described_class.pipeline_creating_for_merge_request?(merge_request, delete_if_all_complete: false))
- .to be_truthy
- expect(read(request_1)).to eq({ 'status' => 'succeeded' })
- expect(read(request_2)).to eq({ 'status' => 'in_progress' })
- end
- end
- end
end
describe '.hset' do
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index ba07ac91ad2..4866e6e589d 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -277,6 +277,23 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
end
+ describe '#owner' do
+ subject(:owner) { runner.owner }
+
+ context 'when runner does not have creator_id' do
+ let_it_be(:runner) { create(:ci_runner, :instance) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when runner has creator' do
+ let_it_be(:creator) { create(:user) }
+ let_it_be(:runner) { create(:ci_runner, :instance, creator: creator) }
+
+ it { is_expected.to eq creator }
+ end
+ end
+
describe '.instance_type' do
let!(:group_runner) { create(:ci_runner, :group, groups: [group]) }
let!(:project_runner) { create(:ci_runner, :project, projects: [project]) }
@@ -1125,8 +1142,8 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
let_it_be(:project1) { create(:project) }
let_it_be(:project2) { create(:project) }
- describe '#owner_project' do
- subject(:owner_project) { project_runner.owner_project }
+ describe '#owner' do
+ subject(:owner) { project_runner.owner }
context 'with project1 as first project associated with runner' do
let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project1, project2]) }
@@ -1638,6 +1655,22 @@ RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
end
end
end
+
+ describe '#owner' do
+ subject(:owner) { runner.owner }
+
+ context 'with runner assigned to child_group' do
+ let(:runner) { child_group_runner }
+
+ it { is_expected.to eq child_group }
+ end
+
+ context 'with runner assigned to top_level_group_runner' do
+ let(:runner) { top_level_group_runner }
+
+ it { is_expected.to eq top_level_group }
+ end
+ end
end
describe '#short_sha' do
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index 89b32793948..ec7b4daf325 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Todo, feature_category: :team_planning do
+RSpec.describe Todo, feature_category: :notifications do
let(:issue) { create(:issue) }
describe 'relationships' do
diff --git a/spec/policies/todo_policy_spec.rb b/spec/policies/todo_policy_spec.rb
index 0230f106f0f..c737cd1a6e4 100644
--- a/spec/policies/todo_policy_spec.rb
+++ b/spec/policies/todo_policy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe TodoPolicy, feature_category: :team_planning do
+RSpec.describe TodoPolicy, feature_category: :notifications do
using RSpec::Parameterized::TableSyntax
let_it_be(:project) { create(:project) }
diff --git a/spec/requests/api/graphql/current_user_todos_spec.rb b/spec/requests/api/graphql/current_user_todos_spec.rb
index 5df39064126..b20314e2d0b 100644
--- a/spec/requests/api/graphql/current_user_todos_spec.rb
+++ b/spec/requests/api/graphql/current_user_todos_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'A Todoable that implements the CurrentUserTodos interface',
- feature_category: :team_planning do
+ feature_category: :notifications do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
diff --git a/spec/requests/api/graphql/todo_query_spec.rb b/spec/requests/api/graphql/todo_query_spec.rb
index 83b7af7dfd4..e3675597cc6 100644
--- a/spec/requests/api/graphql/todo_query_spec.rb
+++ b/spec/requests/api/graphql/todo_query_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'Todo Query', feature_category: :team_planning do
+RSpec.describe 'Todo Query', feature_category: :notifications do
include GraphqlHelpers
let_it_be(:current_user) { nil }
diff --git a/spec/serializers/ci/dag_job_entity_spec.rb b/spec/serializers/ci/dag_job_entity_spec.rb
deleted file mode 100644
index 5e2b186186f..00000000000
--- a/spec/serializers/ci/dag_job_entity_spec.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Ci::DagJobEntity do
- let_it_be(:request) { double(:request) }
-
- let(:job) { create(:ci_build, name: 'dag_job') }
- let(:entity) { described_class.new(job, request: request) }
-
- describe '#as_json' do
- subject { entity.as_json }
-
- RSpec.shared_examples "matches schema" do
- it "matches schema" do
- expect(subject.to_json).to match_schema('entities/dag_job')
- end
- end
-
- it 'contains the name' do
- expect(subject[:name]).to eq 'dag_job'
- end
-
- it_behaves_like "matches schema"
-
- context 'when job is stage scheduled' do
- it 'contains the name scheduling_type' do
- expect(subject[:scheduling_type]).to eq 'stage'
- end
-
- it 'does not expose needs' do
- expect(subject).not_to include(:needs)
- end
-
- it_behaves_like "matches schema"
- end
-
- context 'when job is dag scheduled' do
- let(:job) { create(:ci_build, scheduling_type: 'dag') }
-
- it 'contains the name scheduling_type' do
- expect(subject[:scheduling_type]).to eq 'dag'
- end
-
- it_behaves_like "matches schema"
-
- context 'when job has needs' do
- let!(:need) { create(:ci_build_need, build: job, name: 'compile') }
-
- it 'exposes the array of needs' do
- expect(subject[:needs]).to eq ['compile']
- end
-
- it_behaves_like "matches schema"
- end
-
- context 'when job has empty needs' do
- it 'exposes an empty array of needs' do
- expect(subject[:needs]).to eq []
- end
-
- it_behaves_like "matches schema"
- end
- end
- end
-end
diff --git a/spec/serializers/ci/dag_job_group_entity_spec.rb b/spec/serializers/ci/dag_job_group_entity_spec.rb
deleted file mode 100644
index b654b21f583..00000000000
--- a/spec/serializers/ci/dag_job_group_entity_spec.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Ci::DagJobGroupEntity do
- let_it_be(:request) { double(:request) }
- let_it_be(:pipeline) { create(:ci_pipeline) }
- let_it_be(:stage) { create(:ci_stage, pipeline: pipeline) }
-
- let(:group) { Ci::Group.new(pipeline.project, stage, name: 'test', jobs: jobs) }
- let(:entity) { described_class.new(group, request: request) }
-
- describe '#as_json' do
- subject { entity.as_json }
-
- context 'when group contains 1 job' do
- let(:job) { create(:ci_build, stage_id: stage.id, pipeline: pipeline, name: 'test') }
- let(:jobs) { [job] }
-
- it 'exposes a name' do
- expect(subject.fetch(:name)).to eq 'test'
- end
-
- it 'exposes the size' do
- expect(subject.fetch(:size)).to eq 1
- end
-
- it 'exposes the jobs' do
- exposed_jobs = subject.fetch(:jobs)
-
- expect(exposed_jobs.size).to eq 1
- expect(exposed_jobs.first.fetch(:name)).to eq 'test'
- end
-
- it 'matches schema' do
- expect(subject.to_json).to match_schema('entities/dag_job_group')
- end
- end
-
- context 'when group contains multiple parallel jobs' do
- let(:job_1) { create(:ci_build, stage_id: stage.id, pipeline: pipeline, name: 'test 1/2') }
- let(:job_2) { create(:ci_build, stage_id: stage.id, pipeline: pipeline, name: 'test 2/2') }
- let(:jobs) { [job_1, job_2] }
-
- it 'exposes a name' do
- expect(subject.fetch(:name)).to eq 'test'
- end
-
- it 'exposes the size' do
- expect(subject.fetch(:size)).to eq 2
- end
-
- it 'exposes the jobs' do
- exposed_jobs = subject.fetch(:jobs)
-
- expect(exposed_jobs.size).to eq 2
- expect(exposed_jobs.first.fetch(:name)).to eq 'test 1/2'
- expect(exposed_jobs.last.fetch(:name)).to eq 'test 2/2'
- end
-
- it 'matches schema' do
- expect(subject.to_json).to match_schema('entities/dag_job_group')
- end
- end
- end
-end
diff --git a/spec/serializers/ci/dag_pipeline_entity_spec.rb b/spec/serializers/ci/dag_pipeline_entity_spec.rb
deleted file mode 100644
index a8ac76d800f..00000000000
--- a/spec/serializers/ci/dag_pipeline_entity_spec.rb
+++ /dev/null
@@ -1,163 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Ci::DagPipelineEntity do
- let_it_be(:request) { double(:request) }
-
- let_it_be(:pipeline) { create(:ci_pipeline) }
-
- let(:entity) { described_class.new(pipeline, request: request) }
-
- describe '#as_json' do
- subject { entity.as_json }
-
- RSpec.shared_examples "matches schema" do
- it 'matches schema' do
- expect(subject.to_json).to match_schema('entities/dag_pipeline')
- end
- end
-
- context 'when pipeline is empty' do
- it 'contains stages' do
- expect(subject).to include(:stages)
-
- expect(subject[:stages]).to be_empty
- end
-
- it_behaves_like "matches schema"
- end
-
- context 'when pipeline has jobs' do
- let_it_be(:build_stage) { create(:ci_stage, name: 'build', pipeline: pipeline) }
- let_it_be(:test_stage) { create(:ci_stage, name: 'test', pipeline: pipeline) }
- let_it_be(:deploy_stage) { create(:ci_stage, name: 'deploy', pipeline: pipeline) }
-
- let!(:build_job) { create(:ci_build, ci_stage: build_stage, pipeline: pipeline) }
- let!(:test_job) { create(:ci_build, ci_stage: test_stage, pipeline: pipeline) }
- let!(:deploy_job) { create(:ci_build, ci_stage: deploy_stage, pipeline: pipeline) }
-
- it 'contains 3 stages' do
- stages = subject[:stages]
-
- expect(stages.size).to eq 3
- expect(stages.map { |s| s[:name] }).to contain_exactly('build', 'test', 'deploy')
- end
-
- it_behaves_like "matches schema"
- end
-
- context 'when pipeline has parallel jobs, DAG needs and GenericCommitStatus' do
- let!(:stage_build) { create(:ci_stage, name: 'build', position: 1, pipeline: pipeline) }
- let!(:stage_test) { create(:ci_stage, name: 'test', position: 2, pipeline: pipeline) }
- let!(:stage_deploy) { create(:ci_stage, name: 'deploy', position: 3, pipeline: pipeline) }
-
- let!(:job_build_1) { create(:ci_build, name: 'build 1', ci_stage: stage_build, pipeline: pipeline) }
- let!(:job_build_2) { create(:ci_build, name: 'build 2', ci_stage: stage_build, pipeline: pipeline) }
- let!(:commit_status) { create(:generic_commit_status, ci_stage: stage_build, pipeline: pipeline) }
-
- let!(:job_rspec_1) { create(:ci_build, name: 'rspec 1/2', ci_stage: stage_test, pipeline: pipeline) }
- let!(:job_rspec_2) { create(:ci_build, name: 'rspec 2/2', ci_stage: stage_test, pipeline: pipeline) }
-
- let!(:job_jest) do
- create(:ci_build, name: 'jest', ci_stage: stage_test, scheduling_type: 'dag', pipeline: pipeline)
- .tap do |job|
- create(:ci_build_need, name: 'build 1', build: job)
- end
- end
-
- let!(:job_deploy_ruby) do
- create(:ci_build, name: 'deploy_ruby', ci_stage: stage_deploy, scheduling_type: 'dag', pipeline: pipeline)
- .tap do |job|
- create(:ci_build_need, name: 'rspec 1/2', build: job)
- create(:ci_build_need, name: 'rspec 2/2', build: job)
- end
- end
-
- let!(:job_deploy_js) do
- create(:ci_build, name: 'deploy_js', ci_stage: stage_deploy, scheduling_type: 'dag', pipeline: pipeline)
- .tap do |job|
- create(:ci_build_need, name: 'jest', build: job)
- end
- end
-
- it 'performs the smallest number of queries', :request_store do
- log = ActiveRecord::QueryRecorder.new { subject }
-
- # stages, project, builds, build_needs
- expect(log.count).to eq 4
- end
-
- it 'contains all the data' do
- expected_result = {
- stages: [
- {
- name: 'build',
- groups: [
- {
- name: 'build 1', size: 1, jobs: [
- { name: 'build 1', scheduling_type: 'stage' }
- ]
- },
- {
- name: 'build 2', size: 1, jobs: [
- { name: 'build 2', scheduling_type: 'stage' }
- ]
- },
- {
- name: 'generic', size: 1, jobs: [
- { name: 'generic', scheduling_type: nil }
- ]
- }
- ]
- },
- {
- name: 'test',
- groups: [
- {
- name: 'jest', size: 1, jobs: [
- { name: 'jest', scheduling_type: 'dag', needs: ['build 1'] }
- ]
- },
- {
- name: 'rspec', size: 2, jobs: [
- { name: 'rspec 1/2', scheduling_type: 'stage' },
- { name: 'rspec 2/2', scheduling_type: 'stage' }
- ]
- }
- ]
- },
- {
- name: 'deploy',
- groups: [
- {
- name: 'deploy_js', size: 1, jobs: [
- { name: 'deploy_js', scheduling_type: 'dag', needs: ['jest'] }
- ]
- },
- {
- name: 'deploy_ruby', size: 1, jobs: [
- { name: 'deploy_ruby', scheduling_type: 'dag', needs: ['rspec 1/2', 'rspec 2/2'] }
- ]
- }
- ]
- }
- ]
- }
-
- expect(subject.fetch(:stages)).not_to be_empty
-
- expect(subject.fetch(:stages)[0].fetch(:name)).to eq 'build'
- expect(subject.fetch(:stages)[0]).to eq expected_result.fetch(:stages)[0]
-
- expect(subject.fetch(:stages)[1].fetch(:name)).to eq 'test'
- expect(subject.fetch(:stages)[1]).to eq expected_result.fetch(:stages)[1]
-
- expect(subject.fetch(:stages)[2].fetch(:name)).to eq 'deploy'
- expect(subject.fetch(:stages)[2]).to eq expected_result.fetch(:stages)[2]
- end
-
- it_behaves_like "matches schema"
- end
- end
-end
diff --git a/spec/serializers/ci/dag_pipeline_serializer_spec.rb b/spec/serializers/ci/dag_pipeline_serializer_spec.rb
deleted file mode 100644
index 856f6760d5d..00000000000
--- a/spec/serializers/ci/dag_pipeline_serializer_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Ci::DagPipelineSerializer do
- describe '#represent' do
- subject { described_class.new.represent(pipeline) }
-
- let(:pipeline) { create(:ci_pipeline) }
- let!(:job) { create(:ci_build, pipeline: pipeline) }
-
- it 'includes stages' do
- expect(subject[:stages]).to be_present
- expect(subject[:stages].size).to eq 1
- end
-
- it 'matches schema' do
- expect(subject.to_json).to match_schema('entities/dag_pipeline')
- end
- end
-end
diff --git a/spec/serializers/ci/dag_stage_entity_spec.rb b/spec/serializers/ci/dag_stage_entity_spec.rb
deleted file mode 100644
index 3530a5e2bae..00000000000
--- a/spec/serializers/ci/dag_stage_entity_spec.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Ci::DagStageEntity do
- let_it_be(:pipeline) { create(:ci_pipeline) }
- let_it_be(:request) { double(:request) }
-
- let(:stage) { create(:ci_stage, pipeline: pipeline, name: 'test') }
- let(:entity) { described_class.new(stage, request: request) }
-
- let!(:job) { create(:ci_build, :success, pipeline: pipeline, stage_id: stage.id) }
-
- describe '#as_json' do
- subject { entity.as_json }
-
- it 'contains valid name' do
- expect(subject[:name]).to eq 'test'
- end
-
- it 'contains the job groups' do
- expect(subject).to include :groups
- expect(subject[:groups]).not_to be_empty
-
- job_group = subject[:groups].first
- expect(job_group[:name]).to eq 'test'
- expect(job_group[:size]).to eq 1
- expect(job_group[:jobs]).not_to be_empty
- end
-
- it "matches schema" do
- expect(subject.to_json).to match_schema('entities/dag_stage')
- end
- end
-end
diff --git a/spec/services/ci/create_pipeline_service/rules_spec.rb b/spec/services/ci/create_pipeline_service/rules_spec.rb
index 24aecf43eda..70deebe3369 100644
--- a/spec/services/ci/create_pipeline_service/rules_spec.rb
+++ b/spec/services/ci/create_pipeline_service/rules_spec.rb
@@ -227,7 +227,7 @@ RSpec.describe Ci::CreatePipelineService, feature_category: :pipeline_compositio
script: echo Hello, World!
rules:
- exists:
- - $VAR_NESTED # does not match because of https://gitlab.com/gitlab-org/gitlab/-/issues/411344
+ - $VAR_NESTED
YAML
end
@@ -247,7 +247,18 @@ RSpec.describe Ci::CreatePipelineService, feature_category: :pipeline_compositio
it 'creates all relevant jobs' do
expect(pipeline).to be_persisted
- expect(build_names).to contain_exactly('job1', 'job2')
+ expect(build_names).to contain_exactly('job1', 'job2', 'job4')
+ end
+
+ context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
+ before do
+ stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
+ end
+
+ it 'creates all relevant jobs' do
+ expect(pipeline).to be_persisted
+ expect(build_names).to contain_exactly('job1', 'job2')
+ end
end
end
end
@@ -808,6 +819,10 @@ RSpec.describe Ci::CreatePipelineService, feature_category: :pipeline_compositio
VALID_BRANCH_NAME: feature_1
FEATURE_BRANCH_NAME_PREFIX: feature_
INVALID_BRANCH_NAME: invalid-branch
+ VALID_FILENAME: file2.txt
+ INVALID_FILENAME: file1.txt
+ VALID_BASENAME: file2
+ VALID_NESTED_VARIABLE: ${VALID_BASENAME}.txt
job1:
script: exit 0
rules:
@@ -857,6 +872,52 @@ RSpec.describe Ci::CreatePipelineService, feature_category: :pipeline_compositio
)
end
end
+
+ context 'when paths is defined by a variable' do
+ let(:compare_to) { '${VALID_BRANCH_NAME}' }
+
+ context 'when the variable does not exist' do
+ let(:changed_file) { '$NON_EXISTENT_VAR' }
+
+ it 'does not create job1' do
+ expect(build_names).to contain_exactly('job2')
+ end
+ end
+
+ context 'when the variable contains a matching filename' do
+ let(:changed_file) { '$VALID_FILENAME' }
+
+ it 'creates both jobs' do
+ expect(build_names).to contain_exactly('job1', 'job2')
+ end
+ end
+
+ context 'when the variable does not contain a matching filename' do
+ let(:changed_file) { '$INVALID_FILENAME' }
+
+ it 'does not create job1' do
+ expect(build_names).to contain_exactly('job2')
+ end
+ end
+
+ context 'when the variable is nested and contains a matching filename' do
+ let(:changed_file) { '$VALID_NESTED_VARIABLE' }
+
+ it 'creates both jobs' do
+ expect(build_names).to contain_exactly('job1', 'job2')
+ end
+
+ context 'when expand_nested_variables_in_job_rules_exists_and_changes is disabled' do
+ before do
+ stub_feature_flags(expand_nested_variables_in_job_rules_exists_and_changes: false)
+ end
+
+ it 'does not create job1' do
+ expect(build_names).to contain_exactly('job2')
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/services/ci/runners/assign_runner_service_spec.rb b/spec/services/ci/runners/assign_runner_service_spec.rb
index f585e78345e..dda68e653e3 100644
--- a/spec/services/ci/runners/assign_runner_service_spec.rb
+++ b/spec/services/ci/runners/assign_runner_service_spec.rb
@@ -3,13 +3,13 @@
require 'spec_helper'
RSpec.describe ::Ci::Runners::AssignRunnerService, '#execute', feature_category: :runner do
- let(:service) { described_class.new(runner, new_project, user) }
-
let_it_be(:organization1) { create(:organization) }
let_it_be(:owner_group) { create(:group, organization: organization1) }
let_it_be(:owner_project) { create(:project, group: owner_group, organization: organization1) }
let_it_be(:new_project) { create(:project, organization: organization1) }
- let_it_be(:runner) { create(:ci_runner, :project, projects: [owner_project]) }
+
+ let(:service) { described_class.new(runner, new_project, user) }
+ let(:runner) { create(:ci_runner, :project, projects: [owner_project]) }
subject(:execute) { service.execute }
@@ -108,7 +108,7 @@ RSpec.describe ::Ci::Runners::AssignRunnerService, '#execute', feature_category:
end
context 'with admin user', :enable_admin_mode do
- let(:user) { create(:user, :admin) }
+ let_it_be(:user) { create(:user, :admin) }
it 'calls assign_to on runner and returns success response' do
expect(runner).to receive(:assign_to).with(new_project, user).once.and_call_original
@@ -117,7 +117,7 @@ RSpec.describe ::Ci::Runners::AssignRunnerService, '#execute', feature_category:
end
context 'when runner is not associated with any projects' do
- let_it_be(:runner) { create(:ci_runner, :project, :without_projects) }
+ let(:runner) { create(:ci_runner, :project, :without_projects) }
it 'calls assign_to on runner and returns success response' do
expect(runner).to receive(:assign_to).with(new_project, user).once.and_call_original
diff --git a/spec/services/ci/runners/set_runner_associated_projects_service_spec.rb b/spec/services/ci/runners/set_runner_associated_projects_service_spec.rb
index 35ef27dcd48..9155416bbf5 100644
--- a/spec/services/ci/runners/set_runner_associated_projects_service_spec.rb
+++ b/spec/services/ci/runners/set_runner_associated_projects_service_spec.rb
@@ -59,7 +59,7 @@ RSpec.describe ::Ci::Runners::SetRunnerAssociatedProjectsService, '#execute', fe
runner.reload
- expect(runner.owner_project).to eq(owner_project)
+ expect(runner.owner).to eq(owner_project)
expect(runner.runner_projects.order(:id).map(&:project_id)).to eq([owner_project, *new_projects].map(&:id))
end
end
@@ -72,7 +72,7 @@ RSpec.describe ::Ci::Runners::SetRunnerAssociatedProjectsService, '#execute', fe
runner.reload
- expect(runner.owner_project).to eq(owner_project)
+ expect(runner.owner).to eq(owner_project)
expect(runner.runner_projects.order(:id).map(&:project_id)).to eq([owner_project, *new_projects].map(&:id))
end
end
@@ -85,7 +85,7 @@ RSpec.describe ::Ci::Runners::SetRunnerAssociatedProjectsService, '#execute', fe
runner.reload
- expect(runner.owner_project).to eq(owner_project)
+ expect(runner.owner).to eq(owner_project)
expect(runner.projects.ids).to contain_exactly(owner_project.id)
end
end
@@ -164,7 +164,7 @@ RSpec.describe ::Ci::Runners::SetRunnerAssociatedProjectsService, '#execute', fe
runner.reload
- expect(runner.owner_project).to be_nil
+ expect(runner.owner).to be_nil
expect(runner.projects.ids).to be_empty
end
@@ -176,7 +176,7 @@ RSpec.describe ::Ci::Runners::SetRunnerAssociatedProjectsService, '#execute', fe
runner.reload
- expect(runner.owner_project).to be_nil
+ expect(runner.owner).to be_nil
expect(runner.projects.ids).to be_empty
end
end
@@ -204,7 +204,7 @@ RSpec.describe ::Ci::Runners::SetRunnerAssociatedProjectsService, '#execute', fe
runner.reload
- expect(runner.owner_project).to eq(owner_project)
+ expect(runner.owner).to eq(owner_project)
expect(runner.runner_projects.order(:id).map(&:project_id)).to eq(new_projects.map(&:id))
end
end
@@ -217,7 +217,7 @@ RSpec.describe ::Ci::Runners::SetRunnerAssociatedProjectsService, '#execute', fe
runner.reload
- expect(runner.owner_project).to be_nil
+ expect(runner.owner).to be_nil
expect(runner.projects.ids).to be_empty
end
end
diff --git a/spec/services/merge_requests/handle_assignees_change_service_spec.rb b/spec/services/merge_requests/handle_assignees_change_service_spec.rb
index e1af588ab28..a7fe1077439 100644
--- a/spec/services/merge_requests/handle_assignees_change_service_spec.rb
+++ b/spec/services/merge_requests/handle_assignees_change_service_spec.rb
@@ -113,6 +113,12 @@ RSpec.describe MergeRequests::HandleAssigneesChangeService, feature_category: :c
end
end
+ it 'invalidates cache counts for old assignees' do
+ expect(old_assignees).to all(receive(:invalidate_merge_request_cache_counts))
+
+ execute
+ end
+
context 'when execute_hooks option is set to true' do
let(:options) { { 'execute_hooks' => true } }
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 6389cf23d00..aab9e334956 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe TodoService, feature_category: :team_planning do
+RSpec.describe TodoService, feature_category: :notifications do
include AfterNextHelpers
let_it_be(:group) { create(:group) }
diff --git a/spec/services/users/update_todo_count_cache_service_spec.rb b/spec/services/users/update_todo_count_cache_service_spec.rb
index d69a4ba99b7..6460278a47b 100644
--- a/spec/services/users/update_todo_count_cache_service_spec.rb
+++ b/spec/services/users/update_todo_count_cache_service_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Users::UpdateTodoCountCacheService, feature_category: :team_planning do
+RSpec.describe Users::UpdateTodoCountCacheService, feature_category: :notifications do
describe '#execute' do
let_it_be(:user1) { create(:user) }
let_it_be(:user2) { create(:user) }
diff --git a/spec/services/work_items/callbacks/current_user_todos_spec.rb b/spec/services/work_items/callbacks/current_user_todos_spec.rb
index 35ff8685665..ed554fc64bd 100644
--- a/spec/services/work_items/callbacks/current_user_todos_spec.rb
+++ b/spec/services/work_items/callbacks/current_user_todos_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WorkItems::Callbacks::CurrentUserTodos, feature_category: :team_planning do
+RSpec.describe WorkItems::Callbacks::CurrentUserTodos, feature_category: :notifications do
let_it_be(:reporter) { create(:user) }
let_it_be(:project) { create(:project, :private, reporters: reporter) }
let_it_be(:current_user) { reporter }
diff --git a/spec/support/matchers/have_tracking.rb b/spec/support/matchers/have_tracking.rb
new file mode 100644
index 00000000000..5ca5142cca7
--- /dev/null
+++ b/spec/support/matchers/have_tracking.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+# Basic matcher for view specs to do basic tracking data
+# attribute verification.
+RSpec::Matchers.define :have_tracking do |action:, label: nil|
+ match do |rendered|
+ css = "[data-track-action='#{action}']"
+ css += "[data-track-label='#{label}']" if label
+
+ expect(rendered).to have_css(css)
+ end
+end
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index 264b5f1fc42..92494ca937b 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -7059,11 +7059,6 @@
- './spec/serializers/build_trace_entity_spec.rb'
- './spec/serializers/ci/codequality_mr_diff_entity_spec.rb'
- './spec/serializers/ci/codequality_mr_diff_report_serializer_spec.rb'
-- './spec/serializers/ci/dag_job_entity_spec.rb'
-- './spec/serializers/ci/dag_job_group_entity_spec.rb'
-- './spec/serializers/ci/dag_pipeline_entity_spec.rb'
-- './spec/serializers/ci/dag_pipeline_serializer_spec.rb'
-- './spec/serializers/ci/dag_stage_entity_spec.rb'
- './spec/serializers/ci/daily_build_group_report_result_entity_spec.rb'
- './spec/serializers/ci/daily_build_group_report_result_serializer_spec.rb'
- './spec/serializers/ci/downloadable_artifact_entity_spec.rb'
diff --git a/spec/views/projects/empty.html.haml_spec.rb b/spec/views/projects/empty.html.haml_spec.rb
index 78b8ca289e8..477d36f261a 100644
--- a/spec/views/projects/empty.html.haml_spec.rb
+++ b/spec/views/projects/empty.html.haml_spec.rb
@@ -68,8 +68,7 @@ RSpec.describe 'projects/empty' do
it 'shows invite members info', :aggregate_failures do
render
- expect(rendered).to have_selector('[data-track-action=render]')
- expect(rendered).to have_selector('[data-track-label=invite_members_empty_project]')
+ expect(rendered).to have_tracking(action: 'render', label: 'invite_members_empty_project')
expect(rendered).to have_content('Invite your team')
expect(rendered).to have_content('Add members to this project and start collaborating with your team.')
expect(rendered).to have_selector('.js-invite-members-trigger')