diff --git a/.gitlab/ci/docs.gitlab-ci.yml b/.gitlab/ci/docs.gitlab-ci.yml
index 8b7691045cb..217da6506bf 100644
--- a/.gitlab/ci/docs.gitlab-ci.yml
+++ b/.gitlab/ci/docs.gitlab-ci.yml
@@ -44,7 +44,7 @@ docs-lint markdown:
- .default-retry
- .docs:rules:docs-lint
# When updating the image version here, update it in /scripts/lint-doc.sh too.
- image: registry.gitlab.com/gitlab-org/gitlab-docs/lint-markdown:alpine-3.15-vale-2.15.0-markdownlint-0.31.0
+ image: registry.gitlab.com/gitlab-org/gitlab-docs/lint-markdown:alpine-3.15-vale-2.15.5-markdownlint-0.31.1
stage: lint
needs: []
script:
diff --git a/.gitlab/ci/review-apps/qa.gitlab-ci.yml b/.gitlab/ci/review-apps/qa.gitlab-ci.yml
index a955096992f..d2192a7511a 100644
--- a/.gitlab/ci/review-apps/qa.gitlab-ci.yml
+++ b/.gitlab/ci/review-apps/qa.gitlab-ci.yml
@@ -1,3 +1,10 @@
+include:
+ - project: gitlab-org/quality/pipeline-common
+ ref: 0.3.6
+ file:
+ - /ci/allure-report.yml
+ - /ci/knapsack-report.yml
+
.review-qa-base:
extends:
- .use-docker-in-docker
@@ -43,27 +50,13 @@
when: always
.allure-report-base:
- image:
- name: ${GITLAB_DEPENDENCY_PROXY}andrcuns/allure-report-publisher:0.4.2
- entrypoint: [""]
+ extends: .generate-allure-report-base
stage: post-qa
variables:
- GIT_STRATEGY: none
- STORAGE_CREDENTIALS: $QA_ALLURE_REPORT_GCS_CREDENTIALS
GITLAB_AUTH_TOKEN: $GITLAB_QA_MR_ALLURE_REPORT_TOKEN
ALLURE_PROJECT_PATH: $CI_PROJECT_PATH
ALLURE_MERGE_REQUEST_IID: $CI_MERGE_REQUEST_IID
- allow_failure: true
- script:
- - |
- allure-report-publisher upload gcs \
- --results-glob="qa/tmp/allure-results/*" \
- --bucket="gitlab-qa-allure-reports" \
- --prefix="$ALLURE_REPORT_PATH_PREFIX/$CI_COMMIT_REF_SLUG" \
- --update-pr="comment" \
- --copy-latest \
- --ignore-missing-results \
- --color
+ ALLURE_RESULTS_GLOB: qa/tmp/allure-results/*
review-qa-smoke:
extends:
@@ -121,23 +114,19 @@ review-performance:
performance: performance.json
expire_in: 31d
-allure-report-qa-smoke:
+# Generate single report for both smoke and reliable test jobs
+# Both job types are essentially the same:
+# * always executed
+# * always blocking
+allure-report-qa-blocking:
extends:
- .allure-report-base
- - .review:rules:review-qa-smoke-report
- needs: ["review-qa-smoke"]
+ - .review:rules:review-qa-blocking-report
+ needs:
+ - review-qa-smoke
+ - review-qa-reliable
variables:
- ALLURE_REPORT_PATH_PREFIX: gitlab-review-smoke
- ALLURE_JOB_NAME: review-qa-smoke
-
-allure-report-qa-reliable:
- extends:
- - .allure-report-base
- - .review:rules:review-qa-reliable-report
- needs: ["review-qa-reliable"]
- variables:
- ALLURE_REPORT_PATH_PREFIX: gitlab-review-reliable
- ALLURE_JOB_NAME: review-qa-reliable
+ ALLURE_JOB_NAME: review-qa-blocking
allure-report-qa-all:
extends:
@@ -145,18 +134,11 @@ allure-report-qa-all:
- .review:rules:review-qa-all-report
needs: ["review-qa-all"]
variables:
- ALLURE_REPORT_PATH_PREFIX: gitlab-review-all
ALLURE_JOB_NAME: review-qa-all
knapsack-report:
extends:
- - .review:rules:knapsack-report
- image:
- name: ${QA_IMAGE}
- entrypoint: [""]
+ - .generate-knapsack-report-base
stage: post-qa
- allow_failure: true
- before_script:
- - cd qa
- script:
- - bundle exec rake 'knapsack:upload[tmp/knapsack/*/*.json]'
+ variables:
+ QA_KNAPSACK_REPORT_FILE_PATTERN: $CI_PROJECT_DIR/tmp/knapsack/*/*.json
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 8824c333f96..2494fea94a6 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -1583,17 +1583,12 @@
#
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76756
-# Since `review-qa-smoke` isn't allowed to fail, we need to use `when: always` for `review-qa-smoke-report`.
-.review:rules:review-qa-smoke-report:
- rules:
- - when: always
-
.review:rules:review-qa-reliable:
rules:
- when: on_success
# Since `review-qa-reliable` isn't allowed to fail, we need to use `when: always`for `review-qa-reliable-report`.
-.review:rules:review-qa-reliable-report:
+.review:rules:review-qa-blocking-report:
rules:
- when: always
@@ -1613,11 +1608,6 @@
- when: on_success
- when: on_failure
-.review:rules:knapsack-report:
- rules:
- - if: '$KNAPSACK_GENERATE_REPORT == "true"'
- when: always
-
.review:rules:review-cleanup:
rules:
- <<: *if-not-ee
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index 43c989b5531..373aea97570 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-1.56.1
+1.57.0
diff --git a/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue b/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue
index 4e65d2d5055..025c48f355d 100644
--- a/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue
+++ b/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue
@@ -12,7 +12,17 @@ import { mapState, mapGetters } from 'vuex';
import { s__ } from '~/locale';
import highlight from '~/lib/utils/highlight';
import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
-import { GROUPS_CATEGORY, PROJECTS_CATEGORY, LARGE_AVATAR_PX, SMALL_AVATAR_PX } from '../constants';
+import { truncateNamespace } from '~/lib/utils/text_utility';
+
+import {
+ GROUPS_CATEGORY,
+ PROJECTS_CATEGORY,
+ MERGE_REQUEST_CATEGORY,
+ ISSUES_CATEGORY,
+ RECENT_EPICS_CATEGORY,
+ LARGE_AVATAR_PX,
+ SMALL_AVATAR_PX,
+} from '../constants';
export default {
name: 'HeaderSearchAutocompleteItems',
@@ -40,7 +50,7 @@ export default {
},
},
computed: {
- ...mapState(['search', 'loading', 'autocompleteError']),
+ ...mapState(['search', 'loading', 'autocompleteError', 'searchContext']),
...mapGetters(['autocompleteGroupedSearchOptions']),
},
watch: {
@@ -53,6 +63,13 @@ export default {
},
},
methods: {
+ truncateNamespace(string) {
+ if (string.split(' / ').length > 2) {
+ return truncateNamespace(string);
+ }
+
+ return string;
+ },
highlightedName(val) {
return highlight(val, this.search);
},
@@ -66,6 +83,35 @@ export default {
isOptionFocused(data) {
return this.currentFocusedOption?.html_id === data.html_id;
},
+ isProjectsCategory(data) {
+ return data.category === PROJECTS_CATEGORY;
+ },
+ getEntityId(data) {
+ switch (data.category) {
+ case GROUPS_CATEGORY:
+ case RECENT_EPICS_CATEGORY:
+ return data.group_id || data.id || this.searchContext?.group?.id;
+ case PROJECTS_CATEGORY:
+ case ISSUES_CATEGORY:
+ case MERGE_REQUEST_CATEGORY:
+ return data.project_id || data.id || this.searchContext?.project?.id;
+ default:
+ return data.id;
+ }
+ },
+ getEntitytName(data) {
+ switch (data.category) {
+ case GROUPS_CATEGORY:
+ case RECENT_EPICS_CATEGORY:
+ return data.group_name || data.value || data.label || this.searchContext?.group?.name;
+ case PROJECTS_CATEGORY:
+ case ISSUES_CATEGORY:
+ case MERGE_REQUEST_CATEGORY:
+ return data.project_name || data.value || data.label || this.searchContext?.project?.name;
+ default:
+ return data.label;
+ }
+ },
},
AVATAR_SHAPE_OPTION_RECT,
};
@@ -92,12 +138,22 @@ export default {
-
+
+
+
+
diff --git a/app/assets/javascripts/header_search/constants.js b/app/assets/javascripts/header_search/constants.js
index 9cb08605444..045a552efb0 100644
--- a/app/assets/javascripts/header_search/constants.js
+++ b/app/assets/javascripts/header_search/constants.js
@@ -20,6 +20,12 @@ export const GROUPS_CATEGORY = 'Groups';
export const PROJECTS_CATEGORY = 'Projects';
+export const ISSUES_CATEGORY = 'Recent issues';
+
+export const MERGE_REQUEST_CATEGORY = 'Recent merge requests';
+
+export const RECENT_EPICS_CATEGORY = 'Recent epics';
+
export const LARGE_AVATAR_PX = 32;
export const SMALL_AVATAR_PX = 16;
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue
index 005c3bcd0e3..1fc40e5c0d6 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item.vue
@@ -43,7 +43,9 @@ export default {
message: s__(
'Integrations|You should now see GitLab.com activity inside your Jira Cloud issues. %{linkStart}Learn more%{linkEnd}',
),
- linkUrl: helpPagePath('integration/jira_development_panel.html', { anchor: 'usage' }),
+ linkUrl: helpPagePath('integration/jira_development_panel.html', {
+ anchor: 'use-the-integration',
+ }),
variant: 'success',
});
diff --git a/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue b/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue
index 754908c9385..1eb383a1904 100644
--- a/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue
+++ b/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue
@@ -60,7 +60,8 @@ export default {
{{ description }}
- {{ __('IP Address') }} {{ ipAddress }}
+ {{ __('IP Address') }}
+ {{ ipAddress }}
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 93e56867780..e5d793b1099 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -293,7 +293,7 @@ class GroupsController < Groups::ApplicationController
:setup_for_company,
:jobs_to_be_done,
:crm_enabled
- ]
+ ] + [group_feature_attributes: group_feature_attributes]
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -396,6 +396,10 @@ class GroupsController < Groups::ApplicationController
experiment(:require_verification_for_namespace_creation, user: current_user).track(:start_create_group)
end
+
+ def group_feature_attributes
+ []
+ end
end
GroupsController.prepend_mod_with('GroupsController')
diff --git a/app/experiments/ios_specific_templates_experiment.rb b/app/experiments/ios_specific_templates_experiment.rb
new file mode 100644
index 00000000000..1731fa87be8
--- /dev/null
+++ b/app/experiments/ios_specific_templates_experiment.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+class IosSpecificTemplatesExperiment < ApplicationExperiment
+ before_run(if: :skip_experiment) { throw(:abort) } # rubocop:disable Cop/BanCatchThrow
+
+ private
+
+ def skip_experiment
+ actor_not_able_to_create_pipelines? ||
+ project_targets_non_ios_platforms? ||
+ project_has_gitlab_ci? ||
+ project_has_pipelines?
+ end
+
+ def actor_not_able_to_create_pipelines?
+ !context.actor.is_a?(User) || !context.actor.can?(:create_pipeline, context.project)
+ end
+
+ def project_targets_non_ios_platforms?
+ context.project.project_setting.target_platforms.exclude?('ios')
+ end
+
+ def project_has_gitlab_ci?
+ context.project.has_ci? && context.project.builds_enabled?
+ end
+
+ def project_has_pipelines?
+ context.project.all_pipelines.count > 0
+ end
+end
diff --git a/app/helpers/ci/pipelines_helper.rb b/app/helpers/ci/pipelines_helper.rb
index 8d2f83409be..70d2a4fafd1 100644
--- a/app/helpers/ci/pipelines_helper.rb
+++ b/app/helpers/ci/pipelines_helper.rb
@@ -106,6 +106,12 @@ module Ci
e.candidate { data[:any_runners_available] = project.active_runners.exists?.to_s }
end
+ experiment(:ios_specific_templates, actor: current_user, project: project, sticky_to: project) do |e|
+ e.candidate do
+ data[:registration_token] = project.runners_token if can?(current_user, :register_project_runners, project)
+ end
+ end
+
data
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 8586e93c88f..f8bfc74b344 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -260,6 +260,7 @@ module SearchHelper
{
category: "Groups",
id: group.id,
+ value: "#{search_result_sanitize(group.name)}",
label: "#{search_result_sanitize(group.full_name)}",
url: group_path(group),
avatar_url: group.avatar_url || ''
@@ -311,7 +312,9 @@ module SearchHelper
id: mr.id,
label: search_result_sanitize(mr.title),
url: merge_request_path(mr),
- avatar_url: mr.project.avatar_url || ''
+ avatar_url: mr.project.avatar_url || '',
+ project_id: mr.target_project_id,
+ project_name: mr.target_project.name
}
end
end
@@ -325,7 +328,9 @@ module SearchHelper
id: i.id,
label: search_result_sanitize(i.title),
url: issue_path(i),
- avatar_url: i.project.avatar_url || ''
+ avatar_url: i.project.avatar_url || '',
+ project_id: i.project_id,
+ project_name: i.project.name
}
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index c15c3a65e74..6e5c694cb9c 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1946,6 +1946,10 @@ class Project < ApplicationRecord
Gitlab.config.pages.enabled
end
+ def pages_show_onboarding?
+ !(pages_metadatum&.onboarding_complete || pages_metadatum&.deployed)
+ end
+
def remove_private_deploy_keys
exclude_keys_linked_to_other_projects = <<-SQL
NOT EXISTS (
@@ -1961,6 +1965,10 @@ class Project < ApplicationRecord
.delete_all
end
+ def mark_pages_onboarding_complete
+ ensure_pages_metadatum.update!(onboarding_complete: true)
+ end
+
def mark_pages_as_deployed
ensure_pages_metadatum.update!(deployed: true)
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 4610b5a9398..68c1b1c5549 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -476,7 +476,7 @@ class User < ApplicationRecord
scope :with_no_activity, -> { with_state(:active).human_or_service_user.where(last_activity_on: nil) }
scope :by_provider_and_extern_uid, ->(provider, extern_uid) { joins(:identities).merge(Identity.with_extern_uid(provider, extern_uid)) }
scope :by_ids_or_usernames, -> (ids, usernames) { where(username: usernames).or(where(id: ids)) }
- scope :without_forbidden_states, -> { confirmed.where.not(state: FORBIDDEN_SEARCH_STATES) }
+ scope :without_forbidden_states, -> { where.not(state: FORBIDDEN_SEARCH_STATES) }
strip_attributes! :name
@@ -1732,8 +1732,12 @@ class User < ApplicationRecord
end
def attention_requested_open_merge_requests_count(force: false)
- Rails.cache.fetch(attention_request_cache_key, force: force, expires_in: COUNT_CACHE_VALIDITY_PERIOD) do
+ if Feature.enabled?(:uncached_mr_attention_requests_count, self, default_enabled: :yaml)
MergeRequestsFinder.new(self, attention: self.username, state: 'opened', non_archived: true).execute.count
+ else
+ Rails.cache.fetch(attention_request_cache_key, force: force, expires_in: COUNT_CACHE_VALIDITY_PERIOD) do
+ MergeRequestsFinder.new(self, attention: self.username, state: 'opened', non_archived: true).execute.count
+ end
end
end
diff --git a/app/serializers/merge_request_serializer.rb b/app/serializers/merge_request_serializer.rb
index e8fc18e6cf3..9fd50c8c51d 100644
--- a/app/serializers/merge_request_serializer.rb
+++ b/app/serializers/merge_request_serializer.rb
@@ -27,5 +27,3 @@ class MergeRequestSerializer < BaseSerializer
super(merge_request, opts, entity)
end
end
-
-MergeRequestSerializer.prepend_mod_with('MergeRequestSerializer')
diff --git a/app/services/jira/requests/base.rb b/app/services/jira/requests/base.rb
index a16f8bbd367..3e15d47e8af 100644
--- a/app/services/jira/requests/base.rb
+++ b/app/services/jira/requests/base.rb
@@ -68,7 +68,7 @@ module Jira
end
def auth_docs_link_start
- auth_docs_link_url = Rails.application.routes.url_helpers.help_page_path('integration/jira', anchor: 'authentication-in-jira')
+ auth_docs_link_url = Rails.application.routes.url_helpers.help_page_path('integration/jira/index', anchor: 'authentication-in-jira')
''.html_safe % { url: auth_docs_link_url }
end
diff --git a/app/views/admin/application_settings/_git_lfs_limits.html.haml b/app/views/admin/application_settings/_git_lfs_limits.html.haml
index de5a2ceaa3d..b8970a5bcf1 100644
--- a/app/views/admin/application_settings/_git_lfs_limits.html.haml
+++ b/app/views/admin/application_settings/_git_lfs_limits.html.haml
@@ -1,16 +1,13 @@
-= form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-git-lfs-limits-settings'), html: { class: 'fieldset-form' } do |f|
+= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-git-lfs-limits-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
%h5
= _('Authenticated Git LFS request rate limit')
.form-group
- .form-check
- = f.check_box :throttle_authenticated_git_lfs_enabled, class: 'form-check-input', data: { qa_selector: 'throttle_authenticated_git_lfs_checkbox' }
- = f.label :throttle_authenticated_git_lfs_enabled, class: 'form-check-label gl-font-weight-bold' do
- = _('Enable authenticated Git LFS request rate limit')
- %span.form-text.gl-text-gray-600
- = _('Helps reduce request volume (for example, from crawlers or abusive bots)')
+ = f.gitlab_ui_checkbox_component :throttle_authenticated_git_lfs_enabled,
+ _('Enable authenticated Git LFS request rate limit'),
+ help_text: _('Helps reduce request volume (for example, from crawlers or abusive bots)')
.form-group
= f.label :throttle_authenticated_git_lfs_requests_per_period, _('Max authenticated Git LFS requests per period per user'), class: 'gl-font-weight-bold'
= f.number_field :throttle_authenticated_git_lfs_requests_per_period, class: 'form-control gl-form-input'
diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml
index a1990ad5750..925b3681298 100644
--- a/app/views/admin/applications/_form.html.haml
+++ b/app/views/admin/applications/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [:admin, @application], url: @url, html: {role: 'form'} do |f|
+= gitlab_ui_form_for [:admin, @application], url: @url, html: {role: 'form'} do |f|
= form_errors(application)
= content_tag :div, class: 'form-group row' do
@@ -45,7 +45,7 @@
.col-sm-2.col-form-label.pt-0
= f.label :scopes
.col-sm-10
- = render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes
+ = render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes, f: f
.form-actions
= f.submit _('Save application'), class: "gl-button btn btn-confirm wide"
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index 5f12cd048b3..8ac6f63cdfb 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -18,7 +18,7 @@
.form-group.row
.offset-sm-2.col-sm-10
- = render 'shared/allow_request_access', form: f, bold_label: true
+ = render 'shared/allow_request_access', form: f
= render 'groups/group_admin_settings', f: f
diff --git a/app/views/groups/_group_admin_settings.html.haml b/app/views/groups/_group_admin_settings.html.haml
index ab6861b5f24..785ca71b371 100644
--- a/app/views/groups/_group_admin_settings.html.haml
+++ b/app/views/groups/_group_admin_settings.html.haml
@@ -2,14 +2,12 @@
.col-sm-2.col-form-label.pt-0
= f.label :lfs_enabled, _('Large File Storage')
.col-sm-10
- .form-check
- = f.check_box :lfs_enabled, checked: @group.lfs_enabled?, class: 'form-check-input'
- = f.label :lfs_enabled, class: 'form-check-label' do
- %strong
- = _('Allow projects within this group to use Git LFS')
- = link_to sprite_icon('question-o'), help_page_path('topics/git/lfs/index')
- %br/
- %span= _('This setting can be overridden in each project.')
+ - label = _('Allow projects within this group to use Git LFS')
+ - help_link = link_to sprite_icon('question-o'), help_page_path('topics/git/lfs/index'), class: 'gl-ml-2'
+ = f.gitlab_ui_checkbox_component :lfs_enabled,
+ '%{label}%{help_link}'.html_safe % { label: label, help_link: help_link },
+ help_text: _('This setting can be overridden in each project.'),
+ checkbox_options: { checked: @group.lfs_enabled? }
.form-group.row
.col-sm-2.col-form-label
= f.label s_('ProjectCreationLevel|Allowed to create projects')
@@ -26,12 +24,9 @@
.col-sm-2.col-form-label.pt-0
= f.label :require_two_factor_authentication, _('Two-factor authentication')
.col-sm-10
- .form-check
- = f.check_box :require_two_factor_authentication, class: 'form-check-input'
- = f.label :require_two_factor_authentication, class: 'form-check-label' do
- %strong
- = _("Require all users in this group to set up two-factor authentication")
- = link_to sprite_icon('question-o'), help_page_path('security/two_factor_authentication', anchor: 'enforce-2fa-for-all-users-in-a-group')
+ - label = _("Require all users in this group to set up two-factor authentication")
+ - help_link = link_to sprite_icon('question-o'), help_page_path('security/two_factor_authentication', anchor: 'enforce-2fa-for-all-users-in-a-group'), class: 'gl-ml-2'
+ = f.gitlab_ui_checkbox_component :require_two_factor_authentication, '%{label}%{help_link}'.html_safe % { label: label, help_link: help_link }
.form-group.row
.offset-sm-2.col-sm-10
.form-check
diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml
index dd62c9e118d..1a2f770cd59 100644
--- a/app/views/groups/settings/_permissions.html.haml
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -34,6 +34,8 @@
= render 'groups/settings/ip_restriction_registration_features_cta', f: f
= render_if_exists 'groups/settings/ip_restriction', f: f, group: @group
= render_if_exists 'groups/settings/allowed_email_domain', f: f, group: @group
+ - if Feature.enabled?(:group_wiki_settings_toggle, @group, default_enabled: :yaml)
+ = render_if_exists 'groups/settings/wiki', f: f, group: @group
= render 'groups/settings/lfs', f: f
= render 'groups/settings/project_creation_level', f: f, group: @group
= render 'groups/settings/subgroup_creation_level', f: f, group: @group
diff --git a/app/views/shared/_allow_request_access.html.haml b/app/views/shared/_allow_request_access.html.haml
index ca09fd39dc1..608a0ca37d9 100644
--- a/app/views/shared/_allow_request_access.html.haml
+++ b/app/views/shared/_allow_request_access.html.haml
@@ -1,6 +1,3 @@
-- label_class = local_assigns.fetch(:bold_label, false) ? 'font-weight-bold' : ''
-
= form.gitlab_ui_checkbox_component :request_access_enabled,
_('Allow users to request access (if visibility is public or internal)'),
- label_options: { class: label_class },
checkbox_options: { data: { qa_selector: 'request_access_checkbox' } }
diff --git a/app/views/shared/access_tokens/_form.html.haml b/app/views/shared/access_tokens/_form.html.haml
index 0b68cfe65e5..d4106ba4e5d 100644
--- a/app/views/shared/access_tokens/_form.html.haml
+++ b/app/views/shared/access_tokens/_form.html.haml
@@ -10,7 +10,7 @@
%p.profile-settings-content
= _("Enter the name of your application, and we'll return a unique %{type}.") % { type: type }
-= form_for token, as: prefix, url: path, method: :post, html: { class: 'js-requires-input' } do |f|
+= gitlab_ui_form_for token, as: prefix, url: path, method: :post, html: { class: 'js-requires-input' } do |f|
= form_errors(token)
@@ -42,7 +42,7 @@
%p.text-secondary#select_scope_help_text
= s_('Tokens|Scopes set the permission levels granted to the token.')
= link_to _("Learn more."), help_path, target: '_blank', rel: 'noopener noreferrer'
- = render 'shared/tokens/scopes_form', prefix: prefix, token: token, scopes: scopes
+ = render 'shared/tokens/scopes_form', prefix: prefix, token: token, scopes: scopes, f: f
- if prefix == :personal_access_token && Feature.enabled?(:personal_access_tokens_scoped_to_projects, current_user)
.js-access-tokens-projects
diff --git a/app/views/shared/deploy_keys/_project_group_form.html.haml b/app/views/shared/deploy_keys/_project_group_form.html.haml
index 8da48a7936a..c9edf09b350 100644
--- a/app/views/shared/deploy_keys/_project_group_form.html.haml
+++ b/app/views/shared/deploy_keys/_project_group_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace, @project, @deploy_keys.new_key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input container" } do |f|
+= gitlab_ui_form_for [@project.namespace, @project, @deploy_keys.new_key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input container" } do |f|
= form_errors(@deploy_keys.new_key)
.form-group.row
= f.label :title, class: "label-bold"
@@ -13,12 +13,8 @@
= f.fields_for :deploy_keys_projects do |deploy_keys_project_form|
.form-group.row
- = deploy_keys_project_form.label :can_push do
- = deploy_keys_project_form.check_box :can_push
- %strong= _('Grant write permissions to this key')
- .form-group.row
- %p.light.gl-mb-0
- = _('Allow this key to push to this repository')
+ = deploy_keys_project_form.gitlab_ui_checkbox_component :can_push, _('Grant write permissions to this key'),
+ help_text: _('Allow this key to push to this repository')
.form-group.row
= f.submit _("Add key"), class: "btn gl-button btn-confirm", data: { qa_selector: "add_deploy_key_button"}
diff --git a/app/views/shared/doorkeeper/applications/_form.html.haml b/app/views/shared/doorkeeper/applications/_form.html.haml
index adfd7ea98b7..c1650405776 100644
--- a/app/views/shared/doorkeeper/applications/_form.html.haml
+++ b/app/views/shared/doorkeeper/applications/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for @application, url: url, html: { role: 'form', class: 'doorkeeper-app-form' } do |f|
+= gitlab_ui_form_for @application, url: url, html: { role: 'form', class: 'doorkeeper-app-form' } do |f|
= form_errors(@application)
.form-group
@@ -12,22 +12,19 @@
%span.form-text.text-muted
= _('Use one line per URI')
- .form-group.form-check
- = f.check_box :confidential, class: 'form-check-input'
- = f.label :confidential, class: 'label-bold form-check-label'
- %span.form-text.text-muted
- = _('Enable only for confidential applications exclusively used by a trusted backend server that can securely store the client secret. Do not enable for native-mobile, single-page, or other JavaScript applications because they cannot keep the client secret confidential.')
+ .form-group
+ = f.gitlab_ui_checkbox_component :confidential, _('Confidential'),
+ help_text: _('Enable only for confidential applications exclusively used by a trusted backend server that can securely store the client secret. Do not enable for native-mobile, single-page, or other JavaScript applications because they cannot keep the client secret confidential.')
- .form-group.form-check
- = f.check_box :expire_access_tokens, class: 'form-check-input'
- = f.label :expire_access_tokens, class: 'label-bold form-check-label'
- %span.form-text.text-muted
- = _('Enable access tokens to expire after 2 hours. If disabled, tokens do not expire.')
- = link_to _('Learn more.'), help_page_path('integration/oauth_provider.md', anchor: 'expiring-access-tokens'), target: '_blank', rel: 'noopener noreferrer'
+ .form-group
+ - help_text = _('Enable access tokens to expire after 2 hours. If disabled, tokens do not expire.')
+ - help_link = link_to _('Learn more.'), help_page_path('integration/oauth_provider.md', anchor: 'expiring-access-tokens'), target: '_blank', rel: 'noopener noreferrer'
+ = f.gitlab_ui_checkbox_component :expire_access_tokens, _('Expire access tokens'),
+ help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link }
.form-group
= f.label :scopes, class: 'label-bold'
- = render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: @application, scopes: @scopes
+ = render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: @application, scopes: @scopes, f: f
.gl-mt-3
= f.submit _('Save application'), class: "gl-button btn btn-confirm"
diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml
index 33e95446bd7..010376464f1 100644
--- a/app/views/shared/tokens/_scopes_form.html.haml
+++ b/app/views/shared/tokens/_scopes_form.html.haml
@@ -1,9 +1,14 @@
- scopes = local_assigns.fetch(:scopes)
- prefix = local_assigns.fetch(:prefix)
- token = local_assigns.fetch(:token)
+- f = local_assigns.fetch(:f)
-- scopes.each do |scope|
- %fieldset.form-group.form-check
- = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", class: "form-check-input", data: { qa_selector: "#{scope}_checkbox" }
- = label_tag "#{prefix}_scopes_#{scope}", scope, class: 'label-bold form-check-label'
- .text-secondary= t scope, scope: scope_description(prefix)
+%fieldset
+ - scopes.each do |scope|
+ - help_text = t scope, scope: scope_description(prefix)
+ = f.gitlab_ui_checkbox_component :scopes, scope,
+ help_text: help_text,
+ checkbox_options: { checked: token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", multiple: true, data: { qa_selector: "#{scope}_checkbox" } },
+ checked_value: scope,
+ unchecked_value: nil,
+ label_options: { data: { qa_selector: "#{scope}_label" } }
diff --git a/config/feature_flags/development/group_wiki_settings_toggle.yml b/config/feature_flags/development/group_wiki_settings_toggle.yml
new file mode 100644
index 00000000000..083453a6944
--- /dev/null
+++ b/config/feature_flags/development/group_wiki_settings_toggle.yml
@@ -0,0 +1,8 @@
+---
+name: group_wiki_settings_toggle
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82298
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/358387
+milestone: '14.10'
+type: development
+group: group::editor
+default_enabled: false
diff --git a/config/feature_flags/development/uncached_mr_attention_requests_count.yml b/config/feature_flags/development/uncached_mr_attention_requests_count.yml
new file mode 100644
index 00000000000..239490ab1c2
--- /dev/null
+++ b/config/feature_flags/development/uncached_mr_attention_requests_count.yml
@@ -0,0 +1,8 @@
+---
+name: uncached_mr_attention_requests_count
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84145
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/357480
+milestone: '14.10'
+type: development
+group: group::code review
+default_enabled: false
diff --git a/config/feature_flags/experiment/ios_specific_templates.yml b/config/feature_flags/experiment/ios_specific_templates.yml
new file mode 100644
index 00000000000..0af80e7a5bb
--- /dev/null
+++ b/config/feature_flags/experiment/ios_specific_templates.yml
@@ -0,0 +1,8 @@
+---
+name: ios_specific_templates
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84589
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/356398
+milestone: "14.10"
+type: experiment
+group: group::activation
+default_enabled: false
diff --git a/config/metrics/counts_28d/20220407183012_count_distinct_user_id_from_deployment_approvals.yml b/config/metrics/counts_28d/20220407183012_count_distinct_user_id_from_deployment_approvals.yml
new file mode 100644
index 00000000000..7b594efbf19
--- /dev/null
+++ b/config/metrics/counts_28d/20220407183012_count_distinct_user_id_from_deployment_approvals.yml
@@ -0,0 +1,21 @@
+---
+key_path: usage_activity_by_stage_monthly.release.users_creating_deployment_approvals
+description: Users who have used the deployment approvals feature by month
+product_section: ops
+product_stage: release
+product_group: group::release
+product_category: continuous_delivery
+value_type: number
+status: active
+milestone: "14.10"
+introduced_by_url:
+time_frame: 28d
+data_source: database
+data_category: optional
+instrumentation_class: CountUsersDeploymentApprovals
+performance_indicator_type: []
+distribution:
+- ee
+tier:
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20220407183012_count_distinct_user_id_from_deployment_approvals.yml b/config/metrics/counts_all/20220407183012_count_distinct_user_id_from_deployment_approvals.yml
new file mode 100644
index 00000000000..902347d9f0b
--- /dev/null
+++ b/config/metrics/counts_all/20220407183012_count_distinct_user_id_from_deployment_approvals.yml
@@ -0,0 +1,21 @@
+---
+key_path: usage_activity_by_stage.release.users_creating_deployment_approvals
+description: Users who have used the deployment approvals feature by all time
+product_section: ops
+product_stage: release
+product_group: group::release
+product_category: continuous_delivery
+value_type: number
+status: active
+milestone: "14.10"
+introduced_by_url:
+time_frame: all
+data_source: database
+data_category: optional
+instrumentation_class: CountUsersDeploymentApprovals
+performance_indicator_type: []
+distribution:
+- ee
+tier:
+- premium
+- ultimate
diff --git a/data/removals/15_0/15-0-rerequest-review.yml b/data/removals/15_0/15-0-rerequest-review.yml
deleted file mode 100644
index e692df05670..00000000000
--- a/data/removals/15_0/15-0-rerequest-review.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-- name: "Request a new review" # the name of the feature being removed. Avoid the words `deprecation`, `deprecate`, `removal`, and `remove` in this field because these are implied.
- announcement_milestone: "15.0" # The milestone when this feature was deprecated.
- announcement_date: "2022-05-22" # 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: "15.0" # The milestone when this feature is being removed.
- removal_date: "2022-05-22" # 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: false # Change to true if this removal is a breaking change.
- reporter: phikai # GitLab username of the person reporting the removal
- body: | # Do not modify this line, instead modify the lines below.
- The ability to [request a new review](https://docs.gitlab.com/ee/user/project/merge_requests/reviews/#request-a-new-review) has been removed in GitLab 15.0. This feature is replaced with [requesting attention](https://docs.gitlab.com/ee/user/project/merge_requests/#request-attention-to-a-merge-request) to a merge request.
-# The following items are not published on the docs page, but may be used in the future.
- stage: Create # (optional - may be required in the future) String value of the stage that the feature was created in. e.g., Growth
- 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]
- issue_url: # (optional) This is a link to the deprecation issue in GitLab
- documentation_url: https://docs.gitlab.com/ee/user/project/merge_requests/reviews/#request-a-new-review # (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/20220318141037_add_pages_onboarding_state.rb b/db/migrate/20220318141037_add_pages_onboarding_state.rb
new file mode 100644
index 00000000000..e320bee63c4
--- /dev/null
+++ b/db/migrate/20220318141037_add_pages_onboarding_state.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddPagesOnboardingState < Gitlab::Database::Migration[1.0]
+ def up
+ add_column :project_pages_metadata, :onboarding_complete, :boolean, default: false, null: false
+ end
+
+ def down
+ remove_column :project_pages_metadata, :onboarding_complete
+ end
+end
diff --git a/db/post_migrate/20220322132242_update_pages_onboarding_state.rb b/db/post_migrate/20220322132242_update_pages_onboarding_state.rb
new file mode 100644
index 00000000000..896ab78a266
--- /dev/null
+++ b/db/post_migrate/20220322132242_update_pages_onboarding_state.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class UpdatePagesOnboardingState < Gitlab::Database::Migration[1.0]
+ disable_ddl_transaction!
+ BATCH_SIZE = 75
+
+ def up
+ define_batchable_model(
+ :project_pages_metadata
+ ).where(
+ deployed: true
+ ).each_batch(
+ of: BATCH_SIZE,
+ column: :project_id
+ ) do |batch|
+ batch.update_all(onboarding_complete: true)
+ end
+ end
+
+ def down
+ define_batchable_model(
+ :project_pages_metadata
+ ).where(
+ onboarding_complete: true
+ ).each_batch(
+ of: BATCH_SIZE,
+ column: :project_id
+ ) do |batch|
+ batch.update_all(onboarding_complete: false)
+ end
+ end
+end
diff --git a/db/schema_migrations/20220318141037 b/db/schema_migrations/20220318141037
new file mode 100644
index 00000000000..e2451a42392
--- /dev/null
+++ b/db/schema_migrations/20220318141037
@@ -0,0 +1 @@
+d9a9d143ff553cbad5eb32a908370133549850f10b27b30eb6a1bde686054c45
\ No newline at end of file
diff --git a/db/schema_migrations/20220322132242 b/db/schema_migrations/20220322132242
new file mode 100644
index 00000000000..f21f62874a7
--- /dev/null
+++ b/db/schema_migrations/20220322132242
@@ -0,0 +1 @@
+94dbae9bbfb2da49a0d20b674e15f457c613c8ba0612603dd8fe9ac5699160d1
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 9535e7deab0..771fce97e59 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -19304,7 +19304,8 @@ ALTER SEQUENCE project_mirror_data_id_seq OWNED BY project_mirror_data.id;
CREATE TABLE project_pages_metadata (
project_id bigint NOT NULL,
deployed boolean DEFAULT false NOT NULL,
- pages_deployment_id bigint
+ pages_deployment_id bigint,
+ onboarding_complete boolean DEFAULT false NOT NULL
);
CREATE TABLE project_repositories (
diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md
index 5a9699b3a39..cdf6d48ad41 100644
--- a/doc/administration/integration/plantuml.md
+++ b/doc/administration/integration/plantuml.md
@@ -213,7 +213,7 @@ After configuring your local PlantUML server, you're ready to enable the PlantUM
1. On the left sidebar, go to **Settings > General** and expand the **PlantUML** section.
1. Select the **Enable PlantUML** checkbox.
1. Set the PlantUML instance as `https://gitlab.example.com/-/plantuml/`,
- and click **Save changes**.
+ and select **Save changes**.
Depending on your PlantUML and GitLab version numbers, you may also need to take
these steps:
diff --git a/doc/api/users.md b/doc/api/users.md
index e9fe6bf7dfe..7b4962735e6 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -305,8 +305,11 @@ Parameters:
"website_url": "",
"organization": "",
"job_title": "Operations Specialist",
+ "pronouns": "he/him",
+ "work_information": null,
"followers": 1,
- "following": 1
+ "following": 1,
+ "local_time": "3:38 PM"
}
```
@@ -346,6 +349,11 @@ Example Responses:
"website_url": "",
"organization": "",
"job_title": "Operations Specialist",
+ "pronouns": "he/him",
+ "work_information": null,
+ "followers": 1,
+ "following": 1,
+ "local_time": "3:38 PM",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
"theme_id": 1,
@@ -580,6 +588,13 @@ GET /user
"twitter": "",
"website_url": "",
"organization": "",
+ "job_title": "",
+ "pronouns": "he/him",
+ "bot": false,
+ "work_information": null,
+ "followers": 0,
+ "following": 0,
+ "local_time": "3:38 PM",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
"theme_id": 1,
@@ -596,10 +611,13 @@ GET /user
"can_create_project": true,
"two_factor_enabled": true,
"external": false,
- "private_profile": false
+ "private_profile": false,
+ "commit_email": "admin@example.com",
}
```
+Users on [GitLab Premium or higher](https://about.gitlab.com/pricing/) also see the `shared_runners_minutes_limit`, `extra_shared_runners_minutes_limit` parameters.
+
## List current user (for admins)
> The `namespace_id` field in the response was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82045) in GitLab 14.10.
@@ -654,7 +672,8 @@ Parameters:
"commit_email": "john-codes@example.com",
"current_sign_in_ip": "196.165.1.102",
"last_sign_in_ip": "172.127.2.22",
- "namespace_id": 1
+ "namespace_id": 1,
+ "note": null
}
```
diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md
index 4d74dbb2616..921af68d908 100644
--- a/doc/development/documentation/styleguide/index.md
+++ b/doc/development/documentation/styleguide/index.md
@@ -1518,6 +1518,47 @@ The voting strategy in GitLab 13.4 and later requires the primary and secondary
voters to agree.
```
+#### Deprecated features
+
+When a feature is deprecated, add `(DEPRECATED)` to the page title or to
+the heading of the section documenting the feature, immediately before
+the tier badge:
+
+```markdown
+
+# Feature A (DEPRECATED) **(ALL TIERS)**
+
+
+## Feature B (DEPRECATED) **(PREMIUM SELF)**
+```
+
+Add the deprecation to the version history note (you can include a link
+to a replacement when available):
+
+```markdown
+> - [Deprecated]() in GitLab 11.3. Replaced by [meaningful text]().
+```
+
+You can also describe the replacement in surrounding text, if available. If the
+deprecation isn't obvious in existing text, you may want to include a warning:
+
+```markdown
+WARNING:
+This feature was [deprecated](link-to-issue) in GitLab 12.3 and replaced by
+[Feature name](link-to-feature-documentation).
+```
+
+If you add `(DEPRECATED)` to the page's title and the document is linked from the docs
+navigation, either remove the page from the nav or update the nav item to include the
+same text before the feature name:
+
+```yaml
+ - doc_title: (DEPRECATED) Feature A
+```
+
+In the first major GitLab version after the feature was deprecated, be sure to
+remove information about that deprecated feature.
+
#### End-of-life for features or products
When a feature or product enters its end-of-life, indicate its status by
@@ -1604,47 +1645,6 @@ To view historical information about a feature, review GitLab
[release posts](https://about.gitlab.com/releases/), or search for the issue or
merge request where the work was done.
-### Deprecated features
-
-When a feature is deprecated, add `(DEPRECATED)` to the page title or to
-the heading of the section documenting the feature, immediately before
-the tier badge:
-
-```markdown
-
-# Feature A (DEPRECATED) **(ALL TIERS)**
-
-
-## Feature B (DEPRECATED) **(PREMIUM SELF)**
-```
-
-Add the deprecation to the version history note (you can include a link
-to a replacement when available):
-
-```markdown
-> - [Deprecated]() in GitLab 11.3. Replaced by [meaningful text]().
-```
-
-You can also describe the replacement in surrounding text, if available. If the
-deprecation isn't obvious in existing text, you may want to include a warning:
-
-```markdown
-WARNING:
-This feature was [deprecated](link-to-issue) in GitLab 12.3 and replaced by
-[Feature name](link-to-feature-documentation).
-```
-
-If you add `(DEPRECATED)` to the page's title and the document is linked from the docs
-navigation, either remove the page from the nav or update the nav item to include the
-same text before the feature name:
-
-```yaml
- - doc_title: (DEPRECATED) Feature A
-```
-
-In the first major GitLab version after the feature was deprecated, be sure to
-remove information about that deprecated feature.
-
## Products and features
Refer to the information in this section when describing products and features
diff --git a/doc/operations/incident_management/incidents.md b/doc/operations/incident_management/incidents.md
index 1a1a607e76f..b18d250f717 100644
--- a/doc/operations/incident_management/incidents.md
+++ b/doc/operations/incident_management/incidents.md
@@ -276,7 +276,7 @@ by changing the status. Setting the status to:
For [incidents created from alerts](alerts.md#create-an-incident-from-an-alert),
updating the incident status also updates the alert status.
-## Change escalation policy **(PREMIUM)**
+### Change escalation policy **(PREMIUM)**
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5716) in GitLab 14.9 [with a flag](../../administration/feature_flags.md) named `incident_escalations`. Disabled by default.
diff --git a/doc/update/removals.md b/doc/update/removals.md
index 80f5006982e..7e2b4f84fa1 100644
--- a/doc/update/removals.md
+++ b/doc/update/removals.md
@@ -28,12 +28,6 @@ For removal reviewers (Technical Writers only):
https://about.gitlab.com/handbook/marketing/blog/release-posts/#update-the-removals-doc
-->
-## 15.0
-
-### Request a new review
-
-The ability to [request a new review](https://docs.gitlab.com/ee/user/project/merge_requests/reviews/#request-a-new-review) has been removed in GitLab 15.0. This feature is replaced with [requesting attention](https://docs.gitlab.com/ee/user/project/merge_requests/#request-attention-to-a-merge-request) to a merge request.
-
## 14.9
### Integrated error tracking disabled by default
diff --git a/doc/user/admin_area/monitoring/background_migrations.md b/doc/user/admin_area/monitoring/background_migrations.md
index 260a8515a1a..726827054da 100644
--- a/doc/user/admin_area/monitoring/background_migrations.md
+++ b/doc/user/admin_area/monitoring/background_migrations.md
@@ -185,3 +185,12 @@ The results from the query can be plugged into the command:
```shell
sudo gitlab-rake gitlab:background_migrations:finalize[CopyColumnUsingBackgroundMigrationJob,events,id,'[["id"]\, ["id_convert_to_bigint"]]']
```
+
+### The `BackfillNamespaceIdForNamespaceRoute` batched migration job fails
+
+In GitLab 14.8, the `BackfillNamespaceIdForNamespaceRoute` batched background migration job
+may fail to complete. When retried, a `500 Server Error` is returned. This issue was
+[resolved](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82387) in GitLab 14.9.
+
+To resolve this issue, [upgrade GitLab](../../../update/index.md) from 14.8 to 14.9.
+You can ignore the failed batch migration until after you update to GitLab 14.9.
diff --git a/doc/user/project/import/img/gitlab_import_history_page_v14_10.png b/doc/user/project/import/img/gitlab_import_history_page_v14_10.png
new file mode 100644
index 00000000000..c93b5ed2b27
Binary files /dev/null and b/doc/user/project/import/img/gitlab_import_history_page_v14_10.png differ
diff --git a/doc/user/project/import/index.md b/doc/user/project/import/index.md
index 41ef15108ec..432f043f945 100644
--- a/doc/user/project/import/index.md
+++ b/doc/user/project/import/index.md
@@ -30,6 +30,30 @@ repository is too large, the import can timeout.
You can also [connect your external repository to get CI/CD benefits](../../../ci/ci_cd_for_external_repos/index.md).
+## Project import history
+
+You can view all project imports created by you. This list includes the following:
+
+- Source (without credentials for security reasons)
+- Destination
+- Status
+- Error details if the import failed
+
+To view project import history:
+
+1. Sign in to GitLab.
+1. On the top bar, select **New** (**{plus}**).
+1. Select **New project/repository**.
+1. Select **Import project**.
+1. Select **History**.
+
+![Project import history page](img/gitlab_import_history_page_v14_10.png)
+
+The history also includes projects created from [built-in](../working_with_projects.md#create-a-project-from-a-built-in-template)
+or [custom](../working_with_projects.md#create-a-project-from-a-built-in-template)
+templates. GitLab uses [import repository by URL](repo_by_url.md)
+to create a new project from a template.
+
## LFS authentication
When importing a project that contains LFS objects, if the project has an [`.lfsconfig`](https://github.com/git-lfs/git-lfs/blob/master/docs/man/git-lfs-config.5.ronn)
diff --git a/lib/api/project_export.rb b/lib/api/project_export.rb
index 843f72c0e1d..8b27d8d2163 100644
--- a/lib/api/project_export.rb
+++ b/lib/api/project_export.rb
@@ -25,7 +25,7 @@ module API
detail 'This feature was introduced in GitLab 10.6.'
end
get ':id/export/download' do
- check_rate_limit! :project_download_export, scope: [current_user, user_project]
+ check_rate_limit! :project_download_export, scope: [current_user, user_project.namespace]
if user_project.export_file_exists?
if user_project.export_archive_exists?
diff --git a/lib/gitlab/ci/templates/Jobs/Secret-Detection.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Secret-Detection.gitlab-ci.yml
index 0ef6f63bb94..a3620cc9733 100644
--- a/lib/gitlab/ci/templates/Jobs/Secret-Detection.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Secret-Detection.gitlab-ci.yml
@@ -31,14 +31,7 @@ secret_detection:
script:
- if [ -n "$CI_COMMIT_TAG" ]; then echo "Skipping Secret Detection for tags. No code changes have occurred."; exit 0; fi
# Historic scan
- - |
- if [ "$SECRET_DETECTION_HISTORIC_SCAN" == "true" ]
- then
- echo "historic scan"
- git fetch --unshallow origin $CI_COMMIT_REF_NAME
- /analyzer run
- exit
- fi
+ - if [ "$SECRET_DETECTION_HISTORIC_SCAN" == "true" ]; then echo "Running Secret Detection Historic Scan"; /analyzer run; exit; fi
# Default branch scan
- if [ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]; then echo "Running Secret Detection on default branch."; /analyzer run; exit; fi
# Push event
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 5a4868fcc0c..f1e022aa053 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1276,9 +1276,6 @@ msgid_plural "+%d more"
msgstr[0] ""
msgstr[1] ""
-msgid "+%{approvers} more approvers"
-msgstr ""
-
msgid "+%{extra} more"
msgstr ""
@@ -1596,9 +1593,6 @@ msgstr ""
msgid "A member of the abuse team will review your report as soon as possible."
msgstr ""
-msgid "A merge request hasn't yet been merged"
-msgstr ""
-
msgid "A new Auto DevOps pipeline has been created, go to the Pipelines page for details"
msgstr ""
@@ -3666,6 +3660,12 @@ msgstr ""
msgid "Allow \"%{group_name}\" to sign you in"
msgstr ""
+msgid "Allow access only to members of this group"
+msgstr ""
+
+msgid "Allow access to everyone"
+msgstr ""
+
msgid "Allow access to members of the following group"
msgstr ""
@@ -4514,9 +4514,6 @@ msgstr ""
msgid "Applying suggestions..."
msgstr ""
-msgid "Approval Status"
-msgstr ""
-
msgid "Approval rules"
msgstr ""
@@ -4708,15 +4705,6 @@ msgstr ""
msgid "ApprovalSettings|This setting is configured in %{groupName} and can only be changed in the group settings by an administrator or group owner."
msgstr ""
-msgid "ApprovalStatusTooltip|Adheres to separation of duties"
-msgstr ""
-
-msgid "ApprovalStatusTooltip|At least one rule does not adhere to separation of duties"
-msgstr ""
-
-msgid "ApprovalStatusTooltip|Fails to adhere to separation of duties"
-msgstr ""
-
msgid "Approvals are optional."
msgstr ""
@@ -9259,9 +9247,6 @@ msgstr ""
msgid "Compliance report"
msgstr ""
-msgid "ComplianceDashboard|created by:"
-msgstr ""
-
msgid "ComplianceFrameworks|Add framework"
msgstr ""
@@ -13145,6 +13130,9 @@ msgstr ""
msgid "Disable group runners"
msgstr ""
+msgid "Disable the group-level wiki"
+msgstr ""
+
msgid "Disable two-factor authentication"
msgstr ""
@@ -15155,6 +15143,9 @@ msgstr ""
msgid "Expiration date:"
msgstr ""
+msgid "Expire access tokens"
+msgstr ""
+
msgid "Expired"
msgstr ""
@@ -18550,9 +18541,6 @@ msgstr ""
msgid "Helps reduce request volume for protected paths."
msgstr ""
-msgid "Here you will find recent merge request activity"
-msgstr ""
-
msgid "Hi %{username}!"
msgstr ""
@@ -37528,9 +37516,6 @@ msgstr ""
msgid "The comparison view may be inaccurate due to merge conflicts."
msgstr ""
-msgid "The compliance report captures merged changes that violate compliance best practices."
-msgstr ""
-
msgid "The compliance report shows the merge request violations merged in protected environments."
msgstr ""
@@ -40474,9 +40459,6 @@ msgstr ""
msgid "Updated date"
msgstr ""
-msgid "Updates"
-msgstr ""
-
msgid "Updating"
msgstr ""
@@ -43960,9 +43942,6 @@ msgid_plural "approvals"
msgstr[0] ""
msgstr[1] ""
-msgid "approved by: "
-msgstr ""
-
msgid "archived"
msgstr ""
@@ -44881,9 +44860,6 @@ msgid_plural "merge requests"
msgstr[0] ""
msgstr[1] ""
-msgid "merged %{timeAgo}"
-msgstr ""
-
msgid "metric_id must be unique across a project"
msgstr ""
@@ -45302,9 +45278,6 @@ msgstr ""
msgid "new merge request"
msgstr ""
-msgid "no approvers"
-msgstr ""
-
msgid "no expiration"
msgstr ""
diff --git a/qa/qa/page/component/access_tokens.rb b/qa/qa/page/component/access_tokens.rb
index 3c8a6cf6a1d..6a9249621e1 100644
--- a/qa/qa/page/component/access_tokens.rb
+++ b/qa/qa/page/component/access_tokens.rb
@@ -19,7 +19,7 @@ module QA
end
base.view 'app/views/shared/tokens/_scopes_form.html.haml' do
- element :api_checkbox, '#{scope}_checkbox' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck
+ element :api_label, '#{scope}_label' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck
end
base.view 'app/views/shared/access_tokens/_created_container.html.haml' do
@@ -36,7 +36,7 @@ module QA
end
def check_api
- check_element(:api_checkbox)
+ click_element(:api_label)
end
def click_create_token_button
diff --git a/qa/qa/resource/runner.rb b/qa/qa/resource/runner.rb
index 2075f302370..c014563671d 100644
--- a/qa/qa/resource/runner.rb
+++ b/qa/qa/resource/runner.rb
@@ -31,7 +31,7 @@ module QA
end
def fabricate_via_api!
- Service::DockerRun::GitlabRunner.new(name).tap do |runner|
+ @docker_container = Service::DockerRun::GitlabRunner.new(name).tap do |runner|
runner.pull
runner.token = @token ||= project.runners_token
runner.address = Runtime::Scenario.gitlab_address
@@ -48,11 +48,20 @@ module QA
def remove_via_api!
runners = list_of_runners(tag_list: @tags)
- return if runners.blank?
+ # If we have no runners, print the logs from the runner docker container in case they show why it isn't running.
+ if runners.blank?
+ dump_logs
+
+ return
+ end
this_runner = runners.find { |runner| runner[:description] == name }
+ # As above, but now we should have a specific runner. If not, print the logs from the runner docker container
+ # to see if we can find out why the runner isn't running.
unless this_runner
+ dump_logs
+
raise "Project #{project.path_with_namespace} does not have a runner with a description matching #{name} #{"or tags #{@tags}" if @tags&.any?}. Runners available: #{runners}"
end
@@ -73,6 +82,10 @@ module QA
parse_body(response)
end
+ def reload!
+ super if method(:running?).super_method.call
+ end
+
def api_delete_path
"/runners/#{id}"
end
@@ -86,6 +99,16 @@ module QA
def api_post_body
end
+
+ private
+
+ def dump_logs
+ if @docker_container.running?
+ @docker_container.logs { |line| QA::Runtime::Logger.debug(line) }
+ else
+ QA::Runtime::Logger.debug("No runner container found named #{name}")
+ end
+ end
end
end
end
diff --git a/qa/qa/service/docker_run/base.rb b/qa/qa/service/docker_run/base.rb
index 512960e8232..85c06e6c307 100644
--- a/qa/qa/service/docker_run/base.rb
+++ b/qa/qa/service/docker_run/base.rb
@@ -11,6 +11,12 @@ module QA
@runner_network = Runtime::Scenario.attributes[:runner_network] || @network
end
+ def logs
+ shell "docker logs #{@name}" do |line|
+ yield " #{line.chomp}"
+ end
+ end
+
def network
shell "docker network inspect #{@network}"
rescue CommandError
diff --git a/qa/qa/service/docker_run/gitlab_runner.rb b/qa/qa/service/docker_run/gitlab_runner.rb
index 595d47bf162..0a8ac39dabd 100644
--- a/qa/qa/service/docker_run/gitlab_runner.rb
+++ b/qa/qa/service/docker_run/gitlab_runner.rb
@@ -43,6 +43,8 @@ module QA
#{@image} #{add_gitlab_tls_cert if @address.include? "https"} && docker exec --detach #{@name} sh -c "#{register_command}"
CMD
+ wait_until_running_and_configured
+
# Prove airgappedness
if runner_network == 'airgapped'
shell("docker exec #{@name} sh -c '#{prove_airgap}'")
@@ -111,6 +113,10 @@ module QA
&& docker cp #{gitlab_tls_certificate.path} #{@name}:/etc/gitlab-runner/certs/gitlab.test.crt
CMD
end
+
+ def wait_until_running_and_configured
+ wait_until_shell_command_matches("docker logs #{@name}", /Configuration loaded/)
+ end
end
end
end
diff --git a/qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb
index a59e4a99b7d..79bba484bea 100644
--- a/qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb
@@ -1,7 +1,9 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Manage', :github, :requires_admin do
+ # Spec uses real github.com, which means outage of github.com can actually block deployment
+ # Keep spec in reliable bucket but don't run in blocking pipelines
+ RSpec.describe 'Manage', :github, :reliable, :skip_live_env, :requires_admin do
describe 'Project import', issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/353583' do
let!(:api_client) { Runtime::API::Client.as_admin }
let!(:group) { Resource::Group.fabricate_via_api! { |resource| resource.api_client = api_client } }
diff --git a/qa/qa/specs/features/api/4_verify/remove_runner_spec.rb b/qa/qa/specs/features/api/4_verify/remove_runner_spec.rb
index 64b15c6ff74..d2eba686992 100644
--- a/qa/qa/specs/features/api/4_verify/remove_runner_spec.rb
+++ b/qa/qa/specs/features/api/4_verify/remove_runner_spec.rb
@@ -15,15 +15,11 @@ module QA
end
end
- before do
- sleep 5 # Runner should register within 5 seconds
- end
-
# Removing a runner via the UI is covered by `spec/features/runners_spec.rb``
it 'removes the runner' do
- runners = runner.list_of_runners(tag_list: runner_tags)
-
- expect(runners.size).to eq(1)
+ runners = nil
+ expect { (runners = runner.list_of_runners(tag_list: runner_tags)).size }
+ .to eventually_eq(1).within(max_duration: 10, sleep_interval: 1)
expect(runners.first[:description]).to eq(executor)
request = Runtime::API::Request.new(api_client, "runners/#{runners.first[:id]}")
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
index a74eda1596b..3bf5a11b074 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
@@ -1,7 +1,9 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Manage', :github, :requires_admin do
+ # Spec uses real github.com, which means outage of github can actually block deployment
+ # Keep spec in reliable bucket but don't run in blocking pipelines
+ RSpec.describe 'Manage', :github, :reliable, :skip_live_env, :requires_admin do
describe 'Project import' do
let(:github_repo) { 'gitlab-qa-github/import-test' }
let(:api_client) { Runtime::API::Client.as_admin }
diff --git a/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb
index 8aa01888ae3..f8261bba342 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb
@@ -22,8 +22,6 @@ module QA
Page::Project::Menu.perform(&:go_to_ci_cd_settings)
Page::Project::Settings::CiCd.perform do |settings|
- sleep 5 # Runner should register within 5 seconds
-
settings.expand_runners_settings do |page|
expect(page).to have_content(executor)
expect(page).to have_online_runner
diff --git a/qa/spec/service/docker_run/gitlab_runner_spec.rb b/qa/spec/service/docker_run/gitlab_runner_spec.rb
index a8838db10cf..d9f201cf67e 100644
--- a/qa/spec/service/docker_run/gitlab_runner_spec.rb
+++ b/qa/spec/service/docker_run/gitlab_runner_spec.rb
@@ -24,6 +24,7 @@ module QA
before do
allow(subject).to receive(:shell)
+ allow(subject).to receive(:wait_until_running_and_configured)
end
context 'defaults' do
diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh
index aba815cdf28..5a3f439009d 100755
--- a/scripts/lint-doc.sh
+++ b/scripts/lint-doc.sh
@@ -128,7 +128,7 @@ function run_locally_or_in_docker() {
$cmd $args
elif hash docker 2>/dev/null
then
- docker run -t -v ${PWD}:/gitlab -w /gitlab --rm registry.gitlab.com/gitlab-org/gitlab-docs/lint-markdown:alpine-3.15-vale-2.15.0-markdownlint-0.31.0 ${cmd} ${args}
+ docker run -t -v ${PWD}:/gitlab -w /gitlab --rm registry.gitlab.com/gitlab-org/gitlab-docs/lint-markdown:alpine-3.15-vale-2.15.5-markdownlint-0.31.1 ${cmd} ${args}
else
echo
echo " ✖ ERROR: '${cmd}' not found. Install '${cmd}' or Docker to proceed." >&2
diff --git a/spec/experiments/ios_specific_templates_experiment_spec.rb b/spec/experiments/ios_specific_templates_experiment_spec.rb
new file mode 100644
index 00000000000..4d02381dbde
--- /dev/null
+++ b/spec/experiments/ios_specific_templates_experiment_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe IosSpecificTemplatesExperiment do
+ subject do
+ described_class.new(actor: user, project: project) do |e|
+ e.candidate { true }
+ end.run
+ end
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :auto_devops_disabled) }
+
+ let!(:project_setting) { create(:project_setting, project: project, target_platforms: target_platforms) }
+ let(:target_platforms) { %w(ios) }
+
+ before do
+ stub_experiments(ios_specific_templates: :candidate)
+ project.add_developer(user) if user
+ end
+
+ it { is_expected.to be true }
+
+ describe 'skipping the experiment' do
+ context 'no actor' do
+ let_it_be(:user) { nil }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'actor cannot create pipelines' do
+ before do
+ project.add_guest(user)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'targeting a non iOS platform' do
+ let(:target_platforms) { [] }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'project has a ci.yaml file' do
+ before do
+ allow(project).to receive(:has_ci?).and_return(true)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'project has pipelines' do
+ before do
+ create(:ci_pipeline, project: project)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+end
diff --git a/spec/features/admin/admin_users_impersonation_tokens_spec.rb b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
index 6643ebe82e6..15bc2318022 100644
--- a/spec/features/admin/admin_users_impersonation_tokens_spec.rb
+++ b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
@@ -36,14 +36,14 @@ RSpec.describe 'Admin > Users > Impersonation Tokens', :js do
click_on "1"
# Scopes
- check "api"
+ check "read_api"
check "read_user"
click_on "Create impersonation token"
expect(active_impersonation_tokens).to have_text(name)
expect(active_impersonation_tokens).to have_text('in')
- expect(active_impersonation_tokens).to have_text('api')
+ expect(active_impersonation_tokens).to have_text('read_api')
expect(active_impersonation_tokens).to have_text('read_user')
expect(PersonalAccessTokensFinder.new(impersonation: true).execute.count).to equal(1)
expect(created_impersonation_token).not_to be_empty
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
index f1e5658cd7b..8cbc0491441 100644
--- a/spec/features/profiles/personal_access_tokens_spec.rb
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -47,14 +47,14 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
click_on "1"
# Scopes
- check "api"
+ check "read_api"
check "read_user"
click_on "Create personal access token"
expect(active_personal_access_tokens).to have_text(name)
expect(active_personal_access_tokens).to have_text('in')
- expect(active_personal_access_tokens).to have_text('api')
+ expect(active_personal_access_tokens).to have_text('read_api')
expect(active_personal_access_tokens).to have_text('read_user')
expect(created_personal_access_token).not_to be_empty
end
diff --git a/spec/finders/users_finder_spec.rb b/spec/finders/users_finder_spec.rb
index bf40aea4a5d..271dce44db7 100644
--- a/spec/finders/users_finder_spec.rb
+++ b/spec/finders/users_finder_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe UsersFinder do
it 'returns searchable users' do
users = described_class.new(user).execute
- expect(users).to contain_exactly(user, normal_user, external_user, omniauth_user, internal_user, admin_user, project_bot)
+ expect(users).to contain_exactly(user, normal_user, external_user, unconfirmed_user, omniauth_user, internal_user, admin_user, project_bot)
end
it 'filters by username' do
@@ -56,7 +56,7 @@ RSpec.describe UsersFinder do
it 'filters by non external users' do
users = described_class.new(user, non_external: true).execute
- expect(users).to contain_exactly(user, normal_user, omniauth_user, internal_user, admin_user, project_bot)
+ expect(users).to contain_exactly(user, normal_user, unconfirmed_user, omniauth_user, internal_user, admin_user, project_bot)
end
it 'filters by created_at' do
@@ -73,7 +73,7 @@ RSpec.describe UsersFinder do
it 'filters by non internal users' do
users = described_class.new(user, non_internal: true).execute
- expect(users).to contain_exactly(user, normal_user, external_user, omniauth_user, admin_user, project_bot)
+ expect(users).to contain_exactly(user, normal_user, unconfirmed_user, external_user, omniauth_user, admin_user, project_bot)
end
it 'does not filter by custom attributes' do
@@ -82,18 +82,18 @@ RSpec.describe UsersFinder do
custom_attributes: { foo: 'bar' }
).execute
- expect(users).to contain_exactly(user, normal_user, external_user, omniauth_user, internal_user, admin_user, project_bot)
+ expect(users).to contain_exactly(user, normal_user, external_user, unconfirmed_user, omniauth_user, internal_user, admin_user, project_bot)
end
it 'orders returned results' do
users = described_class.new(user, sort: 'id_asc').execute
- expect(users).to eq([normal_user, admin_user, external_user, omniauth_user, internal_user, project_bot, user])
+ expect(users).to eq([normal_user, admin_user, external_user, unconfirmed_user, omniauth_user, internal_user, project_bot, user])
end
it 'does not filter by admins' do
users = described_class.new(user, admins: true).execute
- expect(users).to contain_exactly(user, normal_user, external_user, admin_user, omniauth_user, internal_user, project_bot)
+ expect(users).to contain_exactly(user, normal_user, external_user, admin_user, unconfirmed_user, omniauth_user, internal_user, project_bot)
end
end
diff --git a/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js b/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js
index b6be53fec99..7952661e2d2 100644
--- a/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js
+++ b/spec/frontend/header_search/components/header_search_autocomplete_items_spec.js
@@ -8,6 +8,9 @@ import {
LARGE_AVATAR_PX,
PROJECTS_CATEGORY,
SMALL_AVATAR_PX,
+ ISSUES_CATEGORY,
+ MERGE_REQUEST_CATEGORY,
+ RECENT_EPICS_CATEGORY,
} from '~/header_search/constants';
import {
MOCK_GROUPED_AUTOCOMPLETE_OPTIONS,
@@ -50,7 +53,12 @@ describe('HeaderSearchAutocompleteItems', () => {
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findGlDropdownDividers = () => wrapper.findAllComponents(GlDropdownDivider);
const findFirstDropdownItem = () => findDropdownItems().at(0);
- const findDropdownItemTitles = () => findDropdownItems().wrappers.map((w) => w.text());
+ const findDropdownItemTitles = () =>
+ findDropdownItems().wrappers.map((w) => w.findAll('span').at(1).text());
+ const findDropdownItemSubTitles = () =>
+ findDropdownItems()
+ .wrappers.filter((w) => w.findAll('span').length > 2)
+ .map((w) => w.findAll('span').at(2).text());
const findDropdownItemLinks = () => findDropdownItems().wrappers.map((w) => w.attributes('href'));
const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findGlAvatar = () => wrapper.findComponent(GlAvatar);
@@ -95,10 +103,17 @@ describe('HeaderSearchAutocompleteItems', () => {
});
it('renders titles correctly', () => {
- const expectedTitles = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.label);
+ const expectedTitles = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.value || o.label);
expect(findDropdownItemTitles()).toStrictEqual(expectedTitles);
});
+ it('renders sub-titles correctly', () => {
+ const expectedSubTitles = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.filter((o) => o.value).map(
+ (o) => o.label,
+ );
+ expect(findDropdownItemSubTitles()).toStrictEqual(expectedSubTitles);
+ });
+
it('renders links correctly', () => {
const expectedLinks = MOCK_SORTED_AUTOCOMPLETE_OPTIONS.map((o) => o.url);
expect(findDropdownItemLinks()).toStrictEqual(expectedLinks);
@@ -106,15 +121,30 @@ describe('HeaderSearchAutocompleteItems', () => {
});
describe.each`
- item | showAvatar | avatarSize
- ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)}
- ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: '/123' }] }} | ${true} | ${String(LARGE_AVATAR_PX)}
- ${{ data: [{ category: 'Help', avatar_url: '' }] }} | ${true} | ${String(SMALL_AVATAR_PX)}
- ${{ data: [{ category: 'Settings' }] }} | ${false} | ${false}
- `('GlAvatar', ({ item, showAvatar, avatarSize }) => {
+ item | showAvatar | avatarSize | searchContext | entityId | entityName
+ ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ project: { id: 29 } }} | ${'29'} | ${''}
+ ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: '/123' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ group: { id: 12 } }} | ${'12'} | ${''}
+ ${{ data: [{ category: 'Help', avatar_url: '' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'0'} | ${''}
+ ${{ data: [{ category: 'Settings' }] }} | ${false} | ${false} | ${null} | ${false} | ${false}
+ ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ group: { id: 1, name: 'test1' } }} | ${'1'} | ${'test1'}
+ ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ project: { id: 2, name: 'test2' } }} | ${'2'} | ${'test2'}
+ ${{ data: [{ category: ISSUES_CATEGORY, avatar_url: null }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 3, name: 'test3' } }} | ${'3'} | ${'test3'}
+ ${{ data: [{ category: MERGE_REQUEST_CATEGORY, avatar_url: null }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 4, name: 'test4' } }} | ${'4'} | ${'test4'}
+ ${{ data: [{ category: RECENT_EPICS_CATEGORY, avatar_url: null }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ group: { id: 5, name: 'test5' } }} | ${'5'} | ${'test5'}
+ ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: null, group_id: 6, group_name: 'test6' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${null} | ${'6'} | ${'test6'}
+ ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null, project_id: 7, project_name: 'test7' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${null} | ${'7'} | ${'test7'}
+ ${{ data: [{ category: ISSUES_CATEGORY, avatar_url: null, project_id: 8, project_name: 'test8' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'8'} | ${'test8'}
+ ${{ data: [{ category: MERGE_REQUEST_CATEGORY, avatar_url: null, project_id: 9, project_name: 'test9' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'9'} | ${'test9'}
+ ${{ data: [{ category: RECENT_EPICS_CATEGORY, avatar_url: null, group_id: 10, group_name: 'test10' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${null} | ${'10'} | ${'test10'}
+ ${{ data: [{ category: GROUPS_CATEGORY, avatar_url: null, group_id: 11, group_name: 'test11' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ group: { id: 1, name: 'test1' } }} | ${'11'} | ${'test11'}
+ ${{ data: [{ category: PROJECTS_CATEGORY, avatar_url: null, project_id: 12, project_name: 'test12' }] }} | ${true} | ${String(LARGE_AVATAR_PX)} | ${{ project: { id: 2, name: 'test2' } }} | ${'12'} | ${'test12'}
+ ${{ data: [{ category: ISSUES_CATEGORY, avatar_url: null, project_id: 13, project_name: 'test13' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 3, name: 'test3' } }} | ${'13'} | ${'test13'}
+ ${{ data: [{ category: MERGE_REQUEST_CATEGORY, avatar_url: null, project_id: 14, project_name: 'test14' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ project: { id: 4, name: 'test4' } }} | ${'14'} | ${'test14'}
+ ${{ data: [{ category: RECENT_EPICS_CATEGORY, avatar_url: null, group_id: 15, group_name: 'test15' }] }} | ${true} | ${String(SMALL_AVATAR_PX)} | ${{ group: { id: 5, name: 'test5' } }} | ${'15'} | ${'test15'}
+ `('GlAvatar', ({ item, showAvatar, avatarSize, searchContext, entityId, entityName }) => {
describe(`when category is ${item.data[0].category} and avatar_url is ${item.data[0].avatar_url}`, () => {
beforeEach(() => {
- createComponent({}, { autocompleteGroupedSearchOptions: () => [item] });
+ createComponent({ searchContext }, { autocompleteGroupedSearchOptions: () => [item] });
});
it(`should${showAvatar ? '' : ' not'} render`, () => {
@@ -124,6 +154,16 @@ describe('HeaderSearchAutocompleteItems', () => {
it(`should set avatarSize to ${avatarSize}`, () => {
expect(findGlAvatar().exists() && findGlAvatar().attributes('size')).toBe(avatarSize);
});
+
+ it(`should set avatar entityId to ${entityId}`, () => {
+ expect(findGlAvatar().exists() && findGlAvatar().attributes('entityid')).toBe(entityId);
+ });
+
+ it(`should set avatar entityName to ${entityName}`, () => {
+ expect(findGlAvatar().exists() && findGlAvatar().attributes('entityname')).toBe(
+ entityName,
+ );
+ });
});
});
});
diff --git a/spec/frontend/header_search/mock_data.js b/spec/frontend/header_search/mock_data.js
index 358c224dfa6..b6f0fdcc29d 100644
--- a/spec/frontend/header_search/mock_data.js
+++ b/spec/frontend/header_search/mock_data.js
@@ -96,19 +96,22 @@ export const MOCK_AUTOCOMPLETE_OPTIONS_RES = [
{
category: 'Projects',
id: 1,
- label: 'MockProject1',
+ label: 'Gitlab Org / MockProject1',
+ value: 'MockProject1',
url: 'project/1',
},
{
category: 'Groups',
id: 1,
- label: 'MockGroup1',
+ label: 'Gitlab Org / MockGroup1',
+ value: 'MockGroup1',
url: 'group/1',
},
{
category: 'Projects',
id: 2,
- label: 'MockProject2',
+ label: 'Gitlab Org / MockProject2',
+ value: 'MockProject2',
url: 'project/2',
},
{
@@ -123,21 +126,24 @@ export const MOCK_AUTOCOMPLETE_OPTIONS = [
category: 'Projects',
html_id: 'autocomplete-Projects-0',
id: 1,
- label: 'MockProject1',
+ label: 'Gitlab Org / MockProject1',
+ value: 'MockProject1',
url: 'project/1',
},
{
category: 'Groups',
html_id: 'autocomplete-Groups-1',
id: 1,
- label: 'MockGroup1',
+ label: 'Gitlab Org / MockGroup1',
+ value: 'MockGroup1',
url: 'group/1',
},
{
category: 'Projects',
html_id: 'autocomplete-Projects-2',
id: 2,
- label: 'MockProject2',
+ label: 'Gitlab Org / MockProject2',
+ value: 'MockProject2',
url: 'project/2',
},
{
@@ -157,7 +163,8 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
html_id: 'autocomplete-Projects-0',
id: 1,
- label: 'MockProject1',
+ label: 'Gitlab Org / MockProject1',
+ value: 'MockProject1',
url: 'project/1',
},
{
@@ -165,7 +172,8 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
html_id: 'autocomplete-Projects-2',
id: 2,
- label: 'MockProject2',
+ label: 'Gitlab Org / MockProject2',
+ value: 'MockProject2',
url: 'project/2',
},
],
@@ -178,7 +186,8 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS = [
html_id: 'autocomplete-Groups-1',
id: 1,
- label: 'MockGroup1',
+ label: 'Gitlab Org / MockGroup1',
+ value: 'MockGroup1',
url: 'group/1',
},
],
@@ -202,21 +211,24 @@ export const MOCK_SORTED_AUTOCOMPLETE_OPTIONS = [
category: 'Projects',
html_id: 'autocomplete-Projects-0',
id: 1,
- label: 'MockProject1',
+ label: 'Gitlab Org / MockProject1',
+ value: 'MockProject1',
url: 'project/1',
},
{
category: 'Projects',
html_id: 'autocomplete-Projects-2',
id: 2,
- label: 'MockProject2',
+ label: 'Gitlab Org / MockProject2',
+ value: 'MockProject2',
url: 'project/2',
},
{
category: 'Groups',
html_id: 'autocomplete-Groups-1',
id: 1,
- label: 'MockGroup1',
+ label: 'Gitlab Org / MockGroup1',
+ value: 'MockGroup1',
url: 'group/1',
},
{
diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js
index b0d5859cd31..3d7bf7acb41 100644
--- a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_item_spec.js
@@ -72,7 +72,7 @@ describe('GroupsListItem', () => {
expect(addSubscriptionSpy).toHaveBeenCalledWith(mockSubscriptionPath, mockGroup1.full_path);
expect(persistAlert).toHaveBeenCalledWith({
- linkUrl: '/help/integration/jira_development_panel.html#usage',
+ linkUrl: '/help/integration/jira_development_panel.html#use-the-integration',
message:
'You should now see GitLab.com activity inside your Jira Cloud issues. %{linkStart}Learn more%{linkEnd}',
title: 'Namespace successfully linked',
diff --git a/spec/helpers/ci/pipelines_helper_spec.rb b/spec/helpers/ci/pipelines_helper_spec.rb
index 2b76eaa87bc..c473e1e4ab6 100644
--- a/spec/helpers/ci/pipelines_helper_spec.rb
+++ b/spec/helpers/ci/pipelines_helper_spec.rb
@@ -151,5 +151,46 @@ RSpec.describe Ci::PipelinesHelper do
end
end
end
+
+ describe 'the `registration_token` attribute' do
+ subject { data[:registration_token] }
+
+ describe 'when the project is eligible for the `ios_specific_templates` experiment' do
+ let_it_be(:project) { create(:project, :auto_devops_disabled) }
+ let_it_be(:user) { create(:user) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ project.add_developer(user)
+ create(:project_setting, project: project, target_platforms: %w(ios))
+ end
+
+ context 'when the `ios_specific_templates` experiment variant is control' do
+ before do
+ stub_experiments(ios_specific_templates: :control)
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when the `ios_specific_templates` experiment variant is candidate' do
+ before do
+ stub_experiments(ios_specific_templates: :candidate)
+ end
+
+ context 'when the user cannot register project runners' do
+ before do
+ allow(helper).to receive(:can?).with(user, :register_project_runners, project).and_return(false)
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when the user can register project runners' do
+ it { is_expected.to eq(project.runners_token) }
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 96d8157dfde..d1be451a759 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -71,7 +71,7 @@ RSpec.describe SearchHelper do
create(:group).add_owner(user)
result = search_autocomplete_opts("gro").first
- expect(result.keys).to match_array(%i[category id label url avatar_url])
+ expect(result.keys).to match_array(%i[category id value label url avatar_url])
end
it 'includes the users recently viewed issues', :aggregate_failures do
diff --git a/spec/migrations/20220322132242_update_pages_onboarding_state_spec.rb b/spec/migrations/20220322132242_update_pages_onboarding_state_spec.rb
new file mode 100644
index 00000000000..fbd5fe546fa
--- /dev/null
+++ b/spec/migrations/20220322132242_update_pages_onboarding_state_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+require 'spec_helper'
+require_migration!
+
+RSpec.describe UpdatePagesOnboardingState do
+ let(:migration) { described_class.new }
+ let!(:namespaces) { table(:namespaces) }
+ let!(:projects) { table(:projects) }
+ let!(:project_pages_metadata) { table(:project_pages_metadata) }
+
+ let!(:namespace1) { namespaces.create!(name: 'foo', path: 'foo') }
+ let!(:namespace2) { namespaces.create!(name: 'bar', path: 'bar') }
+ let!(:project1) { projects.create!(namespace_id: namespace1.id) }
+ let!(:project2) { projects.create!(namespace_id: namespace2.id) }
+ let!(:pages_metadata1) do
+ project_pages_metadata.create!(
+ project_id: project1.id,
+ deployed: true,
+ onboarding_complete: false
+ )
+ end
+
+ let!(:pages_metadata2) do
+ project_pages_metadata.create!(
+ project_id: project2.id,
+ deployed: false,
+ onboarding_complete: false
+ )
+ end
+
+ describe '#up' do
+ before do
+ migration.up
+ end
+
+ it 'sets the onboarding_complete attribute to the value of deployed' do
+ expect(pages_metadata1.reload.onboarding_complete).to eq(true)
+ expect(pages_metadata2.reload.onboarding_complete).to eq(false)
+ end
+ end
+
+ describe '#down' do
+ before do
+ migration.up
+ migration.down
+ end
+
+ it 'sets all onboarding_complete attributes to false' do
+ expect(pages_metadata1.reload.onboarding_complete).to eq(false)
+ expect(pages_metadata2.reload.onboarding_complete).to eq(false)
+ end
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index ead7f7d0786..43088739f34 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -2293,6 +2293,44 @@ RSpec.describe Project, factory_default: :keep do
end
end
+ describe '#pages_show_onboarding?' do
+ let(:project) { create(:project) }
+
+ subject { project.pages_show_onboarding? }
+
+ context "if there is no metadata" do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'if onboarding is complete' do
+ before do
+ project.pages_metadatum.update_column(:onboarding_complete, true)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'if there is metadata, but onboarding is not complete' do
+ before do
+ project.pages_metadatum.update_column(:onboarding_complete, false)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ # During migration, the onboarding_complete property can still be false,
+ # but will be updated later. To account for that case, pages_show_onboarding?
+ # should return false if `deployed` is true.
+ context "will return false if pages is deployed even if onboarding_complete is false" do
+ before do
+ project.pages_metadatum.update_column(:onboarding_complete, false)
+ project.pages_metadatum.update_column(:deployed, true)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
describe '#pages_deployed?' do
let(:project) { create(:project) }
@@ -6626,6 +6664,25 @@ RSpec.describe Project, factory_default: :keep do
end
end
+ describe '#mark_pages_onboarding_complete' do
+ let(:project) { create(:project) }
+
+ it "creates new record and sets onboarding_complete to true if none exists yet" do
+ project.mark_pages_onboarding_complete
+
+ expect(project.pages_metadatum.reload.onboarding_complete).to eq(true)
+ end
+
+ it "overrides an existing setting" do
+ pages_metadatum = project.pages_metadatum
+ pages_metadatum.update!(onboarding_complete: false)
+
+ expect do
+ project.mark_pages_onboarding_complete
+ end.to change { pages_metadatum.reload.onboarding_complete }.from(false).to(true)
+ end
+ end
+
describe '#mark_pages_as_deployed' do
let(:project) { create(:project) }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 5f2777b12d2..09e3a9c1156 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -4988,17 +4988,36 @@ RSpec.describe User do
end
describe '#attention_requested_open_merge_requests_count' do
- it 'returns number of open merge requests from non-archived projects' do
- user = create(:user)
- project = create(:project, :public)
- archived_project = create(:project, :public, :archived)
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:archived_project) { create(:project, :public, :archived) }
+ before do
create(:merge_request, source_project: project, author: user, reviewers: [user])
create(:merge_request, :closed, source_project: project, author: user, reviewers: [user])
create(:merge_request, source_project: archived_project, author: user, reviewers: [user])
+ end
+ it 'returns number of open merge requests from non-archived projects' do
+ expect(Rails.cache).not_to receive(:fetch)
expect(user.attention_requested_open_merge_requests_count(force: true)).to eq 1
end
+
+ context 'when uncached_mr_attention_requests_count is disabled' do
+ before do
+ stub_feature_flags(uncached_mr_attention_requests_count: false)
+ end
+
+ it 'fetches from cache' do
+ expect(Rails.cache).to receive(:fetch).with(
+ user.attention_request_cache_key,
+ force: false,
+ expires_in: described_class::COUNT_CACHE_VALIDITY_PERIOD
+ ).and_call_original
+
+ expect(user.attention_requested_open_merge_requests_count).to eq 1
+ end
+ end
end
describe '#assigned_open_issues_count' do
@@ -6748,9 +6767,9 @@ RSpec.describe User do
let_it_be(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
let_it_be(:internal_user) { User.alert_bot.tap { |u| u.confirm } }
- it 'does not return blocked, banned or unconfirmed users' do
+ it 'does not return blocked or banned users' do
expect(described_class.without_forbidden_states).to match_array([
- normal_user, admin_user, external_user, omniauth_user, internal_user
+ normal_user, admin_user, external_user, unconfirmed_user, omniauth_user, internal_user
])
end
end
diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb
index 2bc31153f2c..07efd56fef4 100644
--- a/spec/requests/api/project_export_spec.rb
+++ b/spec/requests/api/project_export_spec.rb
@@ -260,6 +260,29 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache do
expect(json_response['message']['error']).to eq('This endpoint has been requested too many times. Try again later.')
end
end
+
+ context 'applies correct scope when throttling' do
+ before do
+ stub_application_setting(project_download_export_limit: 1)
+ end
+
+ it 'throttles downloads within same namespaces' do
+ # simulate prior request to the same namespace, which increments the rate limit counter for that scope
+ Gitlab::ApplicationRateLimiter.throttled?(:project_download_export, scope: [user, project_finished.namespace])
+
+ get api(download_path_finished, user)
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+ end
+
+ it 'allows downloads from different namespaces' do
+ # simulate prior request to a different namespace, which increments the rate limit counter for that scope
+ Gitlab::ApplicationRateLimiter.throttled?(:project_download_export,
+ scope: [user, create(:project, :with_export).namespace])
+
+ get api(download_path_finished, user)
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
end
context 'when user is a maintainer' do
diff --git a/spec/support/shared_examples/features/access_tokens_shared_examples.rb b/spec/support/shared_examples/features/access_tokens_shared_examples.rb
index ae246a87bb6..215d9d3e5a8 100644
--- a/spec/support/shared_examples/features/access_tokens_shared_examples.rb
+++ b/spec/support/shared_examples/features/access_tokens_shared_examples.rb
@@ -29,15 +29,15 @@ RSpec.shared_examples 'resource access tokens creation' do |resource_type|
click_on '1'
# Scopes
- check 'api'
check 'read_api'
+ check 'read_repository'
click_on "Create #{resource_type} access token"
expect(active_resource_access_tokens).to have_text(name)
expect(active_resource_access_tokens).to have_text('in')
- expect(active_resource_access_tokens).to have_text('api')
expect(active_resource_access_tokens).to have_text('read_api')
+ expect(active_resource_access_tokens).to have_text('read_repository')
expect(active_resource_access_tokens).to have_text('Maintainer')
expect(created_resource_access_token).not_to be_empty
end