+
### merged_by API field
-Planned removal: GitLab 15.0 (2022-05-22)
+Planned removal: GitLab 16.0 (2023-05-22)
WARNING:
This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
Review the details carefully before upgrading.
-The `merged_by` field in the [merge request API](https://docs.gitlab.com/ee/api/merge_requests.html#list-merge-requests) is being deprecated and will be removed in GitLab 15.0. This field is being replaced with the `merge_user` field (already present in GraphQL) which more correctly identifies who merged a merge request when performing actions (merge when pipeline succeeds, add to merge train) other than a simple merge.
+The `merged_by` field in the [merge request API](https://docs.gitlab.com/ee/api/merge_requests.html#list-merge-requests) has been deprecated in favor of the `merge_user` field which more correctly identifies who merged a merge request when performing actions (merge when pipeline succeeds, add to merge train) other than a simple merge. API users are encouraged to use the new `merge_user` field instead. The `merged_by` field will be removed in v5 of the GitLab REST API.
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index c0cf584f8c3..106e4871736 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -45,7 +45,7 @@ mentions for yourself (the user currently signed in) are highlighted
in a different color.
Avoid mentioning `@all` in issues and merge requests. It sends an email notification
-to all members of that project's parent group, not only the participants of the project,
+to all members of that project's parent group, not only the participants of the project,
and may be interpreted as spam.
Notifications and mentions can be disabled in
[a group's settings](../group/manage.md#disable-email-notifications).
@@ -144,7 +144,7 @@ If you edit an existing comment to add a user mention that wasn't there before,
- Creates a to-do item for the mentioned user.
- Does not send a notification email.
-## Prevent comments by locking an issue
+## Prevent comments by locking the discussion
You can prevent public comments in an issue or merge request.
When you do, only project members can add and edit comments.
@@ -154,6 +154,8 @@ Prerequisite:
- In merge requests, you must have at least the Developer role.
- In issues, you must have at least the Reporter role.
+To lock an issue or merge request:
+
1. On the right sidebar, next to **Lock issue** or **Lock merge request**, select **Edit**.
1. On the confirmation dialog, select **Lock**.
@@ -161,6 +163,9 @@ Notes are added to the page details.
If an issue or merge request is locked and closed, you cannot reopen it.
+
+If you don't see this action on the right sidebar, your project or instance might have [moved sidebar actions](../project/merge_requests/index.md#move-sidebar-actions) enabled.
+
## Add an internal note
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207473) in GitLab 13.9 [with a flag](../../administration/feature_flags.md) named `confidential_notes`. Disabled by default.
diff --git a/doc/user/profile/notifications.md b/doc/user/profile/notifications.md
index 057fed3c2e0..fd5c037fe3a 100644
--- a/doc/user/profile/notifications.md
+++ b/doc/user/profile/notifications.md
@@ -228,20 +228,20 @@ to change their user notification settings to **Watch** instead.
### Edit notification settings for issues, merge requests, and epics
-To enable notifications on a specific issue, merge request, or epic, you must turn on the
-**Notifications** toggle in the right sidebar.
+To toggle notifications on an issue, merge request, or epic: on the right sidebar, turn on or off the **Notifications** toggle.
-- To subscribe, **turn on** if you are not a participant in the discussion, but want to receive
- notifications on each update.
+When you **turn on** notifications, you start receiving notifications on each update, even if you
+haven't participated in the discussion.
+When you turn notifications on in an epic, you aren't automatically subscribed to the issues linked
+to the epic.
- When you turn notifications on in an epic, you aren't automatically subscribed to the issues linked
- to the epic.
+When you **turn off** notifications, you stop receiving notifications for updates.
+Turning this toggle off only unsubscribes you from updates related to this issue, merge request, or epic.
+Learn how to [opt out of all emails from GitLab](#opt-out-of-all-gitlab-emails).
-- To unsubscribe, **turn off** if you are receiving notifications for updates but no longer want to
- receive them.
-
- Turning this toggle off only unsubscribes you from updates related to this issue, merge request, or epic.
- Learn how to [opt out of all emails from GitLab](#opt-out-of-all-gitlab-emails).
+
+If you don't see this action on the right sidebar, your project or instance may have
+enabled a feature flag for [moved sidebar actions](../project/merge_requests/index.md#move-sidebar-actions).
### Notification events on issues, merge requests, and epics
diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md
index e6b0da30bad..e8ec954df8f 100644
--- a/doc/user/project/members/index.md
+++ b/doc/user/project/members/index.md
@@ -61,7 +61,7 @@ To add a user to a project:
1. Select **Invite members**.
1. Enter an email address and select a [role](../../permissions.md).
1. Optional. Select an **Access expiration date**.
- On that date, the user can no longer access the project.
+ From that date onwards, the user can no longer access the project.
1. Select **Invite**.
If the user has a GitLab account, they are added to the members list.
@@ -97,19 +97,20 @@ Each user's access is based on:
- The role they're assigned in the group.
- The maximum role you choose when you invite the group.
-Prerequisite:
+Prerequisites:
- You must have the Maintainer or Owner role.
- Sharing the project with other groups must not be [prevented](../../group/access_and_permissions.md#prevent-a-project-from-being-shared-with-groups).
-To add groups to a project:
+To add a group to a project:
1. On the top bar, select **Main menu > Projects** and find your project.
1. On the left sidebar, select **Project information > Members**.
1. Select **Invite a group**.
1. Select a group.
1. Select the highest [role](../../permissions.md) for users in the group.
-1. Optional. Select an **Access expiration date**. On that date, the group can no longer access the project.
+1. Optional. Select an **Access expiration date**.
+ From that date onwards, the group can no longer access the project.
1. Select **Invite**.
The members of the group are not displayed on the **Members** tab.
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 872f7524cb7..8309b04758a 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -250,6 +250,28 @@ This feature works only when a merge request is merged. Selecting **Remove sourc
after merging does not retarget open merge requests. This improvement is
[proposed as a follow-up](https://gitlab.com/gitlab-org/gitlab/-/issues/321559).
+## Move sidebar actions
+
+
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85584) in GitLab 14.10 [with a flag](../../../administration/feature_flags.md) named `moved_mr_sidebar`. Disabled by default.
+
+FLAG:
+On self-managed GitLab, by default this feature is not available. To make it available per project or for your entire instance, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `moved_mr_sidebar`.
+On GitLab.com, this feature is not available.
+
+When this feature flag is enabled, you can find the following actions in
+**Merge request actions** (**{ellipsis_v}**) on the top right:
+
+- The [notifications](../../profile/notifications.md#edit-notification-settings-for-issues-merge-requests-and-epics) toggle
+- Mark merge request as ready or [draft](../merge_requests/drafts.md)
+- Close merge request
+- [Lock discussion](../../discussions/index.md#prevent-comments-by-locking-the-discussion)
+- Copy reference
+
+When this feature flag is disabled, these actions are in the right sidebar.
+
## Merge request workflows
For a software developer working in a team:
diff --git a/lib/api/api.rb b/lib/api/api.rb
index f0194f5dabd..fa10d796e00 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -173,6 +173,7 @@ module API
mount ::API::AccessRequests
mount ::API::Appearance
mount ::API::BulkImports
+ mount ::API::Ci::ResourceGroups
mount ::API::Ci::Runner
mount ::API::Ci::Runners
mount ::API::Clusters::Agents
@@ -226,7 +227,6 @@ module API
mount ::API::Ci::Jobs
mount ::API::Ci::PipelineSchedules
mount ::API::Ci::Pipelines
- mount ::API::Ci::ResourceGroups
mount ::API::Ci::SecureFiles
mount ::API::Ci::Triggers
mount ::API::Ci::Variables
diff --git a/lib/api/ci/resource_groups.rb b/lib/api/ci/resource_groups.rb
index 3ec2d08158e..a2b6b7fd68b 100644
--- a/lib/api/ci/resource_groups.rb
+++ b/lib/api/ci/resource_groups.rb
@@ -5,17 +5,27 @@ module API
class ResourceGroups < ::API::Base
include PaginationParams
+ ci_resource_groups_tags = %w[ci_resource_groups]
+
before { authenticate! }
feature_category :continuous_delivery
urgency :low
params do
- requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project'
+ requires :id,
+ types: [String, Integer],
+ desc: 'The ID or URL-encoded path of the project owned by the authenticated user'
end
resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
- desc 'Get all resource groups for this project' do
+ desc 'Get all resource groups for a project' do
success Entities::Ci::ResourceGroup
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 404, message: 'Not found' }
+ ]
+ is_array true
+ tags ci_resource_groups_tags
end
params do
use :pagination
@@ -26,8 +36,13 @@ module API
present paginate(user_project.resource_groups), with: Entities::Ci::ResourceGroup
end
- desc 'Get a single resource group' do
+ desc 'Get a specific resource group' do
success Entities::Ci::ResourceGroup
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 404, message: 'Not found' }
+ ]
+ tags ci_resource_groups_tags
end
params do
requires :key, type: String, desc: 'The key of the resource group'
@@ -38,8 +53,14 @@ module API
present resource_group, with: Entities::Ci::ResourceGroup
end
- desc 'List upcoming jobs of a resource group' do
+ desc 'List upcoming jobs for a specific resource group' do
success Entities::Ci::JobBasic
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 404, message: 'Not found' }
+ ]
+ is_array true
+ tags ci_resource_groups_tags
end
params do
requires :key, type: String, desc: 'The key of the resource group'
@@ -57,13 +78,23 @@ module API
present paginate(upcoming_processables), with: Entities::Ci::JobBasic
end
- desc 'Edit a resource group' do
+ desc 'Edit an existing resource group' do
+ detail "Updates an existing resource group's properties."
success Entities::Ci::ResourceGroup
+ failure [
+ { code: 400, message: 'Bad request' },
+ { code: 401, message: 'Unauthorized' },
+ { code: 404, message: 'Not found' }
+ ]
+ tags ci_resource_groups_tags
end
params do
requires :key, type: String, desc: 'The key of the resource group'
- optional :process_mode, type: String, desc: 'The process mode',
- values: ::Ci::ResourceGroup.process_modes.keys
+
+ optional :process_mode,
+ type: String,
+ desc: 'The process mode of the resource group',
+ values: ::Ci::ResourceGroup.process_modes.keys
end
put ':id/resource_groups/:key' do
authorize! :update_resource_group, resource_group
diff --git a/lib/api/entities/ci/job_basic.rb b/lib/api/entities/ci/job_basic.rb
index 26d91d11839..e5b04b86075 100644
--- a/lib/api/entities/ci/job_basic.rb
+++ b/lib/api/entities/ci/job_basic.rb
@@ -4,16 +4,26 @@ module API
module Entities
module Ci
class JobBasic < Grape::Entity
- expose :id, :status, :stage, :name, :ref, :tag, :coverage, :allow_failure
- expose :created_at, :started_at, :finished_at
+ expose :id, documentation: { type: 'integer', example: 1 }
+ expose :status, documentation: { type: 'string', example: 'waiting_for_resource' }
+ expose :stage, documentation: { type: 'string', example: 'deploy' }
+ expose :name, documentation: { type: 'string', example: 'deploy_to_production' }
+ expose :ref, documentation: { type: 'string', example: 'main' }
+ expose :tag, documentation: { type: 'boolean' }
+ expose :coverage, documentation: { type: 'number', format: 'float', example: 0.90 }
+ expose :allow_failure, documentation: { type: 'boolean' }
+ expose :created_at, documentation: { type: 'dateTime', example: '2015-12-24T15:51:21.880Z' }
+ expose :started_at, documentation: { type: 'dateTime', example: '2015-12-24T17:54:30.733Z' }
+ expose :finished_at, documentation: { type: 'dateTime', example: '2015-12-24T17:54:31.198Z' }
expose :duration,
- documentation: { type: 'number', format: 'float', desc: 'Time spent running' }
+ documentation: { type: 'number', format: 'float', desc: 'Time spent running', example: 0.465 }
expose :queued_duration,
- documentation: { type: 'number', format: 'float', desc: 'Time spent enqueued' }
+ documentation: { type: 'number', format: 'float', desc: 'Time spent enqueued', example: 0.123 }
expose :user, with: ::API::Entities::User
expose :commit, with: ::API::Entities::Commit
expose :pipeline, with: ::API::Entities::Ci::PipelineBasic
- expose :failure_reason, if: -> (job) { job.failed? }
+ expose :failure_reason,
+ documentation: { type: 'string', example: 'script_failure' }, if: -> (job) { job.failed? }
expose :web_url do |job, _options|
Gitlab::Routing.url_helpers.project_job_url(job.project, job)
diff --git a/lib/api/entities/ci/resource_group.rb b/lib/api/entities/ci/resource_group.rb
index 0afadfa9e2a..c14e32d32b1 100644
--- a/lib/api/entities/ci/resource_group.rb
+++ b/lib/api/entities/ci/resource_group.rb
@@ -4,7 +4,11 @@ module API
module Entities
module Ci
class ResourceGroup < Grape::Entity
- expose :id, :key, :process_mode, :created_at, :updated_at
+ expose :id, documentation: { type: 'integer', example: 1 }
+ expose :key, documentation: { type: 'string', example: 'production' }
+ expose :process_mode, documentation: { type: 'string', example: 'unordered' }
+ expose :created_at, documentation: { type: 'dateTime', example: '2021-09-01T08:04:59.650Z' }
+ expose :updated_at, documentation: { type: 'dateTime', example: '2021-09-01T08:04:59.650Z' }
end
end
end
diff --git a/lib/gitlab/ci/config/external/file/base.rb b/lib/gitlab/ci/config/external/file/base.rb
index 89da0796906..48ba154862a 100644
--- a/lib/gitlab/ci/config/external/file/base.rb
+++ b/lib/gitlab/ci/config/external/file/base.rb
@@ -47,12 +47,9 @@ module Gitlab
end
def validate!
- context.logger.instrument(:config_file_validation) do
- validate_execution_time!
- validate_location!
- validate_content! if errors.none?
- validate_hash! if errors.none?
- end
+ validate_location!
+ fetch_and_validate_content! if valid?
+ load_and_validate_expanded_hash! if valid?
end
def metadata
@@ -72,11 +69,41 @@ module Gitlab
protected
- def expanded_content_hash
- return unless content_hash
+ def validate_location!
+ if invalid_location_type?
+ errors.push("Included file `#{masked_location}` needs to be a string")
+ elsif invalid_extension?
+ errors.push("Included file `#{masked_location}` does not have YAML extension!")
+ end
+ end
- strong_memoize(:expanded_content_yaml) do
- expand_includes(content_hash)
+ def fetch_and_validate_content!
+ context.logger.instrument(:config_file_fetch_content) do
+ content # calling the method fetches then memoizes the result
+ end
+
+ return if errors.any?
+
+ context.logger.instrument(:config_file_validate_content) do
+ validate_content!
+ end
+ end
+
+ def load_and_validate_expanded_hash!
+ context.logger.instrument(:config_file_fetch_content_hash) do
+ content_hash # calling the method loads then memoizes the result
+ end
+
+ context.logger.instrument(:config_file_expand_content_includes) do
+ expanded_content_hash # calling the method expands then memoizes the result
+ end
+
+ validate_hash!
+ end
+
+ def validate_content!
+ if content.blank?
+ errors.push("Included file `#{masked_location}` is empty or does not exist!")
end
end
@@ -88,21 +115,11 @@ module Gitlab
nil
end
- def validate_execution_time!
- context.check_execution_time!
- end
+ def expanded_content_hash
+ return unless content_hash
- def validate_location!
- if invalid_location_type?
- errors.push("Included file `#{masked_location}` needs to be a string")
- elsif invalid_extension?
- errors.push("Included file `#{masked_location}` does not have YAML extension!")
- end
- end
-
- def validate_content!
- if content.blank?
- errors.push("Included file `#{masked_location}` is empty or does not exist!")
+ strong_memoize(:expanded_content_yaml) do
+ expand_includes(content_hash)
end
end
diff --git a/lib/gitlab/ci/config/external/mapper.rb b/lib/gitlab/ci/config/external/mapper.rb
index 2a1060a6059..cd752694e74 100644
--- a/lib/gitlab/ci/config/external/mapper.rb
+++ b/lib/gitlab/ci/config/external/mapper.rb
@@ -127,6 +127,7 @@ module Gitlab
def verify!(location_object)
verify_max_includes!
+ verify_execution_time!
location_object.validate!
expandset.add(location_object)
end
@@ -137,6 +138,10 @@ module Gitlab
end
end
+ def verify_execution_time!
+ context.check_execution_time!
+ end
+
def expand_variables(data)
logger.instrument(:config_mapper_variables) do
expand_variables_without_instrumentation(data)
diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb
index 3f8745b4ab1..654e24be8e1 100644
--- a/lib/gitlab/ci/pipeline/chain/populate.rb
+++ b/lib/gitlab/ci/pipeline/chain/populate.rb
@@ -25,8 +25,6 @@ module Gitlab
return error('Failed to build the pipeline!')
end
- set_pipeline_name
-
raise Populate::PopulateError if pipeline.persisted?
end
@@ -36,21 +34,6 @@ module Gitlab
private
- def set_pipeline_name
- return if Feature.disabled?(:pipeline_name, pipeline.project) ||
- @command.yaml_processor_result.workflow_name.blank?
-
- name = @command.yaml_processor_result.workflow_name
- name = ExpandVariables.expand(name, -> { global_context.variables.sort_and_expand_all })
-
- pipeline.build_pipeline_metadata(project: pipeline.project, name: name)
- end
-
- def global_context
- Gitlab::Ci::Build::Context::Global.new(
- pipeline, yaml_variables: @command.pipeline_seed.root_variables)
- end
-
def stage_names
# We filter out `.pre/.post` stages, as they alone are not considered
# a complete pipeline:
diff --git a/lib/gitlab/ci/pipeline/chain/populate_metadata.rb b/lib/gitlab/ci/pipeline/chain/populate_metadata.rb
new file mode 100644
index 00000000000..35b907b669c
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/populate_metadata.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ class PopulateMetadata < Chain::Base
+ include Chain::Helpers
+
+ def perform!
+ set_pipeline_name
+ return if pipeline.pipeline_metadata.nil? || pipeline.pipeline_metadata.valid?
+
+ message = pipeline.pipeline_metadata.errors.full_messages.join(', ')
+ error("Failed to build pipeline metadata! #{message}")
+ end
+
+ def break?
+ pipeline.pipeline_metadata&.errors&.any?
+ end
+
+ private
+
+ def set_pipeline_name
+ return if Feature.disabled?(:pipeline_name, pipeline.project) ||
+ @command.yaml_processor_result.workflow_name.blank?
+
+ name = @command.yaml_processor_result.workflow_name
+ name = ExpandVariables.expand(name, -> { global_context.variables.sort_and_expand_all })
+
+ pipeline.build_pipeline_metadata(project: pipeline.project, name: name)
+ end
+
+ def global_context
+ Gitlab::Ci::Build::Context::Global.new(
+ pipeline, yaml_variables: @command.pipeline_seed.root_variables)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/common.rb b/lib/gitlab/email/common.rb
new file mode 100644
index 00000000000..afee8d9cd3d
--- /dev/null
+++ b/lib/gitlab/email/common.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Email
+ # Contains common methods which must be present in all email classes
+ module Common
+ UNSUBSCRIBE_SUFFIX = '-unsubscribe'
+ UNSUBSCRIBE_SUFFIX_LEGACY = '+unsubscribe'
+ WILDCARD_PLACEHOLDER = '%{key}'
+
+ # This can be overridden for a custom config
+ def config
+ raise NotImplementedError
+ end
+
+ def incoming_email_config
+ Gitlab.config.incoming_email
+ end
+
+ def enabled?
+ !!config&.enabled && config.address.present?
+ end
+
+ def supports_wildcard?
+ config_address = incoming_email_config.address
+
+ config_address.present? && config_address.include?(WILDCARD_PLACEHOLDER)
+ end
+
+ def supports_issue_creation?
+ enabled? && supports_wildcard?
+ end
+
+ def reply_address(key)
+ incoming_email_config.address.sub(WILDCARD_PLACEHOLDER, key)
+ end
+
+ # example: incoming+1234567890abcdef1234567890abcdef-unsubscribe@incoming.gitlab.com
+ def unsubscribe_address(key)
+ incoming_email_config.address.sub(WILDCARD_PLACEHOLDER, "#{key}#{UNSUBSCRIBE_SUFFIX}")
+ end
+
+ def key_from_address(address, wildcard_address: nil)
+ raise NotImplementedError
+ end
+
+ def key_from_fallback_message_id(mail_id)
+ message_id_regexp = /\Areply-(.+)@#{Gitlab.config.gitlab.host}\z/
+
+ mail_id[message_id_regexp, 1]
+ end
+
+ def scan_fallback_references(references)
+ # It's looking for each <...>
+ references.scan(/(?!<)[^<>]+(?=>)/)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/handler/create_issue_handler.rb b/lib/gitlab/email/handler/create_issue_handler.rb
index 434893eab82..e21a88c4e0d 100644
--- a/lib/gitlab/email/handler/create_issue_handler.rb
+++ b/lib/gitlab/email/handler/create_issue_handler.rb
@@ -73,7 +73,7 @@ module Gitlab
end
def can_handle_legacy_format?
- project_path && !incoming_email_token.include?('+') && !mail_key.include?(Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX_LEGACY)
+ project_path && !incoming_email_token.include?('+') && !mail_key.include?(Gitlab::Email::Common::UNSUBSCRIBE_SUFFIX_LEGACY)
end
end
end
diff --git a/lib/gitlab/email/handler/unsubscribe_handler.rb b/lib/gitlab/email/handler/unsubscribe_handler.rb
index 528857aff14..a4e526d9a24 100644
--- a/lib/gitlab/email/handler/unsubscribe_handler.rb
+++ b/lib/gitlab/email/handler/unsubscribe_handler.rb
@@ -12,8 +12,8 @@ module Gitlab
delegate :project, to: :sent_notification, allow_nil: true
HANDLER_REGEX_FOR = -> (suffix) { /\A(?