Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.checksum2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue15
-rw-r--r--app/controllers/projects/pages_controller.rb10
-rw-r--r--app/models/ci/resource_group.rb19
-rw-r--r--app/models/concerns/has_user_type.rb12
-rw-r--r--app/models/namespace.rb8
-rw-r--r--app/models/project.rb29
-rw-r--r--app/models/project_setting.rb9
-rw-r--r--app/policies/global_policy.rb4
-rw-r--r--app/services/markup/rendering_service.rb2
-rw-r--r--app/services/projects/update_service.rb20
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml12
-rw-r--r--app/views/projects/pages/_pages_settings.html.haml9
-rw-r--r--config/feature_flags/development/ci_use_downstream_pipeline_duration_for_calculation.yml8
-rw-r--r--config/feature_flags/development/pages_unique_domain.yml8
-rw-r--r--data/deprecations/15-9-ci-builds-column-validations.yml29
-rw-r--r--db/migrate/20230215131026_add_has_failures_column_to_bulk_imports.rb7
-rw-r--r--db/migrate/20230216152912_add_has_failures_column_to_bulk_import_entities.rb7
-rw-r--r--db/post_migrate/20230131184319_update_billable_users_index_for_service_accounts.rb27
-rw-r--r--db/schema_migrations/202301311843191
-rw-r--r--db/schema_migrations/202302151310261
-rw-r--r--db/schema_migrations/202302161529121
-rw-r--r--db/structure.sql4
-rw-r--r--doc/administration/logs/index.md18
-rw-r--r--doc/api/managed_licenses.md5
-rw-r--r--doc/development/fe_guide/vue.md8
-rw-r--r--doc/topics/your_work.md2
-rw-r--r--doc/update/deprecations.md22
-rw-r--r--doc/user/compliance/license_approval_policies.md9
-rw-r--r--doc/user/profile/index.md9
-rw-r--r--doc/user/project/code_owners.md56
-rw-r--r--doc/user/project/img/multi_approvals_code_owners_sections_v15_9.pngbin0 -> 18972 bytes
-rw-r--r--doc/user/project/issues/managing_issues.md8
-rw-r--r--doc/user/tasks.md10
-rw-r--r--lib/gitlab/ci/pipeline/duration.rb31
-rw-r--r--lib/gitlab/ci/resource_groups/logger.rb13
-rw-r--r--lib/gitlab/pages/random_domain.rb51
-rw-r--r--lib/object_storage/config.rb12
-rw-r--r--locale/gitlab.pot17
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/ci_variable/custom_variable_spec.rb5
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb5
-rw-r--r--spec/controllers/projects/pages_controller_spec.rb121
-rw-r--r--spec/factories/users.rb4
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb4
-rw-r--r--spec/frontend/issues/show/components/description_spec.js392
-rw-r--r--spec/lib/gitlab/ci/pipeline/duration_spec.rb204
-rw-r--r--spec/lib/gitlab/pages/random_domain_spec.rb44
-rw-r--r--spec/lib/object_storage/config_spec.rb7
-rw-r--r--spec/models/concerns/has_user_type_spec.rb4
-rw-r--r--spec/models/project_setting_spec.rb38
-rw-r--r--spec/models/project_spec.rb185
-rw-r--r--spec/policies/global_policy_spec.rb31
-rw-r--r--spec/services/markup/rendering_service_spec.rb17
-rw-r--r--spec/services/projects/update_service_spec.rb106
56 files changed, 1230 insertions, 458 deletions
diff --git a/Gemfile b/Gemfile
index f837c9b7025..8b5a44a27fd 100644
--- a/Gemfile
+++ b/Gemfile
@@ -143,7 +143,7 @@ gem 'carrierwave', '~> 1.3'
gem 'mini_magick', '~> 4.10.1'
# for backups
-gem 'fog-aws', '~> 3.15'
+gem 'fog-aws', '~> 3.18'
# Locked until fog-google resolves https://github.com/fog/fog-google/issues/421.
# Also see config/initializers/fog_core_patch.rb.
gem 'fog-core', '= 2.1.0'
diff --git a/Gemfile.checksum b/Gemfile.checksum
index cfd4b470b8f..4c2ffae4949 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -179,7 +179,7 @@
{"name":"flipper-active_record","version":"0.25.0","platform":"ruby","checksum":"85a5c99465e2cc6a09e91931a9998b0dbd463cd6c80dd513129377132e3eb67f"},
{"name":"flipper-active_support_cache_store","version":"0.25.0","platform":"ruby","checksum":"7282bf994b08d1a076b65c6f3b51e3dc04fcb00fa6e7b20089e60db25c7b531b"},
{"name":"fog-aliyun","version":"0.4.0","platform":"ruby","checksum":"8f2334604beb781eafbb9cd5f50141fbb2c7eb77c7f2b01f45c2e04db0e5cc38"},
-{"name":"fog-aws","version":"3.15.0","platform":"ruby","checksum":"09752931ea0c6165b018e1a89253248d86b246645086ccf19bc44fabe3381e8c"},
+{"name":"fog-aws","version":"3.18.0","platform":"ruby","checksum":"f4c5880ecfbc4edbf711dfd41140f9f17dfc68b519546d121448d2d3a5584704"},
{"name":"fog-core","version":"2.1.0","platform":"ruby","checksum":"53e5d793554d7080d015ef13cd44b54027e421d924d9dba4ce3d83f95f37eda9"},
{"name":"fog-google","version":"1.19.0","platform":"ruby","checksum":"3c909a230837fe84117fffdfd927b523821b88f61d3aeab531e1417a9810f488"},
{"name":"fog-json","version":"1.2.0","platform":"ruby","checksum":"dd4f5ab362dbc72b687240bba9d2dd841d5dfe888a285797533f85c03ea548fe"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 95307b80a14..a9b48ca82e7 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -502,7 +502,7 @@ GEM
fog-json
ipaddress (~> 0.8)
xml-simple (~> 1.1)
- fog-aws (3.15.0)
+ fog-aws (3.18.0)
fog-core (~> 2.1)
fog-json (~> 1.1)
fog-xml (~> 0.1)
@@ -1663,7 +1663,7 @@ DEPENDENCIES
flipper-active_record (~> 0.25.0)
flipper-active_support_cache_store (~> 0.25.0)
fog-aliyun (~> 0.4)
- fog-aws (~> 3.15)
+ fog-aws (~> 3.18)
fog-core (= 2.1.0)
fog-google (~> 1.19)
fog-local (~> 0.8)
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index 188a6f6b15e..bca895bf764 100644
--- a/app/assets/javascripts/issues/show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -143,7 +143,7 @@ export default {
};
},
skip() {
- return !this.workItemId || !this.workItemsMvcEnabled;
+ return !this.workItemId;
},
},
workItemTypes: {
@@ -156,15 +156,9 @@ export default {
update(data) {
return data.workspace?.workItemTypes?.nodes;
},
- skip() {
- return !this.workItemsMvcEnabled;
- },
},
},
computed: {
- workItemsMvcEnabled() {
- return this.glFeatures.workItemsMvc;
- },
taskWorkItemType() {
return this.workItemTypes.find((type) => type.name === TASK_TYPE_NAME)?.id;
},
@@ -194,8 +188,7 @@ export default {
this.renderGFM();
this.updateTaskStatusText();
-
- if (this.workItemId && this.workItemsMvcEnabled) {
+ if (this.workItemId) {
const taskLink = this.$el.querySelector(
`.gfm-issue[data-issue="${getIdFromGraphQLId(this.workItemId)}"]`,
);
@@ -228,9 +221,7 @@ export default {
this.renderSortableLists();
- if (this.workItemsMvcEnabled) {
- this.renderTaskListItemActions();
- }
+ this.renderTaskListItemActions();
}
},
renderSortableLists() {
diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb
index 3d0b48c2fc1..13c2a3ab750 100644
--- a/app/controllers/projects/pages_controller.rb
+++ b/app/controllers/projects/pages_controller.rb
@@ -75,7 +75,15 @@ class Projects::PagesController < Projects::ApplicationController
end
def project_params_attributes
- %i[pages_https_only]
+ attributes = %i[pages_https_only]
+
+ return attributes unless Feature.enabled?(:pages_unique_domain)
+
+ attributes + [
+ project_setting_attributes: [
+ :pages_unique_domain_enabled
+ ]
+ ]
end
end
diff --git a/app/models/ci/resource_group.rb b/app/models/ci/resource_group.rb
index b788e4f58c1..a220aa7bb18 100644
--- a/app/models/ci/resource_group.rb
+++ b/app/models/ci/resource_group.rb
@@ -29,13 +29,19 @@ module Ci
partition_id: processable.partition_id
}
- resources.free.limit(1).update_all(attrs) > 0
+ success = resources.free.limit(1).update_all(attrs) > 0
+ log_event(success: success, processable: processable, action: "assign resource to processable")
+
+ success
end
def release_resource_from(processable)
attrs = { build_id: nil, partition_id: nil }
- resources.retained_by(processable).update_all(attrs) > 0
+ success = resources.retained_by(processable).update_all(attrs) > 0
+ log_event(success: success, processable: processable, action: "release resource from processable")
+
+ success
end
def upcoming_processables
@@ -72,5 +78,14 @@ module Ci
# belong to the same resource group are executed once at time.
self.resources.build if self.resources.empty?
end
+
+ def log_event(success:, processable:, action:)
+ Gitlab::Ci::ResourceGroups::Logger.build.info({
+ resource_group_id: self.id,
+ processable_id: processable.id,
+ message: "attempted to #{action}",
+ success: success
+ })
+ end
end
end
diff --git a/app/models/concerns/has_user_type.rb b/app/models/concerns/has_user_type.rb
index b02c95c9662..752be378ab0 100644
--- a/app/models/concerns/has_user_type.rb
+++ b/app/models/concerns/has_user_type.rb
@@ -15,7 +15,8 @@ module HasUserType
security_bot: 8,
automation_bot: 9,
admin_bot: 11,
- suggested_reviewers_bot: 12
+ suggested_reviewers_bot: 12,
+ service_account: 13
}.with_indifferent_access.freeze
BOT_USER_TYPES = %w[
@@ -28,9 +29,12 @@ module HasUserType
automation_bot
admin_bot
suggested_reviewers_bot
+ service_account
].freeze
- NON_INTERNAL_USER_TYPES = %w[human project_bot service_user].freeze
+ # `service_account` allows instance/namespaces to configure a user for external integrations/automations
+ # `service_user` is an internal, `gitlab-com`-specific user type for integrations like suggested reviewers
+ NON_INTERNAL_USER_TYPES = %w[human project_bot service_user service_account].freeze
INTERNAL_USER_TYPES = (USER_TYPES.keys - NON_INTERNAL_USER_TYPES).freeze
included do
@@ -53,10 +57,8 @@ module HasUserType
BOT_USER_TYPES.include?(user_type)
end
- # The explicit check for project_bot will be removed with Bot Categorization
- # Ref: https://gitlab.com/gitlab-org/gitlab/-/issues/213945
def internal?
- ghost? || (bot? && !project_bot?)
+ INTERNAL_USER_TYPES.include?(user_type)
end
def redacted_name(viewing_user)
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 9d9b09e3562..c3bff24cb1a 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -479,9 +479,13 @@ class Namespace < ApplicationRecord
end
Pages::VirtualDomain.new(
- projects: all_projects_with_pages.includes(:route, :project_feature, pages_metadatum: :pages_deployment),
trim_prefix: full_path,
- cache: cache
+ cache: cache,
+ projects: all_projects_with_pages.includes(
+ :route,
+ :project_setting,
+ :project_feature,
+ pages_metadatum: :pages_deployment)
)
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 43ec26be786..1909ed8dfdd 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -2116,14 +2116,9 @@ class Project < ApplicationRecord
pages_metadatum&.deployed?
end
- def pages_namespace_url
- # The host in URL always needs to be downcased
- Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix|
- "#{prefix}#{pages_subdomain}."
- end.downcase
- end
-
def pages_url
+ return pages_unique_url if pages_unique_domain_enabled?
+
url = pages_namespace_url
url_path = full_path.partition('/').last
namespace_url = "#{Settings.pages.protocol}://#{url_path}".downcase
@@ -2141,6 +2136,14 @@ class Project < ApplicationRecord
"#{url}/#{url_path}"
end
+ def pages_unique_url
+ pages_url_for(project_setting.pages_unique_domain)
+ end
+
+ def pages_namespace_url
+ pages_url_for(pages_subdomain)
+ end
+
def pages_subdomain
full_path.partition('/').first
end
@@ -3121,6 +3124,18 @@ class Project < ApplicationRecord
private
+ def pages_unique_domain_enabled?
+ Feature.enabled?(:pages_unique_domain) &&
+ project_setting.pages_unique_domain_enabled?
+ end
+
+ def pages_url_for(domain)
+ # The host in URL always needs to be downcased
+ Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix|
+ "#{prefix}#{domain}."
+ end.downcase
+ end
+
# overridden in EE
def project_group_links_with_preload
project_group_links
diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb
index db86bb5e1fb..379b94b3af5 100644
--- a/app/models/project_setting.rb
+++ b/app/models/project_setting.rb
@@ -25,6 +25,10 @@ class ProjectSetting < ApplicationRecord
validates :target_platforms, inclusion: { in: ALLOWED_TARGET_PLATFORMS }
validates :suggested_reviewers_enabled, inclusion: { in: [true, false] }
+ validates :pages_unique_domain,
+ uniqueness: { if: -> { pages_unique_domain.present? } },
+ presence: { if: :require_unique_domain? }
+
validate :validates_mr_default_target_self
attribute :legacy_open_source_license_available, default: -> do
@@ -68,6 +72,11 @@ class ProjectSetting < ApplicationRecord
errors.add :mr_default_target_self, _('This setting is allowed for forked projects only')
end
end
+
+ def require_unique_domain?
+ pages_unique_domain_enabled ||
+ pages_unique_domain_in_database.present?
+ end
end
ProjectSetting.prepend_mod
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index d028738ccc9..7afecb2c4dd 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -26,6 +26,8 @@ class GlobalPolicy < BasePolicy
Feature.enabled?(:create_runner_workflow)
end
+ condition(:service_account, scope: :user) { @user&.service_account? }
+
rule { anonymous }.policy do
prevent :log_in
prevent :receive_notifications
@@ -64,7 +66,7 @@ class GlobalPolicy < BasePolicy
prevent :access_git
end
- rule { project_bot }.policy do
+ rule { project_bot | service_account }.policy do
prevent :log_in
prevent :receive_notifications
end
diff --git a/app/services/markup/rendering_service.rb b/app/services/markup/rendering_service.rb
index cd89c170efa..104bdb6dd41 100644
--- a/app/services/markup/rendering_service.rb
+++ b/app/services/markup/rendering_service.rb
@@ -52,6 +52,8 @@ module Markup
def other_markup_unsafe
Gitlab::OtherMarkup.render(file_name, text, context)
+ rescue GitHub::Markup::CommandError
+ ActionController::Base.helpers.simple_format(text)
end
def postprocess(html)
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 301d11d841c..bea994e8bb2 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -10,6 +10,8 @@ module Projects
def execute
build_topics
remove_unallowed_params
+ add_pages_unique_domain
+
validate!
ensure_wiki_exists if enabling_wiki?
@@ -48,6 +50,24 @@ module Projects
private
+ def add_pages_unique_domain
+ if Feature.disabled?(:pages_unique_domain)
+ params[:project_setting_attributes]&.delete(:pages_unique_domain_enabled)
+
+ return
+ end
+
+ return unless params.dig(:project_setting_attributes, :pages_unique_domain_enabled)
+
+ # If the project used a unique domain once, it'll always use the same
+ return if project.project_setting.pages_unique_domain_in_database.present?
+
+ params[:project_setting_attributes][:pages_unique_domain] = Gitlab::Pages::RandomDomain.generate(
+ project_path: project.path,
+ namespace_path: project.parent.full_path
+ )
+ end
+
def validate!
unless valid_visibility_level_change?(project, project.visibility_attribute_value(params))
raise ValidationError, s_('UpdateProject|New visibility level not allowed!')
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 2d40a566608..5a3e94afc63 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -8,7 +8,7 @@
.row.gl-mt-3
.col-lg-4
%h4.gl-mt-0
- = _('Register Two-Factor Authenticator')
+ = _('Register a one-time password authenticator')
%p
= _('Use a one-time password authenticator on your mobile device or computer to enable two-factor authentication (2FA).')
.col-lg-8
@@ -76,13 +76,15 @@
.col-lg-4
%h4.gl-mt-0
- if webauthn_enabled
- = _('Register WebAuthn Device')
+ = _('Register a WebAuthn device')
- else
- = _('Register Universal Two-Factor (U2F) Device')
+ = _('Register a universal two-factor (U2F) device')
%p
- = _('Set up a hardware device as a second factor to sign in.')
+ = _('Set up a hardware device to enable two-factor authentication (2FA).')
%p
- - if webauthn_enabled
+ - if webauthn_enabled && Feature.enabled?(:webauthn_without_totp)
+ = _("Not all browsers support WebAuthn. You must save your recovery codes after you first register a two-factor authenticator to be able to sign in, even from an unsupported browser.")
+ - elsif webauthn_enabled
= _("Not all browsers support WebAuthn. Therefore, we require that you set up a two-factor authentication app first. That way you'll always be able to sign in - even from an unsupported browser.")
- else
= _("Not all browsers support U2F devices. Therefore, we require that you set up a two-factor authentication app first. That way you'll always be able to sign in - even when you're using an unsupported browser.")
diff --git a/app/views/projects/pages/_pages_settings.html.haml b/app/views/projects/pages/_pages_settings.html.haml
index 0010564081e..e60cdf754da 100644
--- a/app/views/projects/pages/_pages_settings.html.haml
+++ b/app/views/projects/pages/_pages_settings.html.haml
@@ -17,5 +17,14 @@
%p.gl-pl-6
= s_("GitLabPages|When enabled, all attempts to visit your website through HTTP are automatically redirected to HTTPS using a response with status code 301. Requires a valid certificate for all domains. %{docs_link_start}Learn more.%{link_end}").html_safe % { docs_link_start: docs_link_start, link_end: link_end }
+ - if Feature.enabled?(:pages_unique_domain)
+ .form-group
+ = f.fields_for :project_setting do |settings|
+ = settings.gitlab_ui_checkbox_component :pages_unique_domain_enabled,
+ s_('GitLabPages|Use unique domain'),
+ label_options: { class: 'label-bold' }
+ %p.gl-pl-6
+ = s_("GitLabPages|When enabled, a unique domain is generated to access pages.").html_safe
+
.gl-mt-3
= f.submit s_('GitLabPages|Save changes'), class: 'btn btn-confirm gl-button'
diff --git a/config/feature_flags/development/ci_use_downstream_pipeline_duration_for_calculation.yml b/config/feature_flags/development/ci_use_downstream_pipeline_duration_for_calculation.yml
new file mode 100644
index 00000000000..b4cc97c181e
--- /dev/null
+++ b/config/feature_flags/development/ci_use_downstream_pipeline_duration_for_calculation.yml
@@ -0,0 +1,8 @@
+---
+name: ci_use_downstream_pipeline_duration_for_calculation
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109445
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/388776
+milestone: '15.9'
+type: development
+group: group::pipeline execution
+default_enabled: false
diff --git a/config/feature_flags/development/pages_unique_domain.yml b/config/feature_flags/development/pages_unique_domain.yml
new file mode 100644
index 00000000000..7894cf5ceed
--- /dev/null
+++ b/config/feature_flags/development/pages_unique_domain.yml
@@ -0,0 +1,8 @@
+---
+name: pages_unique_domain
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109011
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/388151
+milestone: '15.9'
+type: development
+group: group::editor
+default_enabled: false
diff --git a/data/deprecations/15-9-ci-builds-column-validations.yml b/data/deprecations/15-9-ci-builds-column-validations.yml
new file mode 100644
index 00000000000..227b3145320
--- /dev/null
+++ b/data/deprecations/15-9-ci-builds-column-validations.yml
@@ -0,0 +1,29 @@
+#
+# REQUIRED FIELDS
+#
+- title: "Enforced validation of CI/CD parameter character lengths" # (required) Clearly explain the change. For example, "The `confidential` field for a `Note` is removed" or "CI/CD job names are limited to 250 characters."
+ announcement_milestone: "15.9" # (required) The milestone when this feature was deprecated.
+ announcement_date: "2023-02-22" # (required) The date of the milestone release when this feature was deprecated. This should almost always be the 22nd of a month (YYYY-MM-DD), unless you did an out of band blog post.
+ removal_milestone: "16.0" # (required) The milestone when this feature is being removed.
+ removal_date: "2023-05-22" # (required) This should almost always be the 22nd of a month (YYYY-MM-DD), the date of the milestone release when this feature will be removed.
+ breaking_change: true # (required) Change to false if this is not a breaking change.
+ reporter: jheimbuck_gl # (required) GitLab username of the person reporting the removal
+ stage: Verify # (required) String value of the stage that the feature was created in. e.g., Growth
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/372770 # (required) Link to the deprecation issue in GitLab
+ body: | # (required) Do not modify this line, instead modify the lines below.
+ While CI/CD [job names](https://docs.gitlab.com/ee/ci/jobs/index.html#job-name-limitations) have a strict 255 character limit, other CI/CD parameters do not yet have validations ensuring they also stay under the limit.
+
+ In GitLab 16.0, validation will be added to strictly limit the following to 255 characters as well:
+
+ - The `stage` keyword.
+ - The `ref`, which is the Git branch or tag name for the pipeline.
+ - The `description` and `target_url` parameter, used by external CI/CD integrations.
+
+ Users on self-managed instances should update their pipelines to ensure they do not use parameters that exceed 255 characters. Users on GitLab.com do not need to make any changes, as these are already limited in that database.
+#
+# OPTIONAL FIELDS
+#
+ tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
+ documentation_url: "https://docs.gitlab.com/ee/ci/yaml/#stages" # (optional) This is a link to the current documentation page
+ image_url: # (optional) This is a link to a thumbnail image depicting the feature
+ video_url: # (optional) Use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
diff --git a/db/migrate/20230215131026_add_has_failures_column_to_bulk_imports.rb b/db/migrate/20230215131026_add_has_failures_column_to_bulk_imports.rb
new file mode 100644
index 00000000000..52517244f3e
--- /dev/null
+++ b/db/migrate/20230215131026_add_has_failures_column_to_bulk_imports.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddHasFailuresColumnToBulkImports < Gitlab::Database::Migration[2.1]
+ def change
+ add_column :bulk_imports, :has_failures, :boolean, default: false
+ end
+end
diff --git a/db/migrate/20230216152912_add_has_failures_column_to_bulk_import_entities.rb b/db/migrate/20230216152912_add_has_failures_column_to_bulk_import_entities.rb
new file mode 100644
index 00000000000..4c48acd9dce
--- /dev/null
+++ b/db/migrate/20230216152912_add_has_failures_column_to_bulk_import_entities.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddHasFailuresColumnToBulkImportEntities < Gitlab::Database::Migration[2.1]
+ def change
+ add_column :bulk_import_entities, :has_failures, :boolean, default: false
+ end
+end
diff --git a/db/post_migrate/20230131184319_update_billable_users_index_for_service_accounts.rb b/db/post_migrate/20230131184319_update_billable_users_index_for_service_accounts.rb
new file mode 100644
index 00000000000..e86a2476156
--- /dev/null
+++ b/db/post_migrate/20230131184319_update_billable_users_index_for_service_accounts.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class UpdateBillableUsersIndexForServiceAccounts < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ OLD_INDEX = 'index_users_for_billable_users'
+ NEW_INDEX = 'index_users_for_active_billable_users'
+
+ OLD_INDEX_CONDITION = <<~QUERY
+ ((state)::text = 'active'::text) AND ((user_type IS NULL)
+ OR (user_type = ANY (ARRAY[6, 4]))) AND
+ ((user_type IS NULL) OR (user_type = ANY (ARRAY[4, 5])))
+ QUERY
+ NEW_INDEX_CONDITION = <<~QUERY
+ state = 'active' AND (user_type IS NULL OR user_type IN (6, 4, 13)) AND (user_type IS NULL OR user_type IN (4, 5))
+ QUERY
+
+ def up
+ add_concurrent_index(:users, :id, where: NEW_INDEX_CONDITION, name: NEW_INDEX)
+ remove_concurrent_index_by_name(:users, OLD_INDEX)
+ end
+
+ def down
+ add_concurrent_index(:users, :id, where: OLD_INDEX_CONDITION, name: OLD_INDEX)
+ remove_concurrent_index_by_name(:users, NEW_INDEX)
+ end
+end
diff --git a/db/schema_migrations/20230131184319 b/db/schema_migrations/20230131184319
new file mode 100644
index 00000000000..3028f92b316
--- /dev/null
+++ b/db/schema_migrations/20230131184319
@@ -0,0 +1 @@
+06a6005ecc7de9b6db9912b246aa27c30b308f47f23f1258043b7a7c636962b6 \ No newline at end of file
diff --git a/db/schema_migrations/20230215131026 b/db/schema_migrations/20230215131026
new file mode 100644
index 00000000000..3bec8e04f4f
--- /dev/null
+++ b/db/schema_migrations/20230215131026
@@ -0,0 +1 @@
+095cc516f50dcb11e01ccda962a9776fddcec439520cef795f6c8715b5941aba \ No newline at end of file
diff --git a/db/schema_migrations/20230216152912 b/db/schema_migrations/20230216152912
new file mode 100644
index 00000000000..e9f1dfb9db3
--- /dev/null
+++ b/db/schema_migrations/20230216152912
@@ -0,0 +1 @@
+66b74e0442763b2a05ec411344d8ca97b7d3d2e8cef9d2e04baba246b1c025a2 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index e1d3e46161f..b4b0b2f83b0 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -12662,6 +12662,7 @@ CREATE TABLE bulk_import_entities (
updated_at timestamp with time zone NOT NULL,
source_xid integer,
migrate_projects boolean DEFAULT true NOT NULL,
+ has_failures boolean DEFAULT false,
CONSTRAINT check_13f279f7da CHECK ((char_length(source_full_path) <= 255)),
CONSTRAINT check_715d725ea2 CHECK ((char_length(destination_name) <= 255)),
CONSTRAINT check_796a4d9cc6 CHECK ((char_length(jid) <= 255)),
@@ -12778,6 +12779,7 @@ CREATE TABLE bulk_imports (
updated_at timestamp with time zone NOT NULL,
source_version text,
source_enterprise boolean DEFAULT true NOT NULL,
+ has_failures boolean DEFAULT false,
CONSTRAINT check_ea4e58775a CHECK ((char_length(source_version) <= 63))
);
@@ -31981,7 +31983,7 @@ CREATE INDEX index_user_statuses_on_user_id ON user_statuses USING btree (user_i
CREATE UNIQUE INDEX index_user_synced_attributes_metadata_on_user_id ON user_synced_attributes_metadata USING btree (user_id);
-CREATE INDEX index_users_for_billable_users ON users USING btree (id) WHERE (((state)::text = 'active'::text) AND ((user_type IS NULL) OR (user_type = ANY (ARRAY[6, 4]))) AND ((user_type IS NULL) OR (user_type = ANY (ARRAY[4, 5]))));
+CREATE INDEX index_users_for_active_billable_users ON users USING btree (id) WHERE (((state)::text = 'active'::text) AND ((user_type IS NULL) OR (user_type = ANY (ARRAY[6, 4, 13]))) AND ((user_type IS NULL) OR (user_type = ANY (ARRAY[4, 5]))));
CREATE INDEX index_users_on_accepted_term_id ON users USING btree (accepted_term_id);
diff --git a/doc/administration/logs/index.md b/doc/administration/logs/index.md
index eab4c9b7d83..916b50a410d 100644
--- a/doc/administration/logs/index.md
+++ b/doc/administration/logs/index.md
@@ -771,6 +771,24 @@ are recorded in this file. For example:
{"severity":"INFO","time":"2020-11-24T02:31:29.329Z","correlation_id":null,"key":"cd_auto_rollback","action":"remove"}
```
+## `ci_resource_groups_json.log`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/384180) in GitLab 15.9.
+
+Depending on your installation method, this file is located at:
+
+- Omnibus GitLab: `/var/log/gitlab/gitlab-rails/ci_resource_group_json.log`
+- Installations from source: `/home/git/gitlab/log/ci_resource_group_json.log`
+
+It contains information about [resource group](../../ci/resource_groups/index.md) acquisition. For example:
+
+```json
+{"severity":"INFO","time":"2023-02-10T23:02:06.095Z","correlation_id":"01GRYS10C2DZQ9J1G12ZVAD4YD","resource_group_id":1,"processable_id":288,"message":"attempted to assign resource to processable","success":true}
+{"severity":"INFO","time":"2023-02-10T23:02:08.945Z","correlation_id":"01GRYS138MYEG32C0QEWMC4BDM","resource_group_id":1,"processable_id":288,"message":"attempted to release resource from processable","success":true}
+```
+
+The examples show the `resource_group_id`, `processable_id`, `message`, and `success` fields for each entry.
+
## `auth.log`
> Introduced in GitLab 12.0.
diff --git a/doc/api/managed_licenses.md b/doc/api/managed_licenses.md
index 6aee60c57e0..17da038afaa 100644
--- a/doc/api/managed_licenses.md
+++ b/doc/api/managed_licenses.md
@@ -4,7 +4,10 @@ group: Utilization
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
-# Managed Licenses API **(ULTIMATE)**
+# Managed Licenses API (DEPRECATED) **(ULTIMATE)**
+
+WARNING:
+This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/390417) in GitLab 15.9.
WARNING:
"approval" and "blacklisted" approval statuses are changed to "allowed" and "denied" in GitLab 15.0.
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index cea47bc0e4c..ded44fd910b 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -76,7 +76,13 @@ component, is that you avoid creating a fixture or an HTML element in the unit t
`initSimpleApp` is a helper function that streamlines the process of mounting a component in Vue.js. It accepts two arguments: a selector string representing the mount point in the HTML, and a Vue component.
-To use `initSimpleApp`, include the HTML element in the page with the appropriate selector and add a data-view-model attribute containing a JSON object. Then, import the desired Vue component and pass it along with the selector to `initSimpleApp`. This mounts the component at the specified location.
+To use `initSimpleApp`:
+
+1. Include an HTML element in the page with an ID or unique class.
+1. Add a data-view-model attribute containing a JSON object.
+1. Import the desired Vue component, and pass it along with a valid CSS selector string
+ that selects the HTML element to `initSimpleApp`. This string mounts the component
+ at the specified location.
`initSimpleApp` automatically retrieves the content of the data-view-model attribute as a JSON object and passes it as props to the mounted Vue component. This can be used to pre-populate the component with data.
diff --git a/doc/topics/your_work.md b/doc/topics/your_work.md
index 862f9ae8430..fbdb6f327cf 100644
--- a/doc/topics/your_work.md
+++ b/doc/topics/your_work.md
@@ -16,3 +16,5 @@ The **Your work** left sidebar provides access to your:
- [Merge requests](../user/project/merge_requests/index.md)
- [To-do List](../user/todos.md)
- [Milestones](../user/project/milestones/index.md)
+- [Snippets](../user/snippets.md#snippets)
+- [Activity](../user/profile/index.md#user-activity)
diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md
index f212316fa16..2f4f9f060a1 100644
--- a/doc/update/deprecations.md
+++ b/doc/update/deprecations.md
@@ -141,6 +141,28 @@ We intend to replace this feature with the ability to [embed charts](https://git
<div class="deprecation removal-160 breaking-change">
+### Enforced validation of CI/CD parameter character lengths
+
+Planned removal: GitLab <span class="removal-milestone">16.0</span> <span class="removal-date"></span>
+
+WARNING:
+This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
+Review the details carefully before upgrading.
+
+While CI/CD [job names](https://docs.gitlab.com/ee/ci/jobs/index.html#job-name-limitations) have a strict 255 character limit, other CI/CD parameters do not yet have validations ensuring they also stay under the limit.
+
+In GitLab 16.0, validation will be added to strictly limit the following to 255 characters as well:
+
+- The `stage` keyword.
+- The `ref`, which is the Git branch or tag name for the pipeline.
+- The `description` and `target_url` parameter, used by external CI/CD integrations.
+
+Users on self-managed instances should update their pipelines to ensure they do not use parameters that exceed 255 characters. Users on GitLab.com do not need to make any changes, as these are already limited in that database.
+
+</div>
+
+<div class="deprecation removal-160 breaking-change">
+
### Error Tracking UI in GitLab Rails is deprecated
Planned removal: GitLab <span class="removal-milestone">16.0</span> <span class="removal-date"></span>
diff --git a/doc/user/compliance/license_approval_policies.md b/doc/user/compliance/license_approval_policies.md
index 32c90a1d317..3e43869aabd 100644
--- a/doc/user/compliance/license_approval_policies.md
+++ b/doc/user/compliance/license_approval_policies.md
@@ -9,7 +9,14 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/8092) in GitLab 15.9 [with a flag](../../administration/feature_flags.md) named `license_scanning_policies`. Disabled by default.
-License Approval Policies allow you to specify multiple types of criteria that define when approval is required before a merge request can be merged in.
+License Approval Policies allow you to specify multiple types of criteria that define when approval is required before a merge request can be merged in. The following video provides an overview of these policies.
+
+<div class="video-fallback">
+ See the video: <a href="https://www.youtube.com/watch?v=34qBQ9t8qO8">Overview of GitLab License Approval Policies</a>.
+</div>
+<figure class="video-container">
+ <iframe src="https://www.youtube-nocookie.com/embed/34qBQ9t8qO8" frameborder="0" allowfullscreen> </iframe>
+</figure>
## Create a new license approval policy
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index 90a45d85c49..f6a6253b4c0 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -312,7 +312,14 @@ git config --global user.email <your email address>
## User activity
-GitLab tracks [user contribution activity](contributions_calendar.md). You can follow or unfollow other users from either:
+GitLab tracks [user contribution activity](contributions_calendar.md).
+You can view your own activity by clicking **Activity** in the
+[**Your work**](../../topics/your_work.md) sidebar, or by visiting your
+[profile page](#access-your-user-profile).
+
+### Follow users' activity
+
+You can follow or unfollow other users from either:
- Their [user profiles](#access-your-user-profile).
- The small popover that appears when you hover over a user's name ([introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76050)
diff --git a/doc/user/project/code_owners.md b/doc/user/project/code_owners.md
index 9de9d445965..44cfc232513 100644
--- a/doc/user/project/code_owners.md
+++ b/doc/user/project/code_owners.md
@@ -86,7 +86,7 @@ Next steps:
- [Add Code Owners as merge request approvers](merge_requests/approvals/rules.md#code-owners-as-eligible-approvers).
- Set up [Code Owner approval on a protected branch](protected_branches.md#require-code-owner-approval-on-a-protected-branch).
-## Groups as Code Owners
+### Groups as Code Owners
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/53182) in GitLab 12.1.
> - Group and subgroup hierarchy support was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32432) in GitLab 13.0.
@@ -137,7 +137,7 @@ For approval to be required, groups as Code Owners must have a direct membership
that inherit membership. Members in the Code Owners group also must be direct members,
and not inherit membership from any parent groups.
-### Add a group as a Code Owner
+#### Add a group as a Code Owner
To set a group as a Code Owner:
@@ -154,23 +154,26 @@ file.md @group-x/subgroup-y
file.md @group-x @group-x/subgroup-y
```
-## When a file matches multiple `CODEOWNERS` entries
+### Define more specific owners for more specifically defined files or directories
-When a file matches multiple entries in the `CODEOWNERS` file,
-the users from last pattern matching the file are used.
+When a file or directory matches multiple entries in the `CODEOWNERS` file,
+the users from last pattern matching the file or directory are used. This enables you
+to define more specific owners for more specifically defined files or directories, when
+you order the entries in a sensible way.
For example, in the following `CODEOWNERS` file:
```plaintext
-README.md @user1
+# This line would match the file terms.md
+*.md @doc-team
-# This line would also match the file README.md
-*.md @user2
+# This line would also match the file terms.md
+terms.md @legal-team
```
-The Code Owner for `README.md` would be `@user2`.
+The Code Owner for `terms.md` would be `@legal-team`.
-If you use sections, the last pattern matching the file for each section is used.
+If you use sections, the last pattern matching the file or directory for each section is used.
For example, in a `CODEOWNERS` file using sections:
```plaintext
@@ -183,9 +186,9 @@ README.md @user3
```
The Code Owners for the `README.md` in the root directory are `@user1`, `@user2`,
-and `@user3`. The Code Owners for `internal/README.md` are `@user4` and `@user3`.
+and `@user3`. The Code Owners for `internal/README.md` are `@user4` and `@user3`.
-Only one CODEOWNERS pattern can match per file path.
+Only one CODEOWNERS pattern per section will be matched to a file path.
### Organize Code Owners by putting them into sections
@@ -268,6 +271,35 @@ when changes are submitted by using merge requests. If a change is submitted dir
to the protected branch, approval from Code Owners is still required, even if the
section is marked as optional.
+### Require multiple approvals from Code Owners
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/335451) in GitLab 15.9.
+
+You can require multiple approvals for the Code Owners sections under the Approval Rules area in merge requests.
+Append the section name with a number `n` in brackets. This requires `n` approvals from the Code Owners in this section.
+Please note valid entries for `n` are integers `≥ 1`. `[1]` is optional as it is the default. Invalid values for `n` are treated as `1`.
+
+WARNING:
+[Issue #384881](https://gitlab.com/gitlab-org/gitlab/-/issues/385881) proposes changes
+to the behavior of this setting. Do not intentionally set invalid values. They may
+become valid in the future, and cause unexpected behavior.
+
+Please confirm you enabled `Require approval from code owners` in `Settings > Repository > Protected branches`, otherwise the Code Owner approvals will be optional.
+
+In this example, the `[Documentation]` section requires 2 approvals:
+
+```plaintext
+[Documentation][2]
+*.md @tech-writer-team
+
+[Ruby]
+*.rb @dev-team
+```
+
+The `Documentation` Code Owners section under the **Approval Rules** area displays 2 approvals are required:
+
+![MR widget - Multiple Approval Code Owners sections](img/multi_approvals_code_owners_sections_v15_9.png)
+
### Allowed to Push
The Code Owner approval and protected branch features do not apply to users who
diff --git a/doc/user/project/img/multi_approvals_code_owners_sections_v15_9.png b/doc/user/project/img/multi_approvals_code_owners_sections_v15_9.png
new file mode 100644
index 00000000000..a7fea76d5b1
--- /dev/null
+++ b/doc/user/project/img/multi_approvals_code_owners_sections_v15_9.png
Binary files differ
diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md
index 953d08ea903..c16074ea1d8 100644
--- a/doc/user/project/issues/managing_issues.md
+++ b/doc/user/project/issues/managing_issues.md
@@ -24,13 +24,7 @@ To edit an issue:
### Remove a task list item
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/377307) in GitLab 15.9 [with a flag](../../../administration/feature_flags.md) named `work_items_mvc`. Disabled by default.
-
-FLAG:
-On self-managed GitLab, by default this feature is not available.
-To make it available, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `work_items_mvc`.
-On GitLab.com, this feature is not available.
-The feature is not ready for production use.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/377307) in GitLab 15.9.
Prerequisites:
diff --git a/doc/user/tasks.md b/doc/user/tasks.md
index 42a3975a9d2..0fc4c7571ab 100644
--- a/doc/user/tasks.md
+++ b/doc/user/tasks.md
@@ -58,15 +58,9 @@ To create a task:
1. Enter the task title.
1. Select **Create task**.
-### Create a task from a task list item
+### From a task list item
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/377307) in GitLab 15.9 [with a flag](../administration/feature_flags.md) named `work_items_mvc`. Disabled by default.
-
-FLAG:
-On self-managed GitLab, by default this feature is not available.
-To make it available, ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `work_items_mvc`.
-On GitLab.com, this feature is not available.
-The feature is not ready for production use.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/377307) in GitLab 15.9.
Prerequisites:
diff --git a/lib/gitlab/ci/pipeline/duration.rb b/lib/gitlab/ci/pipeline/duration.rb
index e8a991026b5..15cc238fa84 100644
--- a/lib/gitlab/ci/pipeline/duration.rb
+++ b/lib/gitlab/ci/pipeline/duration.rb
@@ -82,6 +82,8 @@ module Gitlab
module Duration
extend self
+ STATUSES = %w[success failed running canceled].freeze
+
Period = Struct.new(:first, :last) do
def duration
last - first
@@ -90,14 +92,20 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def from_pipeline(pipeline)
- status = %w[success failed running canceled]
- builds = pipeline.processables.latest
- .where(status: status).where.not(started_at: nil).order(:started_at)
+ builds =
+ if Feature.enabled?(:ci_use_downstream_pipeline_duration_for_calculation, pipeline.project)
+ self_and_downstreams_builds_of_pipeline(pipeline)
+ else
+ pipeline.processables.latest
+ .with_status(STATUSES).where.not(started_at: nil).order(:started_at)
+ end
from_builds(builds)
end
# rubocop: enable CodeReuse/ActiveRecord
+ private
+
def from_builds(builds)
now = Time.now
@@ -113,8 +121,6 @@ module Gitlab
process_duration(process_periods(periods))
end
- private
-
def process_periods(periods)
return periods if periods.empty?
@@ -139,6 +145,21 @@ module Gitlab
end
# rubocop: disable CodeReuse/ActiveRecord
+ def self_and_downstreams_builds_of_pipeline(pipeline)
+ ::Ci::Build
+ .unscoped # Will be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/391186
+ .select(:id, :type, :started_at, :finished_at)
+ .in_pipelines(
+ pipeline.self_and_downstreams.select(:id)
+ )
+ .with_status(STATUSES)
+ .latest
+ .where.not(started_at: nil)
+ .order(:started_at)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ # rubocop: disable CodeReuse/ActiveRecord
def process_duration(periods)
periods.sum(&:duration)
end
diff --git a/lib/gitlab/ci/resource_groups/logger.rb b/lib/gitlab/ci/resource_groups/logger.rb
new file mode 100644
index 00000000000..9c93ee95bc7
--- /dev/null
+++ b/lib/gitlab/ci/resource_groups/logger.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module ResourceGroups
+ class Logger < ::Gitlab::JsonLogger
+ def self.file_name_noext
+ 'ci_resource_groups_json'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/pages/random_domain.rb b/lib/gitlab/pages/random_domain.rb
new file mode 100644
index 00000000000..8aa7611c910
--- /dev/null
+++ b/lib/gitlab/pages/random_domain.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Pages
+ class RandomDomain
+ PROJECT_PATH_LIMIT = 48
+ SUBDOMAIN_LABEL_LIMIT = 63
+
+ def self.generate(project_path:, namespace_path:)
+ new(project_path: project_path, namespace_path: namespace_path).generate
+ end
+
+ def initialize(project_path:, namespace_path:)
+ @project_path = project_path
+ @namespace_path = namespace_path
+ end
+
+ # Subdomains have a limit of 63 bytes (https://www.freesoft.org/CIE/RFC/1035/9.htm)
+ # For this reason we're limiting each part of the unique subdomain
+ #
+ # The domain is made up of 3 parts, like: projectpath-namespacepath-randomstring
+ # - project path: between 1 and 48 chars
+ # - namespace path: when the project path has less than 48 chars,
+ # the namespace full path will be used to fill the value up to 48 chars
+ # - random hexadecimal: to ensure a random value, the domain is then filled
+ # with a random hexadecimal value to complete 63 chars
+ def generate
+ domain = project_path.byteslice(0, PROJECT_PATH_LIMIT)
+
+ # if the project_path has less than PROJECT_PATH_LIMIT chars,
+ # fill the domain with the parent full_path up to 48 chars like:
+ # projectpath-namespacepath
+ if domain.length < PROJECT_PATH_LIMIT
+ namespace_size = PROJECT_PATH_LIMIT - domain.length - 1
+ domain.concat('-', namespace_path.byteslice(0, namespace_size))
+ end
+
+ # Complete the domain with random hexadecimal values util it is 63 chars long
+ # PS.: SecureRandom.hex return an string twice the size passed as argument.
+ domain.concat('-', SecureRandom.hex(SUBDOMAIN_LABEL_LIMIT - domain.length - 1))
+
+ # Slugify ensures the format and size (63 chars) of the given string
+ Gitlab::Utils.slugify(domain)
+ end
+
+ private
+
+ attr_reader :project_path, :namespace_path
+ end
+ end
+end
diff --git a/lib/object_storage/config.rb b/lib/object_storage/config.rb
index 056e22278dd..fb0334959a3 100644
--- a/lib/object_storage/config.rb
+++ b/lib/object_storage/config.rb
@@ -13,7 +13,7 @@ module ObjectStorage
end
def credentials
- @credentials ||= options[:connection] || {}
+ @credentials ||= connection_params
end
def storage_options
@@ -86,6 +86,16 @@ module ObjectStorage
private
+ def connection_params
+ base_params = options[:connection] || {}
+
+ return base_params unless base_params[:provider].to_s == AWS_PROVIDER
+ return base_params unless ::Gitlab::FIPS.enabled?
+
+ # In fog-aws, this disables the use of Content-Md5: https://github.com/fog/fog-aws/pull/668
+ base_params.merge({ disable_content_md5_validation: true })
+ end
+
# This returns a Hash of HTTP encryption headers to send along to S3.
#
# They can also be passed in as Fog::AWS::Storage::File attributes, since there
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 8391c00bc60..8fb74bcaf56 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -19112,12 +19112,18 @@ msgstr ""
msgid "GitLabPages|Updating your Pages configuration..."
msgstr ""
+msgid "GitLabPages|Use unique domain"
+msgstr ""
+
msgid "GitLabPages|Verified"
msgstr ""
msgid "GitLabPages|Waiting for the Pages Pipeline to complete..."
msgstr ""
+msgid "GitLabPages|When enabled, a unique domain is generated to access pages."
+msgstr ""
+
msgid "GitLabPages|When enabled, all attempts to visit your website through HTTP are automatically redirected to HTTPS using a response with status code 301. Requires a valid certificate for all domains. %{docs_link_start}Learn more.%{link_end}"
msgstr ""
@@ -28565,6 +28571,9 @@ msgstr ""
msgid "Not all browsers support WebAuthn. Therefore, we require that you set up a two-factor authentication app first. That way you'll always be able to sign in - even from an unsupported browser."
msgstr ""
+msgid "Not all browsers support WebAuthn. You must save your recovery codes after you first register a two-factor authenticator to be able to sign in, even from an unsupported browser."
+msgstr ""
+
msgid "Not all data has been processed yet, the accuracy of the chart for the selected timeframe is limited."
msgstr ""
@@ -35310,13 +35319,13 @@ msgstr ""
msgid "Register / Sign In"
msgstr ""
-msgid "Register Two-Factor Authenticator"
+msgid "Register a WebAuthn device"
msgstr ""
-msgid "Register Universal Two-Factor (U2F) Device"
+msgid "Register a one-time password authenticator"
msgstr ""
-msgid "Register WebAuthn Device"
+msgid "Register a universal two-factor (U2F) device"
msgstr ""
msgid "Register device"
@@ -39571,7 +39580,7 @@ msgstr ""
msgid "Set up a %{type} runner for a project"
msgstr ""
-msgid "Set up a hardware device as a second factor to sign in."
+msgid "Set up a hardware device to enable two-factor authentication (2FA)."
msgstr ""
msgid "Set up assertions/attributes/claims (email, first_name, last_name) and NameID according to %{docsLinkStart}the documentation %{icon}%{docsLinkEnd}"
diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/custom_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/custom_variable_spec.rb
index 2ae28d54242..cc6f4a9ddfd 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/custom_variable_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/custom_variable_spec.rb
@@ -51,11 +51,6 @@ module QA
project.visit!
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button)
-
- # Sometimes the variables will not be prefilled because of reactive cache so we revisit the page again.
- # TODO: Investigate alternatives to deal with cache implementation
- # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/381233
- page.refresh
end
after do
diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb
index 1878292015e..eb609403007 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb
@@ -54,11 +54,6 @@ module QA
# Navigate to Run Pipeline page
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button)
-
- # Sometimes the variables will not be prefilled because of reactive cache so we revisit the page again.
- # TODO: Investigate alternatives to deal with cache implementation
- # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/381233
- page.refresh
end
it 'shows only variables with description as prefill variables on the run pipeline page',
diff --git a/spec/controllers/projects/pages_controller_spec.rb b/spec/controllers/projects/pages_controller_spec.rb
index 136f98ac907..ded5dd57e3e 100644
--- a/spec/controllers/projects/pages_controller_spec.rb
+++ b/spec/controllers/projects/pages_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::PagesController do
+RSpec.describe Projects::PagesController, feature_category: :pages do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
@@ -14,7 +14,12 @@ RSpec.describe Projects::PagesController do
end
before do
- allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ stub_config(pages: {
+ enabled: true,
+ external_https: true,
+ access_control: false
+ })
+
sign_in(user)
project.add_maintainer(user)
end
@@ -123,49 +128,99 @@ RSpec.describe Projects::PagesController do
end
describe 'PATCH update' do
- let(:request_params) do
- {
- namespace_id: project.namespace,
- project_id: project,
- project: { pages_https_only: 'false' }
- }
- end
+ context 'when updating pages_https_only' do
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ project: { pages_https_only: 'true' }
+ }
+ end
- let(:update_service) { double(execute: { status: :success }) }
+ it 'updates project field and redirects back to the pages settings' do
+ project.update!(pages_https_only: false)
- before do
- allow(Projects::UpdateService).to receive(:new) { update_service }
- end
+ expect { patch :update, params: request_params }
+ .to change { project.reload.pages_https_only }
+ .from(false).to(true)
- it 'returns 302 status' do
- patch :update, params: request_params
+ expect(response).to have_gitlab_http_status(:found)
+ expect(response).to redirect_to(project_pages_path(project))
+ end
- expect(response).to have_gitlab_http_status(:found)
- end
+ context 'when it fails to update' do
+ it 'adds an error message' do
+ expect_next_instance_of(Projects::UpdateService) do |service|
+ expect(service)
+ .to receive(:execute)
+ .and_return(status: :error, message: 'some error happened')
+ end
- it 'redirects back to the pages settings' do
- patch :update, params: request_params
+ expect { patch :update, params: request_params }
+ .not_to change { project.reload.pages_https_only }
- expect(response).to redirect_to(project_pages_path(project))
+ expect(response).to redirect_to(project_pages_path(project))
+ expect(flash[:alert]).to eq('some error happened')
+ end
+ end
end
- it 'calls the update service' do
- expect(Projects::UpdateService)
- .to receive(:new)
- .with(project, user, ActionController::Parameters.new(request_params[:project]).permit!)
- .and_return(update_service)
+ context 'when updating pages_unique_domain' do
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ project: {
+ project_setting_attributes: {
+ pages_unique_domain_enabled: 'true'
+ }
+ }
+ }
+ end
- patch :update, params: request_params
- end
+ before do
+ create(:project_setting, project: project, pages_unique_domain_enabled: false)
+ end
- context 'when update_service returns an error message' do
- let(:update_service) { double(execute: { status: :error, message: 'some error happened' }) }
+ context 'with pages_unique_domain feature flag disabled' do
+ it 'does not update pages unique domain' do
+ stub_feature_flags(pages_unique_domain: false)
- it 'adds an error message' do
- patch :update, params: request_params
+ expect { patch :update, params: request_params }
+ .not_to change { project.project_setting.reload.pages_unique_domain_enabled }
+ end
+ end
- expect(response).to redirect_to(project_pages_path(project))
- expect(flash[:alert]).to eq('some error happened')
+ context 'with pages_unique_domain feature flag enabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: true)
+ end
+
+ it 'updates pages_https_only and pages_unique_domain and redirects back to pages settings' do
+ expect { patch :update, params: request_params }
+ .to change { project.project_setting.reload.pages_unique_domain_enabled }
+ .from(false).to(true)
+
+ expect(project.project_setting.pages_unique_domain).not_to be_nil
+ expect(response).to have_gitlab_http_status(:found)
+ expect(response).to redirect_to(project_pages_path(project))
+ end
+
+ context 'when it fails to update' do
+ it 'adds an error message' do
+ expect_next_instance_of(Projects::UpdateService) do |service|
+ expect(service)
+ .to receive(:execute)
+ .and_return(status: :error, message: 'some error happened')
+ end
+
+ expect { patch :update, params: request_params }
+ .not_to change { project.project_setting.reload.pages_unique_domain_enabled }
+
+ expect(response).to redirect_to(project_pages_path(project))
+ expect(flash[:alert]).to eq('some error happened')
+ end
+ end
end
end
end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index e641f925758..a3a2af52807 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -59,6 +59,10 @@ FactoryBot.define do
user_type { :project_bot }
end
+ trait :service_account do
+ user_type { :service_account }
+ end
+
trait :migration_bot do
user_type { :migration_bot }
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index b5f640f1cca..e3c2402b2c9 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -695,7 +695,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :projects do
end
context 'when variables are specified' do
- it 'creates a new pipeline with variables', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/375552' do
+ it 'creates a new pipeline with variables' do
page.within(find("[data-testid='ci-variable-row']")) do
find("[data-testid='pipeline-form-ci-variable-key']").set('key_name')
find("[data-testid='pipeline-form-ci-variable-value']").set('value')
@@ -721,7 +721,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :projects do
it { expect(page).to have_content('Missing CI config file') }
- it 'creates a pipeline after first request failed and a valid gitlab-ci.yml file is available when trying again', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/375552' do
+ it 'creates a pipeline after first request failed and a valid gitlab-ci.yml file is available when trying again' do
stub_ci_pipeline_to_return_yaml_file
expect do
diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js
index 3f4513e6bfa..da51372dd3d 100644
--- a/spec/frontend/issues/show/components/description_spec.js
+++ b/spec/frontend/issues/show/components/description_spec.js
@@ -310,69 +310,58 @@ describe('Description component', () => {
});
});
- describe('with work_items_mvc feature flag enabled', () => {
- describe('empty description', () => {
- beforeEach(() => {
- createComponent({
- props: {
- descriptionHtml: '',
- },
- provide: {
- glFeatures: {
- workItemsMvc: true,
- },
- },
- });
- return nextTick();
+ describe('empty description', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ descriptionHtml: '',
+ },
});
+ return nextTick();
+ });
- it('renders without error', () => {
- expect(findTaskActionButtons()).toHaveLength(0);
- });
+ it('renders without error', () => {
+ expect(findTaskActionButtons()).toHaveLength(0);
});
+ });
- describe('description with checkboxes', () => {
- beforeEach(() => {
- createComponent({
- props: {
- descriptionHtml: descriptionHtmlWithCheckboxes,
- },
- provide: {
- glFeatures: {
- workItemsMvc: true,
- },
- },
- });
- return nextTick();
+ describe('description with checkboxes', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ descriptionHtml: descriptionHtmlWithCheckboxes,
+ },
});
+ return nextTick();
+ });
- it('renders a list of hidden buttons corresponding to checkboxes in description HTML', () => {
- expect(findTaskActionButtons()).toHaveLength(3);
- });
+ it('renders a list of hidden buttons corresponding to checkboxes in description HTML', () => {
+ expect(findTaskActionButtons()).toHaveLength(3);
+ });
- it('does not show a modal by default', () => {
- expect(findModal().exists()).toBe(false);
- });
+ it('does not show a modal by default', () => {
+ expect(findModal().exists()).toBe(false);
+ });
- it('shows toast after delete success', async () => {
- const newDesc = 'description';
- findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc);
+ it('shows toast after delete success', async () => {
+ const newDesc = 'description';
+ findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc);
- expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]);
- expect($toast.show).toHaveBeenCalledWith('Task deleted');
- });
+ expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]);
+ expect($toast.show).toHaveBeenCalledWith('Task deleted');
});
+ });
- describe('task list item actions', () => {
- describe('converting the task list item to a task', () => {
- describe('when successful', () => {
- let createWorkItemMutationHandler;
+ describe('task list item actions', () => {
+ describe('converting the task list item to a task', () => {
+ describe('when successful', () => {
+ let createWorkItemMutationHandler;
- beforeEach(async () => {
- createWorkItemMutationHandler = jest
- .fn()
- .mockResolvedValue(createWorkItemMutationResponse);
- const descriptionText = `Tasks
+ beforeEach(async () => {
+ createWorkItemMutationHandler = jest
+ .fn()
+ .mockResolvedValue(createWorkItemMutationResponse);
+ const descriptionText = `Tasks
1. [ ] item 1
1. [ ] item 2
@@ -381,218 +370,207 @@ describe('Description component', () => {
1. [ ] item 3
1. [ ] item 4;`;
- createComponent({
- props: { descriptionText },
- provide: { glFeatures: { workItemsMvc: true } },
- createWorkItemMutationHandler,
- });
- await waitForPromises();
-
- eventHub.$emit('convert-task-list-item', '4:4-8:19');
- await waitForPromises();
+ createComponent({
+ props: { descriptionText },
+ createWorkItemMutationHandler,
});
+ await waitForPromises();
- it('emits an event to update the description with the deleted task list item omitted', () => {
- const newDescriptionText = `Tasks
+ eventHub.$emit('convert-task-list-item', '4:4-8:19');
+ await waitForPromises();
+ });
+
+ it('emits an event to update the description with the deleted task list item omitted', () => {
+ const newDescriptionText = `Tasks
1. [ ] item 1
1. [ ] item 3
1. [ ] item 4;`;
- expect(wrapper.emitted('saveDescription')).toEqual([[newDescriptionText]]);
- });
+ expect(wrapper.emitted('saveDescription')).toEqual([[newDescriptionText]]);
+ });
- it('calls a mutation to create a task', () => {
- const {
+ it('calls a mutation to create a task', () => {
+ const {
+ confidential,
+ iteration,
+ milestone,
+ } = issueDetailsResponse.data.workspace.issuable;
+ expect(createWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
confidential,
- iteration,
- milestone,
- } = issueDetailsResponse.data.workspace.issuable;
- expect(createWorkItemMutationHandler).toHaveBeenCalledWith({
- input: {
- confidential,
- description: '\nparagraph text\n',
- hierarchyWidget: {
- parentId: 'gid://gitlab/WorkItem/1',
- },
- iterationWidget: {
- iterationId: IS_EE ? iteration.id : null,
- },
- milestoneWidget: {
- milestoneId: milestone.id,
- },
- projectPath: 'gitlab-org/gitlab-test',
- title: 'item 2',
- workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
+ description: '\nparagraph text\n',
+ hierarchyWidget: {
+ parentId: 'gid://gitlab/WorkItem/1',
+ },
+ iterationWidget: {
+ iterationId: IS_EE ? iteration.id : null,
+ },
+ milestoneWidget: {
+ milestoneId: milestone.id,
},
- });
+ projectPath: 'gitlab-org/gitlab-test',
+ title: 'item 2',
+ workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
+ },
});
+ });
- it('shows a toast to confirm the creation of the task', () => {
- expect($toast.show).toHaveBeenCalledWith('Converted to task', expect.any(Object));
- });
+ it('shows a toast to confirm the creation of the task', () => {
+ expect($toast.show).toHaveBeenCalledWith('Converted to task', expect.any(Object));
});
+ });
- describe('when unsuccessful', () => {
- beforeEach(async () => {
- createComponent({
- props: { descriptionText: 'description' },
- provide: { glFeatures: { workItemsMvc: true } },
- createWorkItemMutationHandler: jest
- .fn()
- .mockResolvedValue(createWorkItemMutationErrorResponse),
- });
- await waitForPromises();
-
- eventHub.$emit('convert-task-list-item', '1:1-1:11');
- await waitForPromises();
+ describe('when unsuccessful', () => {
+ beforeEach(async () => {
+ createComponent({
+ props: { descriptionText: 'description' },
+ createWorkItemMutationHandler: jest
+ .fn()
+ .mockResolvedValue(createWorkItemMutationErrorResponse),
});
+ await waitForPromises();
- it('shows an alert with an error message', () => {
- expect(createAlert).toHaveBeenCalledWith({
- message: 'Something went wrong when creating task. Please try again.',
- error: new Error('an error'),
- captureError: true,
- });
+ eventHub.$emit('convert-task-list-item', '1:1-1:11');
+ await waitForPromises();
+ });
+
+ it('shows an alert with an error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'Something went wrong when creating task. Please try again.',
+ error: new Error('an error'),
+ captureError: true,
});
});
});
+ });
- describe('deleting the task list item', () => {
- it('emits an event to update the description with the deleted task list item', () => {
- const descriptionText = `Tasks
+ describe('deleting the task list item', () => {
+ it('emits an event to update the description with the deleted task list item', () => {
+ const descriptionText = `Tasks
1. [ ] item 1
1. [ ] item 2
1. [ ] item 3
1. [ ] item 4;`;
- const newDescriptionText = `Tasks
+ const newDescriptionText = `Tasks
1. [ ] item 1
1. [ ] item 3
1. [ ] item 4;`;
- createComponent({
- props: { descriptionText },
- provide: { glFeatures: { workItemsMvc: true } },
- });
+ createComponent({
+ props: { descriptionText },
+ });
- eventHub.$emit('delete-task-list-item', '4:4-5:19');
+ eventHub.$emit('delete-task-list-item', '4:4-5:19');
- expect(wrapper.emitted('saveDescription')).toEqual([[newDescriptionText]]);
- });
+ expect(wrapper.emitted('saveDescription')).toEqual([[newDescriptionText]]);
});
});
+ });
- describe('work items detail', () => {
- describe('when opening and closing', () => {
- beforeEach(() => {
- createComponent({
- props: {
- descriptionHtml: descriptionHtmlWithTask,
- },
- provide: {
- glFeatures: { workItemsMvc: true },
- },
- });
- return nextTick();
+ describe('work items detail', () => {
+ describe('when opening and closing', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ descriptionHtml: descriptionHtmlWithTask,
+ },
});
+ return nextTick();
+ });
- it('opens when task button is clicked', async () => {
- await findTaskLink().trigger('click');
+ it('opens when task button is clicked', async () => {
+ await findTaskLink().trigger('click');
- expect(showDetailsModal).toHaveBeenCalled();
- expect(updateHistory).toHaveBeenCalledWith({
- url: `${TEST_HOST}/?work_item_id=2`,
- replace: true,
- });
+ expect(showDetailsModal).toHaveBeenCalled();
+ expect(updateHistory).toHaveBeenCalledWith({
+ url: `${TEST_HOST}/?work_item_id=2`,
+ replace: true,
});
+ });
- it('closes from an open state', async () => {
- await findTaskLink().trigger('click');
+ it('closes from an open state', async () => {
+ await findTaskLink().trigger('click');
- findWorkItemDetailModal().vm.$emit('close');
- await nextTick();
+ findWorkItemDetailModal().vm.$emit('close');
+ await nextTick();
- expect(updateHistory).toHaveBeenLastCalledWith({
- url: `${TEST_HOST}/`,
- replace: true,
- });
+ expect(updateHistory).toHaveBeenLastCalledWith({
+ url: `${TEST_HOST}/`,
+ replace: true,
});
+ });
- it('tracks when opened', async () => {
- const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
-
- await findTaskLink().trigger('click');
+ it('tracks when opened', async () => {
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- expect(trackingSpy).toHaveBeenCalledWith(
- TRACKING_CATEGORY_SHOW,
- 'viewed_work_item_from_modal',
- {
- category: TRACKING_CATEGORY_SHOW,
- label: 'work_item_view',
- property: 'type_task',
- },
- );
- });
- });
+ await findTaskLink().trigger('click');
- describe('when url query `work_item_id` exists', () => {
- it.each`
- behavior | workItemId | modalOpened
- ${'opens'} | ${'2'} | ${1}
- ${'does not open'} | ${'123'} | ${0}
- ${'does not open'} | ${'123e'} | ${0}
- ${'does not open'} | ${'12e3'} | ${0}
- ${'does not open'} | ${'1e23'} | ${0}
- ${'does not open'} | ${'x'} | ${0}
- ${'does not open'} | ${'undefined'} | ${0}
- `(
- '$behavior when url contains `work_item_id=$workItemId`',
- async ({ workItemId, modalOpened }) => {
- setWindowLocation(`?work_item_id=${workItemId}`);
-
- createComponent({
- props: { descriptionHtml: descriptionHtmlWithTask },
- provide: { glFeatures: { workItemsMvc: true } },
- });
-
- expect(showDetailsModal).toHaveBeenCalledTimes(modalOpened);
+ expect(trackingSpy).toHaveBeenCalledWith(
+ TRACKING_CATEGORY_SHOW,
+ 'viewed_work_item_from_modal',
+ {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'work_item_view',
+ property: 'type_task',
},
);
});
});
- describe('when hovering task links', () => {
- beforeEach(() => {
- createComponent({
- props: {
- descriptionHtml: descriptionHtmlWithTask,
- },
- provide: {
- glFeatures: { workItemsMvc: true },
- },
- });
- return nextTick();
- });
+ describe('when url query `work_item_id` exists', () => {
+ it.each`
+ behavior | workItemId | modalOpened
+ ${'opens'} | ${'2'} | ${1}
+ ${'does not open'} | ${'123'} | ${0}
+ ${'does not open'} | ${'123e'} | ${0}
+ ${'does not open'} | ${'12e3'} | ${0}
+ ${'does not open'} | ${'1e23'} | ${0}
+ ${'does not open'} | ${'x'} | ${0}
+ ${'does not open'} | ${'undefined'} | ${0}
+ `(
+ '$behavior when url contains `work_item_id=$workItemId`',
+ async ({ workItemId, modalOpened }) => {
+ setWindowLocation(`?work_item_id=${workItemId}`);
- it('prefetches work item detail after work item link is hovered for 150ms', async () => {
- await findTaskLink().trigger('mouseover');
- jest.advanceTimersByTime(150);
- await waitForPromises();
+ createComponent({
+ props: { descriptionHtml: descriptionHtmlWithTask },
+ });
- expect(queryHandler).toHaveBeenCalledWith({
- id: 'gid://gitlab/WorkItem/2',
- });
+ expect(showDetailsModal).toHaveBeenCalledTimes(modalOpened);
+ },
+ );
+ });
+ });
+
+ describe('when hovering task links', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ descriptionHtml: descriptionHtmlWithTask,
+ },
});
+ return nextTick();
+ });
- it('does not work item detail after work item link is hovered for less than 150ms', async () => {
- await findTaskLink().trigger('mouseover');
- await findTaskLink().trigger('mouseout');
- jest.advanceTimersByTime(150);
- await waitForPromises();
+ it('prefetches work item detail after work item link is hovered for 150ms', async () => {
+ await findTaskLink().trigger('mouseover');
+ jest.advanceTimersByTime(150);
+ await waitForPromises();
- expect(queryHandler).not.toHaveBeenCalled();
+ expect(queryHandler).toHaveBeenCalledWith({
+ id: 'gid://gitlab/WorkItem/2',
});
});
+
+ it('does not work item detail after work item link is hovered for less than 150ms', async () => {
+ await findTaskLink().trigger('mouseover');
+ await findTaskLink().trigger('mouseout');
+ jest.advanceTimersByTime(150);
+ await waitForPromises();
+
+ expect(queryHandler).not.toHaveBeenCalled();
+ });
});
});
diff --git a/spec/lib/gitlab/ci/pipeline/duration_spec.rb b/spec/lib/gitlab/ci/pipeline/duration_spec.rb
index 36714413da6..088a901c80e 100644
--- a/spec/lib/gitlab/ci/pipeline/duration_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/duration_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Ci::Pipeline::Duration do
+RSpec.describe Gitlab::Ci::Pipeline::Duration, feature_category: :continuous_integration do
describe '.from_periods' do
let(:calculated_duration) { calculate(data) }
@@ -113,16 +113,17 @@ RSpec.describe Gitlab::Ci::Pipeline::Duration do
described_class::Period.new(first, last)
end
- described_class.from_periods(periods.sort_by(&:first))
+ described_class.send(:from_periods, periods.sort_by(&:first))
end
end
describe '.from_pipeline' do
+ let_it_be_with_reload(:pipeline) { create(:ci_pipeline) }
+
let_it_be(:start_time) { Time.current.change(usec: 0) }
let_it_be(:current) { start_time + 1000 }
- let_it_be(:pipeline) { create(:ci_pipeline) }
- let_it_be(:success_build) { create_build(:success, started_at: start_time, finished_at: start_time + 60) }
- let_it_be(:failed_build) { create_build(:failed, started_at: start_time + 60, finished_at: start_time + 120) }
+ let_it_be(:success_build) { create_build(:success, started_at: start_time, finished_at: start_time + 50) }
+ let_it_be(:failed_build) { create_build(:failed, started_at: start_time + 60, finished_at: start_time + 110) }
let_it_be(:canceled_build) { create_build(:canceled, started_at: start_time + 120, finished_at: start_time + 180) }
let_it_be(:skipped_build) { create_build(:skipped, started_at: start_time) }
let_it_be(:pending_build) { create_build(:pending) }
@@ -141,21 +142,55 @@ RSpec.describe Gitlab::Ci::Pipeline::Duration do
end
context 'when there is no running build' do
- let(:running_build) { nil }
+ let!(:running_build) { nil }
it 'returns the duration for all the builds' do
travel_to(current) do
- expect(described_class.from_pipeline(pipeline)).to eq 180.seconds
+ # 160 = success (50) + failed (50) + canceled (60)
+ expect(described_class.from_pipeline(pipeline)).to eq 160.seconds
end
end
end
- context 'when there are bridge jobs' do
- let!(:success_bridge) { create_bridge(:success, started_at: start_time + 220, finished_at: start_time + 280) }
- let!(:failed_bridge) { create_bridge(:failed, started_at: start_time + 180, finished_at: start_time + 240) }
- let!(:skipped_bridge) { create_bridge(:skipped, started_at: start_time) }
- let!(:created_bridge) { create_bridge(:created) }
- let!(:manual_bridge) { create_bridge(:manual) }
+ context 'when there are direct bridge jobs' do
+ let_it_be(:success_bridge) do
+ create_bridge(:success, started_at: start_time + 220, finished_at: start_time + 280)
+ end
+
+ let_it_be(:failed_bridge) { create_bridge(:failed, started_at: start_time + 180, finished_at: start_time + 240) }
+ # NOTE: bridge won't be `canceled` as it will be marked as failed when downstream pipeline is canceled
+ # @see Ci::Bridge#inherit_status_from_downstream
+ let_it_be(:canceled_bridge) do
+ create_bridge(:failed, started_at: start_time + 180, finished_at: start_time + 210)
+ end
+
+ let_it_be(:skipped_bridge) { create_bridge(:skipped, started_at: start_time) }
+ let_it_be(:created_bridge) { create_bridge(:created) }
+ let_it_be(:manual_bridge) { create_bridge(:manual) }
+
+ let_it_be(:success_bridge_pipeline) do
+ create(:ci_pipeline, :success, started_at: start_time + 230, finished_at: start_time + 280).tap do |p|
+ create(:ci_sources_pipeline, source_job: success_bridge, pipeline: p)
+ create_build(:success, pipeline: p, started_at: start_time + 235, finished_at: start_time + 280)
+ create_bridge(:success, pipeline: p, started_at: start_time + 240, finished_at: start_time + 280)
+ end
+ end
+
+ let_it_be(:failed_bridge_pipeline) do
+ create(:ci_pipeline, :failed, started_at: start_time + 225, finished_at: start_time + 240).tap do |p|
+ create(:ci_sources_pipeline, source_job: failed_bridge, pipeline: p)
+ create_build(:failed, pipeline: p, started_at: start_time + 230, finished_at: start_time + 240)
+ create_bridge(:success, pipeline: p, started_at: start_time + 235, finished_at: start_time + 240)
+ end
+ end
+
+ let_it_be(:canceled_bridge_pipeline) do
+ create(:ci_pipeline, :canceled, started_at: start_time + 190, finished_at: start_time + 210).tap do |p|
+ create(:ci_sources_pipeline, source_job: canceled_bridge, pipeline: p)
+ create_build(:canceled, pipeline: p, started_at: start_time + 200, finished_at: start_time + 210)
+ create_bridge(:success, pipeline: p, started_at: start_time + 205, finished_at: start_time + 210)
+ end
+ end
it 'returns the duration of the running build' do
travel_to(current) do
@@ -166,12 +201,147 @@ RSpec.describe Gitlab::Ci::Pipeline::Duration do
context 'when there is no running build' do
let!(:running_build) { nil }
- it 'returns the duration for all the builds and bridge jobs' do
+ it 'returns the duration for all the builds (including self and downstreams)' do
+ travel_to(current) do
+ # 220 = 160 (see above)
+ # + success build (45) + failed (10) + canceled (10) - overlapping (success & failed) (5)
+ expect(described_class.from_pipeline(pipeline)).to eq 220.seconds
+ end
+ end
+ end
+
+ context 'when feature flag ci_use_downstream_pipeline_duration_for_calculation is disabled' do
+ before do
+ stub_feature_flags(ci_use_downstream_pipeline_duration_for_calculation: false)
+ end
+
+ it 'returns the duration of the running build' do
+ travel_to(current) do
+ expect(described_class.from_pipeline(pipeline)).to eq 1000.seconds
+ end
+ end
+
+ context 'when there is no running build' do
+ let!(:running_build) { nil }
+
+ it 'returns the duration for builds and bridges' do
+ travel_to(current) do
+ # 260 = 160 (see above)
+ # + success bridge build (60) + failed (60) + canceled (30)
+ # - overlapping (success & failed) (20) - overlapping (failed & canceled) (30)
+ expect(described_class.from_pipeline(pipeline)).to eq 260.seconds
+ end
+ end
+ end
+ end
+
+ # rubocop:disable RSpec/MultipleMemoizedHelpers
+ context 'when there are downstream bridge jobs' do
+ let_it_be(:success_direct_bridge) do
+ create_bridge(:success, started_at: start_time + 280, finished_at: start_time + 400)
+ end
+
+ let_it_be(:success_downstream_pipeline) do
+ create(:ci_pipeline, :success, started_at: start_time + 285, finished_at: start_time + 300).tap do |p|
+ create(:ci_sources_pipeline, source_job: success_direct_bridge, pipeline: p)
+ create_build(:success, pipeline: p, started_at: start_time + 290, finished_at: start_time + 296)
+ create_bridge(:success, pipeline: p, started_at: start_time + 285, finished_at: start_time + 288)
+ end
+ end
+
+ let_it_be(:failed_downstream_pipeline) do
+ create(:ci_pipeline, :failed, started_at: start_time + 305, finished_at: start_time + 350).tap do |p|
+ create(:ci_sources_pipeline, source_job: success_direct_bridge, pipeline: p)
+ create_build(:failed, pipeline: p, started_at: start_time + 320, finished_at: start_time + 327)
+ create_bridge(:success, pipeline: p, started_at: start_time + 305, finished_at: start_time + 350)
+ end
+ end
+
+ let_it_be(:canceled_downstream_pipeline) do
+ create(:ci_pipeline, :canceled, started_at: start_time + 360, finished_at: start_time + 400).tap do |p|
+ create(:ci_sources_pipeline, source_job: success_direct_bridge, pipeline: p)
+ create_build(:canceled, pipeline: p, started_at: start_time + 390, finished_at: start_time + 398)
+ create_bridge(:success, pipeline: p, started_at: start_time + 360, finished_at: start_time + 378)
+ end
+ end
+
+ it 'returns the duration of the running build' do
travel_to(current) do
- expect(described_class.from_pipeline(pipeline)).to eq 280.seconds
+ expect(described_class.from_pipeline(pipeline)).to eq 1000.seconds
+ end
+ end
+
+ context 'when there is no running build' do
+ let!(:running_build) { nil }
+
+ it 'returns the duration for all the builds (including self and downstreams)' do
+ travel_to(current) do
+ # 241 = 220 (see above)
+ # + success downstream build (6) + failed (7) + canceled (8)
+ expect(described_class.from_pipeline(pipeline)).to eq 241.seconds
+ end
+ end
+ end
+
+ context 'when feature flag ci_use_downstream_pipeline_duration_for_calculation is disabled' do
+ before do
+ stub_feature_flags(ci_use_downstream_pipeline_duration_for_calculation: false)
+ end
+
+ it 'returns the duration of the running build' do
+ travel_to(current) do
+ expect(described_class.from_pipeline(pipeline)).to eq 1000.seconds
+ end
+ end
+
+ context 'when there is no running build' do
+ let!(:running_build) { nil }
+
+ it 'returns the duration for builds and bridges' do
+ travel_to(current) do
+ # 380 = 260 (see above) + success direct bridge (120)
+ expect(described_class.from_pipeline(pipeline)).to eq 380.seconds
+ end
+ end
end
end
end
+ # rubocop:enable RSpec/MultipleMemoizedHelpers
+ end
+
+ it 'does not generate N+1 queries if more builds are added' do
+ travel_to(current) do
+ expect do
+ described_class.from_pipeline(pipeline)
+ end.not_to exceed_query_limit(1)
+
+ create_list(:ci_build, 2, :success, pipeline: pipeline, started_at: start_time, finished_at: start_time + 50)
+
+ expect do
+ described_class.from_pipeline(pipeline)
+ end.not_to exceed_query_limit(1)
+ end
+ end
+
+ it 'does not generate N+1 queries if more bridges and their pipeline builds are added' do
+ travel_to(current) do
+ expect do
+ described_class.from_pipeline(pipeline)
+ end.not_to exceed_query_limit(1)
+
+ create_list(
+ :ci_bridge, 2, :success,
+ pipeline: pipeline, started_at: start_time + 220, finished_at: start_time + 280).each do |bridge|
+ create(:ci_pipeline, :success, started_at: start_time + 235, finished_at: start_time + 280).tap do |p|
+ create(:ci_sources_pipeline, source_job: bridge, pipeline: p)
+ create_builds(3, :success)
+ end
+ end
+
+ expect do
+ described_class.from_pipeline(pipeline)
+ end.not_to exceed_query_limit(1)
+ end
end
private
@@ -180,6 +350,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Duration do
create(:ci_build, trait, pipeline: pipeline, **opts)
end
+ def create_builds(counts, trait, **opts)
+ create_list(:ci_build, counts, trait, pipeline: pipeline, **opts)
+ end
+
def create_bridge(trait, **opts)
create(:ci_bridge, trait, pipeline: pipeline, **opts)
end
diff --git a/spec/lib/gitlab/pages/random_domain_spec.rb b/spec/lib/gitlab/pages/random_domain_spec.rb
new file mode 100644
index 00000000000..978412bb72c
--- /dev/null
+++ b/spec/lib/gitlab/pages/random_domain_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Pages::RandomDomain, feature_category: :pages do
+ let(:namespace_path) { 'namespace' }
+
+ subject(:generator) do
+ described_class.new(project_path: project_path, namespace_path: namespace_path)
+ end
+
+ RSpec.shared_examples 'random domain' do |domain|
+ it do
+ expect(SecureRandom)
+ .to receive(:hex)
+ .and_wrap_original do |_, size, _|
+ ('h' * size)
+ end
+
+ generated = generator.generate
+
+ expect(generated).to eq(domain)
+ expect(generated.length).to eq(63)
+ end
+ end
+
+ context 'when project path is less than 48 chars' do
+ let(:project_path) { 'p' }
+
+ it_behaves_like 'random domain', 'p-namespace-hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh'
+ end
+
+ context 'when project path is close to 48 chars' do
+ let(:project_path) { 'p' * 45 }
+
+ it_behaves_like 'random domain', 'ppppppppppppppppppppppppppppppppppppppppppppp-na-hhhhhhhhhhhhhh'
+ end
+
+ context 'when project path is larger than 48 chars' do
+ let(:project_path) { 'p' * 49 }
+
+ it_behaves_like 'random domain', 'pppppppppppppppppppppppppppppppppppppppppppppppp-hhhhhhhhhhhhhh'
+ end
+end
diff --git a/spec/lib/object_storage/config_spec.rb b/spec/lib/object_storage/config_spec.rb
index 2a81142ea44..3099468c07d 100644
--- a/spec/lib/object_storage/config_spec.rb
+++ b/spec/lib/object_storage/config_spec.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require 'fast_spec_helper'
+require 'spec_helper'
require 'rspec-parameterized'
require 'fog/core'
@@ -130,6 +130,11 @@ RSpec.describe ObjectStorage::Config do
it { expect(subject.provider).to eq('AWS') }
it { expect(subject.aws?).to be true }
it { expect(subject.google?).to be false }
+ it { expect(subject.credentials).to eq(credentials) }
+
+ context 'with FIPS enabled', :fips_mode do
+ it { expect(subject.credentials).to eq(credentials.merge(disable_content_md5_validation: true)) }
+ end
end
context 'with Google credentials' do
diff --git a/spec/models/concerns/has_user_type_spec.rb b/spec/models/concerns/has_user_type_spec.rb
index bd128112113..0f90bbcda4e 100644
--- a/spec/models/concerns/has_user_type_spec.rb
+++ b/spec/models/concerns/has_user_type_spec.rb
@@ -2,11 +2,11 @@
require 'spec_helper'
-RSpec.describe User do
+RSpec.describe User, feature_category: :authentication_and_authorization do
specify 'types consistency checks', :aggregate_failures do
expect(described_class::USER_TYPES.keys)
.to match_array(%w[human ghost alert_bot project_bot support_bot service_user security_bot visual_review_bot
- migration_bot automation_bot admin_bot suggested_reviewers_bot])
+ migration_bot automation_bot admin_bot suggested_reviewers_bot service_account])
expect(described_class::USER_TYPES).to include(*described_class::BOT_USER_TYPES)
expect(described_class::USER_TYPES).to include(*described_class::NON_INTERNAL_USER_TYPES)
expect(described_class::USER_TYPES).to include(*described_class::INTERNAL_USER_TYPES)
diff --git a/spec/models/project_setting_spec.rb b/spec/models/project_setting_spec.rb
index feb5985818b..42433a2a84a 100644
--- a/spec/models/project_setting_spec.rb
+++ b/spec/models/project_setting_spec.rb
@@ -39,6 +39,44 @@ RSpec.describe ProjectSetting, type: :model do
[nil, 'not_allowed', :invalid].each do |invalid_value|
it { is_expected.not_to allow_value([invalid_value]).for(:target_platforms) }
end
+
+ context "when pages_unique_domain is required", feature_category: :pages do
+ it "is not required if pages_unique_domain_enabled is false" do
+ project_setting = build(:project_setting, pages_unique_domain_enabled: false)
+
+ expect(project_setting).to be_valid
+ expect(project_setting.errors.full_messages).not_to include("Pages unique domain can't be blank")
+ end
+
+ it "is required when pages_unique_domain_enabled is true" do
+ project_setting = build(:project_setting, pages_unique_domain_enabled: true)
+
+ expect(project_setting).not_to be_valid
+ expect(project_setting.errors.full_messages).to include("Pages unique domain can't be blank")
+ end
+
+ it "is required if it is already saved in the database" do
+ project_setting = create(
+ :project_setting,
+ pages_unique_domain: "random-unique-domain-here",
+ pages_unique_domain_enabled: true
+ )
+
+ project_setting.pages_unique_domain = nil
+
+ expect(project_setting).not_to be_valid
+ expect(project_setting.errors.full_messages).to include("Pages unique domain can't be blank")
+ end
+ end
+
+ it "validates uniqueness of pages_unique_domain", feature_category: :pages do
+ create(:project_setting, pages_unique_domain: "random-unique-domain-here")
+
+ project_setting = build(:project_setting, pages_unique_domain: "random-unique-domain-here")
+
+ expect(project_setting).not_to be_valid
+ expect(project_setting.errors.full_messages).to include("Pages unique domain has already been taken")
+ end
end
describe 'target_platforms=' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index dfc8919e19d..5304fec506e 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -2666,7 +2666,11 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
describe '#pages_url', feature_category: :pages do
+ let(:group_name) { 'group' }
+ let(:project_name) { 'project' }
+
let(:group) { create(:group, name: group_name) }
+ let(:nested_group) { create(:group, parent: group) }
let(:project_path) { project_name.downcase }
let(:project) do
@@ -2689,101 +2693,114 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
.and_return(['http://example.com', port].compact.join(':'))
end
- context 'group page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'group.example.com' }
+ context 'when pages_unique_domain feature flag is disabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: false)
+ end
- it { is_expected.to eq("http://group.example.com") }
+ it { is_expected.to eq('http://group.example.com/project') }
+ end
- context 'mixed case path' do
- let(:project_path) { 'Group.example.com' }
+ context 'when pages_unique_domain feature flag is enabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: true)
- it { is_expected.to eq("http://group.example.com") }
+ project.project_setting.update!(
+ pages_unique_domain_enabled: pages_unique_domain_enabled,
+ pages_unique_domain: 'unique-domain'
+ )
end
- end
- context 'project page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'Project' }
+ context 'when pages_unique_domain_enabled is false' do
+ let(:pages_unique_domain_enabled) { false }
- it { is_expected.to eq("http://group.example.com/project") }
+ it { is_expected.to eq('http://group.example.com/project') }
+ end
- context 'mixed case path' do
- let(:project_path) { 'Project' }
+ context 'when pages_unique_domain_enabled is false' do
+ let(:pages_unique_domain_enabled) { true }
- it { is_expected.to eq("http://group.example.com/Project") }
+ it { is_expected.to eq('http://unique-domain.example.com') }
end
end
- context 'when there is an explicit port' do
- let(:port) { 3000 }
-
- context 'when not in dev mode' do
- before do
- stub_rails_env('production')
- end
+ context 'with nested group' do
+ let(:project) { create(:project, namespace: nested_group, name: project_name) }
+ let(:expected_url) { "http://group.example.com/#{nested_group.path}/#{project.path}" }
- context 'group page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'group.example.com' }
+ context 'group page' do
+ let(:project_name) { 'group.example.com' }
- it { is_expected.to eq('http://group.example.com:3000/group.example.com') }
+ it { is_expected.to eq(expected_url) }
+ end
- context 'mixed case path' do
- let(:project_path) { 'Group.example.com' }
+ context 'project page' do
+ let(:project_name) { 'Project' }
- it { is_expected.to eq('http://group.example.com:3000/Group.example.com') }
- end
- end
+ it { is_expected.to eq(expected_url) }
+ end
+ end
- context 'project page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'Project' }
+ context 'when the project matches its namespace url' do
+ let(:project_name) { 'group.example.com' }
- it { is_expected.to eq("http://group.example.com:3000/project") }
+ it { is_expected.to eq('http://group.example.com') }
- context 'mixed case path' do
- let(:project_path) { 'Project' }
+ context 'with different group name capitalization' do
+ let(:group_name) { 'Group' }
- it { is_expected.to eq("http://group.example.com:3000/Project") }
- end
- end
+ it { is_expected.to eq("http://group.example.com") }
end
- context 'when in dev mode' do
- before do
- stub_rails_env('development')
- end
-
- context 'group page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'group.example.com' }
+ context 'with different project path capitalization' do
+ let(:project_path) { 'Group.example.com' }
- it { is_expected.to eq('http://group.example.com:3000') }
+ it { is_expected.to eq("http://group.example.com") }
+ end
- context 'mixed case path' do
- let(:project_path) { 'Group.example.com' }
+ context 'with different project name capitalization' do
+ let(:project_name) { 'Project' }
- it { is_expected.to eq('http://group.example.com:3000') }
- end
- end
+ it { is_expected.to eq("http://group.example.com/project") }
+ end
- context 'project page' do
- let(:group_name) { 'Group' }
- let(:project_name) { 'Project' }
+ context 'when there is an explicit port' do
+ let(:port) { 3000 }
- it { is_expected.to eq("http://group.example.com:3000/project") }
+ context 'when not in dev mode' do
+ before do
+ stub_rails_env('production')
+ end
- context 'mixed case path' do
- let(:project_path) { 'Project' }
+ it { is_expected.to eq('http://group.example.com:3000/group.example.com') }
+ end
- it { is_expected.to eq("http://group.example.com:3000/Project") }
+ context 'when in dev mode' do
+ before do
+ stub_rails_env('development')
end
+
+ it { is_expected.to eq('http://group.example.com:3000') }
end
end
end
end
+ describe '#pages_unique_url', feature_category: :pages do
+ let(:project_settings) { create(:project_setting, pages_unique_domain: 'unique-domain') }
+ let(:project) { build(:project, project_setting: project_settings) }
+ let(:domain) { 'example.com' }
+
+ before do
+ allow(Settings.pages).to receive(:host).and_return(domain)
+ allow(Gitlab.config.pages).to receive(:url).and_return("http://#{domain}")
+ end
+
+ it 'returns the pages unique url' do
+ expect(project.pages_unique_url).to eq('http://unique-domain.example.com')
+ end
+ end
+
describe '#pages_namespace_url', feature_category: :pages do
let(:group) { create(:group, name: group_name) }
let(:project) { create(:project, namespace: group, name: project_name) }
@@ -4643,52 +4660,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
- describe '#pages_url' do
- let(:group) { create(:group, name: 'Group') }
- let(:nested_group) { create(:group, parent: group) }
- let(:domain) { 'Example.com' }
-
- subject { project.pages_url }
-
- before do
- allow(Settings.pages).to receive(:host).and_return(domain)
- allow(Gitlab.config.pages).to receive(:url).and_return('http://example.com')
- end
-
- context 'top-level group' do
- let(:project) { create(:project, namespace: group, name: project_name) }
-
- context 'group page' do
- let(:project_name) { 'group.example.com' }
-
- it { is_expected.to eq("http://group.example.com") }
- end
-
- context 'project page' do
- let(:project_name) { 'Project' }
-
- it { is_expected.to eq("http://group.example.com/project") }
- end
- end
-
- context 'nested group' do
- let(:project) { create(:project, namespace: nested_group, name: project_name) }
- let(:expected_url) { "http://group.example.com/#{nested_group.path}/#{project.path}" }
-
- context 'group page' do
- let(:project_name) { 'group.example.com' }
-
- it { is_expected.to eq(expected_url) }
- end
-
- context 'project page' do
- let(:project_name) { 'Project' }
-
- it { is_expected.to eq(expected_url) }
- end
- end
- end
-
describe '#lfs_http_url_to_repo' do
let(:project) { create(:project) }
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index 0575ba3237b..78bbff57572 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
let_it_be(:admin_user) { create(:admin) }
let_it_be(:project_bot) { create(:user, :project_bot) }
+ let_it_be(:service_account) { create(:user, :service_account) }
let_it_be(:migration_bot) { create(:user, :migration_bot) }
let_it_be(:security_bot) { create(:user, :security_bot) }
let_it_be_with_reload(:current_user) { create(:user) }
@@ -219,6 +220,12 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
it { is_expected.to be_allowed(:access_api) }
end
+ context 'service account' do
+ let(:current_user) { service_account }
+
+ it { is_expected.to be_allowed(:access_api) }
+ end
+
context 'migration bot' do
let(:current_user) { migration_bot }
@@ -345,6 +352,12 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
it { is_expected.to be_disallowed(:receive_notifications) }
end
+ context 'service account' do
+ let(:current_user) { service_account }
+
+ it { is_expected.to be_disallowed(:receive_notifications) }
+ end
+
context 'migration bot' do
let(:current_user) { migration_bot }
@@ -433,6 +446,12 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
it { is_expected.to be_allowed(:access_git) }
end
+ context 'service account' do
+ let(:current_user) { service_account }
+
+ it { is_expected.to be_allowed(:access_git) }
+ end
+
context 'user blocked pending approval' do
before do
current_user.block_pending_approval
@@ -517,6 +536,12 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
it { is_expected.to be_allowed(:use_slash_commands) }
end
+ context 'service account' do
+ let(:current_user) { service_account }
+
+ it { is_expected.to be_allowed(:use_slash_commands) }
+ end
+
context 'migration bot' do
let(:current_user) { migration_bot }
@@ -571,6 +596,12 @@ RSpec.describe GlobalPolicy, feature_category: :shared do
it { is_expected.to be_disallowed(:log_in) }
end
+ context 'service account' do
+ let(:current_user) { service_account }
+
+ it { is_expected.to be_disallowed(:log_in) }
+ end
+
context 'migration bot' do
let(:current_user) { migration_bot }
diff --git a/spec/services/markup/rendering_service_spec.rb b/spec/services/markup/rendering_service_spec.rb
index 99ab87f2072..ca70e983714 100644
--- a/spec/services/markup/rendering_service_spec.rb
+++ b/spec/services/markup/rendering_service_spec.rb
@@ -111,5 +111,22 @@ RSpec.describe Markup::RenderingService do
is_expected.to eq(expected_html)
end
end
+
+ context 'with reStructuredText' do
+ let(:file_name) { 'foo.rst' }
+ let(:text) { "####\nPART\n####" }
+
+ it 'returns rendered html' do
+ is_expected.to eq("<h1>PART</h1>\n\n")
+ end
+
+ context 'when input has an invalid syntax' do
+ let(:text) { "####\nPART\n##" }
+
+ it 'uses a simple formatter for html' do
+ is_expected.to eq("<p>####\n<br>PART\n<br>##</p>")
+ end
+ end
+ end
end
end
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index 3cda6bc2627..57249bcd562 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -794,6 +794,112 @@ RSpec.describe Projects::UpdateService do
expect(project.topic_list).to eq(%w[tag_list])
end
end
+
+ describe 'when updating pages unique domain', feature_category: :pages do
+ let(:group) { create(:group, path: 'group') }
+ let(:project) { create(:project, path: 'project', group: group) }
+
+ context 'with pages_unique_domain feature flag disabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: false)
+ end
+
+ it 'does not change pages unique domain' do
+ expect(project)
+ .to receive(:update)
+ .with({ project_setting_attributes: { has_confluence: true } })
+ .and_call_original
+
+ expect do
+ update_project(project, user, project_setting_attributes: {
+ has_confluence: true,
+ pages_unique_domain_enabled: true
+ })
+ end.not_to change { project.project_setting.pages_unique_domain_enabled }
+ end
+
+ it 'does not remove other attributes' do
+ expect(project)
+ .to receive(:update)
+ .with({ name: 'True' })
+ .and_call_original
+
+ update_project(project, user, name: 'True')
+ end
+ end
+
+ context 'with pages_unique_domain feature flag enabled' do
+ before do
+ stub_feature_flags(pages_unique_domain: true)
+ end
+
+ it 'updates project pages unique domain' do
+ expect do
+ update_project(project, user, project_setting_attributes: {
+ pages_unique_domain_enabled: true
+ })
+ end.to change { project.project_setting.pages_unique_domain_enabled }
+
+ expect(project.project_setting.pages_unique_domain_enabled).to eq true
+ expect(project.project_setting.pages_unique_domain).to match %r{project-group-\w+}
+ end
+
+ it 'does not changes unique domain when it already exists' do
+ project.project_setting.update!(
+ pages_unique_domain_enabled: false,
+ pages_unique_domain: 'unique-domain'
+ )
+
+ expect do
+ update_project(project, user, project_setting_attributes: {
+ pages_unique_domain_enabled: true
+ })
+ end.to change { project.project_setting.pages_unique_domain_enabled }
+
+ expect(project.project_setting.pages_unique_domain_enabled).to eq true
+ expect(project.project_setting.pages_unique_domain).to eq 'unique-domain'
+ end
+
+ it 'does not changes unique domain when it disabling unique domain' do
+ project.project_setting.update!(
+ pages_unique_domain_enabled: true,
+ pages_unique_domain: 'unique-domain'
+ )
+
+ expect do
+ update_project(project, user, project_setting_attributes: {
+ pages_unique_domain_enabled: false
+ })
+ end.not_to change { project.project_setting.pages_unique_domain }
+
+ expect(project.project_setting.pages_unique_domain_enabled).to eq false
+ expect(project.project_setting.pages_unique_domain).to eq 'unique-domain'
+ end
+
+ context 'when there is another project with the unique domain' do
+ it 'fails pages unique domain already exists' do
+ create(
+ :project_setting,
+ pages_unique_domain_enabled: true,
+ pages_unique_domain: 'unique-domain'
+ )
+
+ allow(Gitlab::Pages::RandomDomain)
+ .to receive(:generate)
+ .and_return('unique-domain')
+
+ result = update_project(project, user, project_setting_attributes: {
+ pages_unique_domain_enabled: true
+ })
+
+ expect(result).to eq(
+ status: :error,
+ message: 'Project setting pages unique domain has already been taken'
+ )
+ end
+ end
+ end
+ end
end
describe '#run_auto_devops_pipeline?' do