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:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-03-11 15:07:56 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-03-11 15:07:56 +0300
commite105f6b881d9341331558c8c42f90391bab2fd19 (patch)
tree8b3dcfcd9b57f94bd1d8488c7728e6894a897829
parent27ad9b4c89456548d3eaa380a8ab7cc7380f34c3 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/environments/components/new_environment_folder.vue9
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue15
-rw-r--r--app/assets/javascripts/persistent_user_callouts.js1
-rw-r--r--app/graphql/resolvers/group_members/notification_email_resolver.rb29
-rw-r--r--app/graphql/types/group_member_type.rb4
-rw-r--r--app/helpers/pagination_helper.rb4
-rw-r--r--app/helpers/storage_helper.rb17
-rw-r--r--app/models/users/callout.rb6
-rw-r--r--app/models/users/group_callout.rb8
-rw-r--r--app/views/layouts/group.html.haml3
-rw-r--r--app/views/layouts/header/_storage_enforcement_banner.html.haml9
-rw-r--r--app/views/layouts/profile.html.haml4
-rw-r--r--data/deprecations/14-9-deprecate-testcoveragesetting.yml2
-rw-r--r--doc/.vale/gitlab/Uppercase.yml3
-rw-r--r--doc/.vale/gitlab/spelling-exceptions.txt2
-rw-r--r--doc/api/graphql/reference/index.md5
-rw-r--r--doc/ci/unit_test_reports.md5
-rw-r--r--doc/development/fe_guide/vue3_migration.md8
-rw-r--r--doc/development/index.md1
-rw-r--r--doc/development/integrations/index.md332
-rw-r--r--doc/update/deprecations.md2
-rw-r--r--doc/user/admin_area/settings/continuous_integration.md23
-rw-r--r--doc/user/gitlab_com/index.md28
-rw-r--r--locale/gitlab.pot2
-rw-r--r--qa/Gemfile1
-rw-r--r--qa/Gemfile.lock15
-rw-r--r--qa/Rakefile23
-rw-r--r--qa/qa/runtime/env.rb5
-rw-r--r--qa/qa/tools/delete_test_resources.rb106
-rw-r--r--qa/qa/tools/test_resource_data_processor.rb6
-rw-r--r--qa/qa/tools/test_resources_handler.rb182
-rw-r--r--qa/spec/runtime/env_spec.rb32
-rw-r--r--qa/spec/spec_helper.rb2
-rw-r--r--qa/spec/tools/test_resources_data_processor_spec.rb28
-rw-r--r--spec/features/groups_spec.rb65
-rw-r--r--spec/features/profiles/user_visits_profile_spec.rb51
-rw-r--r--spec/frontend/incidents/components/incidents_list_spec.js88
-rw-r--r--spec/graphql/mutations/boards/issues/issue_move_list_spec.rb18
-rw-r--r--spec/graphql/mutations/ci/runner/delete_spec.rb6
-rw-r--r--spec/graphql/mutations/ci/runner/update_spec.rb6
-rw-r--r--spec/graphql/mutations/release_asset_links/create_spec.rb16
-rw-r--r--spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb6
-rw-r--r--spec/graphql/resolvers/group_members/notification_email_resolver_spec.rb46
-rw-r--r--spec/graphql/resolvers/group_milestones_resolver_spec.rb24
-rw-r--r--spec/graphql/types/group_member_type_spec.rb2
-rw-r--r--spec/helpers/storage_helper_spec.rb43
-rw-r--r--spec/requests/api/graphql/group/group_members_spec.rb46
-rw-r--r--spec/support/helpers/graphql_helpers.rb6
-rw-r--r--spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb18
49 files changed, 1054 insertions, 309 deletions
diff --git a/app/assets/javascripts/environments/components/new_environment_folder.vue b/app/assets/javascripts/environments/components/new_environment_folder.vue
index 0d3867a4d74..510c194f15f 100644
--- a/app/assets/javascripts/environments/components/new_environment_folder.vue
+++ b/app/assets/javascripts/environments/components/new_environment_folder.vue
@@ -1,6 +1,7 @@
<script>
import { GlButton, GlCollapse, GlIcon, GlBadge, GlLink } from '@gitlab/ui';
import { __, s__ } from '~/locale';
+import pollIntervalQuery from '../graphql/queries/poll_interval.query.graphql';
import folderQuery from '../graphql/queries/folder.query.graphql';
import EnvironmentItem from './new_environment_item.vue';
@@ -20,7 +21,7 @@ export default {
},
},
data() {
- return { visible: false };
+ return { visible: false, interval: undefined };
},
apollo: {
folder: {
@@ -28,6 +29,12 @@ export default {
variables() {
return { environment: this.nestedEnvironment.latest };
},
+ pollInterval() {
+ return this.interval;
+ },
+ },
+ interval: {
+ query: pollIntervalQuery,
},
},
i18n: {
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index 44ef5fe0c07..324797ad645 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -1,5 +1,6 @@
<script>
import {
+ GlLink,
GlLoadingIcon,
GlTable,
GlAvatarsInline,
@@ -106,6 +107,7 @@ export default {
],
MAX_VISIBLE_ASSIGNEES,
components: {
+ GlLink,
GlLoadingIcon,
GlTable,
GlAvatarsInline,
@@ -271,7 +273,7 @@ export default {
return Boolean(assignees.nodes?.length);
},
navigateToIncidentDetails({ iid }) {
- return visitUrl(joinPaths(this.issuePath, INCIDENT_DETAILS_PATH, iid));
+ return visitUrl(this.showIncidentLink({ iid }));
},
navigateToCreateNewIncident() {
const { category, action } = this.$options.trackIncidentCreateNewOptions;
@@ -297,6 +299,9 @@ export default {
getEscalationStatus(escalationStatus) {
return ESCALATION_STATUSES[escalationStatus] || this.$options.i18n.noEscalationStatus;
},
+ showIncidentLink({ iid }) {
+ return joinPaths(this.issuePath, INCIDENT_DETAILS_PATH, iid);
+ },
pageChanged(pagination) {
this.pagination = pagination;
},
@@ -384,12 +389,14 @@ export default {
<template #cell(title)="{ item }">
<div :class="{ 'gl-display-flex gl-align-items-center': item.state === 'closed' }">
- <tooltip-on-truncate
+ <gl-link
+ v-gl-tooltip
:title="item.title"
- class="gl-max-w-full gl-text-truncate gl-display-block"
+ data-testid="incident-link"
+ :href="showIncidentLink(item)"
>
{{ item.title }}
- </tooltip-on-truncate>
+ </gl-link>
<gl-icon
v-if="item.state === 'closed'"
name="issue-close"
diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js
index 337c204c36a..f6de21ec0c5 100644
--- a/app/assets/javascripts/persistent_user_callouts.js
+++ b/app/assets/javascripts/persistent_user_callouts.js
@@ -11,6 +11,7 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-eoa-bronze-plan-banner',
'.js-security-newsletter-callout',
'.js-approaching-seats-count-threshold',
+ '.js-storage-enforcement-banner',
];
const initCallouts = () => {
diff --git a/app/graphql/resolvers/group_members/notification_email_resolver.rb b/app/graphql/resolvers/group_members/notification_email_resolver.rb
new file mode 100644
index 00000000000..6cff4fbf531
--- /dev/null
+++ b/app/graphql/resolvers/group_members/notification_email_resolver.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module GroupMembers
+ class NotificationEmailResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type GraphQL::Types::String, null: true
+
+ def resolve
+ authorize!
+
+ BatchLoader::GraphQL.for(object.user_id).batch do |user_ids, loader|
+ User.find(user_ids).each do |user|
+ loader.call(user.id, user.notification_email_for(object.group))
+ end
+ end
+ end
+
+ def authorize!
+ raise_resource_not_available_error! unless user_is_admin?
+ end
+
+ def user_is_admin?
+ context[:current_user].present? && context[:current_user].can_admin_all_resources?
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/group_member_type.rb b/app/graphql/types/group_member_type.rb
index d68abc11bba..18242f7b8b1 100644
--- a/app/graphql/types/group_member_type.rb
+++ b/app/graphql/types/group_member_type.rb
@@ -13,6 +13,10 @@ module Types
field :group, Types::GroupType, null: true,
description: 'Group that a User is a member of.'
+ field :notification_email,
+ resolver: Resolvers::GroupMembers::NotificationEmailResolver,
+ description: "Group notification email for User. Only availble for admins."
+
def group
Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, object.source_id).find
end
diff --git a/app/helpers/pagination_helper.rb b/app/helpers/pagination_helper.rb
index 3167142e193..88bf09f0c03 100644
--- a/app/helpers/pagination_helper.rb
+++ b/app/helpers/pagination_helper.rb
@@ -22,4 +22,8 @@ module PaginationHelper
def paginate_with_count(collection, remote: nil, total_pages: nil)
paginate(collection, remote: remote, theme: 'gitlab', total_pages: total_pages)
end
+
+ def page_size
+ Kaminari.config.default_per_page
+ end
end
diff --git a/app/helpers/storage_helper.rb b/app/helpers/storage_helper.rb
index 34ba66db444..a075ccc38f5 100644
--- a/app/helpers/storage_helper.rb
+++ b/app/helpers/storage_helper.rb
@@ -25,16 +25,17 @@ module StorageHelper
end
def storage_enforcement_banner_info(namespace)
+ return unless can?(current_user, :admin_namespace, namespace)
return if namespace.paid?
return unless namespace.storage_enforcement_date && namespace.storage_enforcement_date >= Date.today
return if user_dismissed_storage_enforcement_banner?(namespace)
{
text: html_escape_once(s_("UsageQuota|From %{storage_enforcement_date} storage limits will apply to this namespace. " \
- "View and manage your usage in %{strong_start}Group Settings &gt; Usage quotas%{strong_end}.")).html_safe %
- { storage_enforcement_date: namespace.storage_enforcement_date, strong_start: "<strong>".html_safe, strong_end: "</strong>".html_safe },
+ "View and manage your usage in %{strong_start}%{namespace_type} settings &gt; Usage quotas%{strong_end}.")).html_safe %
+ { storage_enforcement_date: namespace.storage_enforcement_date, strong_start: "<strong>".html_safe, strong_end: "</strong>".html_safe, namespace_type: namespace.type },
variant: 'warning',
- callouts_path: group_callouts_path,
+ callouts_path: namespace.user_namespace? ? callouts_path : group_callouts_path,
callouts_feature_name: storage_enforcement_banner_user_callouts_feature_name(namespace),
learn_more_link: link_to(_('Learn more.'), help_page_path('/'), rel: 'noopener noreferrer', target: '_blank') # TBD: https://gitlab.com/gitlab-org/gitlab/-/issues/350632
}
@@ -52,13 +53,17 @@ module StorageHelper
return :first if days_to_enforcement_date > 30
return :second if days_to_enforcement_date > 15 && days_to_enforcement_date <= 30
return :third if days_to_enforcement_date > 7 && days_to_enforcement_date <= 15
- return :fourth if days_to_enforcement_date > 0 && days_to_enforcement_date <= 7
+ return :fourth if days_to_enforcement_date >= 0 && days_to_enforcement_date <= 7
end
def user_dismissed_storage_enforcement_banner?(namespace)
return false unless current_user
- current_user.dismissed_callout_for_group?(feature_name: storage_enforcement_banner_user_callouts_feature_name(namespace),
- group: namespace)
+ if namespace.user_namespace?
+ current_user.dismissed_callout?(feature_name: storage_enforcement_banner_user_callouts_feature_name(namespace))
+ else
+ current_user.dismissed_callout_for_group?(feature_name: storage_enforcement_banner_user_callouts_feature_name(namespace),
+ group: namespace)
+ end
end
end
diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb
index 5c39e29a128..800256a613b 100644
--- a/app/models/users/callout.rb
+++ b/app/models/users/callout.rb
@@ -42,7 +42,11 @@ module Users
security_newsletter_callout: 39,
verification_reminder: 40, # EE-only
ci_deprecation_warning_for_types_keyword: 41,
- security_training_feature_promotion: 42 # EE-only
+ security_training_feature_promotion: 42, # EE-only
+ storage_enforcement_banner_first_enforcement_threshold: 43,
+ storage_enforcement_banner_second_enforcement_threshold: 44,
+ storage_enforcement_banner_third_enforcement_threshold: 45,
+ storage_enforcement_banner_fourth_enforcement_threshold: 46
}
validates :feature_name,
diff --git a/app/models/users/group_callout.rb b/app/models/users/group_callout.rb
index 0dc449719ab..839be8d2a48 100644
--- a/app/models/users/group_callout.rb
+++ b/app/models/users/group_callout.rb
@@ -11,10 +11,10 @@ module Users
enum feature_name: {
invite_members_banner: 1,
approaching_seat_count_threshold: 2, # EE-only
- storage_enforcement_banner_first_enforcement_threshold: 43,
- storage_enforcement_banner_second_enforcement_threshold: 44,
- storage_enforcement_banner_third_enforcement_threshold: 45,
- storage_enforcement_banner_fourth_enforcement_threshold: 46
+ storage_enforcement_banner_first_enforcement_threshold: 3,
+ storage_enforcement_banner_second_enforcement_threshold: 4,
+ storage_enforcement_banner_third_enforcement_threshold: 5,
+ storage_enforcement_banner_fourth_enforcement_threshold: 6
}
validates :group, presence: true
diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml
index 58fed89dfe7..940724e0e4a 100644
--- a/app/views/layouts/group.html.haml
+++ b/app/views/layouts/group.html.haml
@@ -6,6 +6,9 @@
- display_namespace_storage_limit_alert!
- @left_sidebar = true
+- content_for :flash_message do
+ = render "layouts/header/storage_enforcement_banner", namespace: @group
+
- content_for :page_specific_javascripts do
- if current_user
= javascript_tag do
diff --git a/app/views/layouts/header/_storage_enforcement_banner.html.haml b/app/views/layouts/header/_storage_enforcement_banner.html.haml
new file mode 100644
index 00000000000..851fc57e44d
--- /dev/null
+++ b/app/views/layouts/header/_storage_enforcement_banner.html.haml
@@ -0,0 +1,9 @@
+- return unless current_user
+- namespace = local_assigns.fetch(:namespace)
+- banner_info = storage_enforcement_banner_info(namespace)
+- return unless banner_info.present?
+
+= render 'shared/global_alert', variant: :warning, alert_class: 'js-storage-enforcement-banner', alert_data: { feature_id: banner_info[:callouts_feature_name], dismiss_endpoint: banner_info[:callouts_path], group_id: namespace.id, defer_links: "true" } do
+ .gl-alert-body
+ = banner_info[:text]
+ = banner_info[:learn_more_link]
diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml
index 17153e72e6e..322a77116c8 100644
--- a/app/views/layouts/profile.html.haml
+++ b/app/views/layouts/profile.html.haml
@@ -5,4 +5,8 @@
- @left_sidebar = true
- enable_search_settings locals: { container_class: 'gl-my-5' }
+
+- content_for :flash_message do
+ = render "layouts/header/storage_enforcement_banner", namespace: current_user.namespace
+
= render template: "layouts/application"
diff --git a/data/deprecations/14-9-deprecate-testcoveragesetting.yml b/data/deprecations/14-9-deprecate-testcoveragesetting.yml
index 19bc6004b02..9015172eeb9 100644
--- a/data/deprecations/14-9-deprecate-testcoveragesetting.yml
+++ b/data/deprecations/14-9-deprecate-testcoveragesetting.yml
@@ -7,7 +7,7 @@
reporter: jreporter # GitLab username of the person reporting the deprecation
body: | # Do not modify this line, instead modify the lines below.
To simplify setting a test coverage pattern, in GitLab 15.0 the
- [project setting for test coverage parsing](https://docs.gitlab.com/ee/ci/pipelines/settings.html#add-test-coverage-results-to-a-merge-request-deprecated)
+ [project setting for test coverage parsing](https://docs.gitlab.com/ee/ci/pipelines/settings.html#add-test-coverage-results-using-project-settings-deprecated)
is being removed.
Instead, using the project’s `.gitlab-ci.yml`, provide a regular expression with the `coverage` keyword to set
diff --git a/doc/.vale/gitlab/Uppercase.yml b/doc/.vale/gitlab/Uppercase.yml
index c9021dc862e..f1b06e10fe6 100644
--- a/doc/.vale/gitlab/Uppercase.yml
+++ b/doc/.vale/gitlab/Uppercase.yml
@@ -51,6 +51,7 @@ exceptions:
- EKS
- ELB
- EOL
+ - EWM
- EXIF
- FAQ
- FIDO
@@ -144,6 +145,7 @@ exceptions:
- RSA
- RDS
- RSS
+ - RTC
- RVM
- SAAS
- SAML
@@ -171,6 +173,7 @@ exceptions:
- SSH
- SSL
- SSO
+ - STI
- SVG
- SVN
- TCP
diff --git a/doc/.vale/gitlab/spelling-exceptions.txt b/doc/.vale/gitlab/spelling-exceptions.txt
index 98254c2259b..6dc0704963b 100644
--- a/doc/.vale/gitlab/spelling-exceptions.txt
+++ b/doc/.vale/gitlab/spelling-exceptions.txt
@@ -229,6 +229,7 @@ Gemfile
Gemnasium
Gemojione
Getter
+getters
Getters
gettext
Git
@@ -576,6 +577,7 @@ sharded
sharding
shfmt
Shibboleth
+Shimo
Shopify
Sidekiq
Silverlight
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 1aef3cbc669..169d5dee615 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -11794,6 +11794,7 @@ Represents a Group Membership.
| <a id="groupmemberexpiresat"></a>`expiresAt` | [`Time`](#time) | Date and time the membership expires. |
| <a id="groupmembergroup"></a>`group` | [`Group`](#group) | Group that a User is a member of. |
| <a id="groupmemberid"></a>`id` | [`ID!`](#id) | ID of the member. |
+| <a id="groupmembernotificationemail"></a>`notificationEmail` | [`String`](#string) | Group notification email for User. Only availble for admins. |
| <a id="groupmemberupdatedat"></a>`updatedAt` | [`Time`](#time) | Date and time the membership was last updated. |
| <a id="groupmemberuser"></a>`user` | [`UserCore`](#usercore) | User that is associated with the member object. |
| <a id="groupmemberuserpermissions"></a>`userPermissions` | [`GroupPermissions!`](#grouppermissions) | Permissions for the current user on the resource. |
@@ -18534,6 +18535,10 @@ Name of the feature that the callout is for.
| <a id="usercalloutfeaturenameenumsecurity_configuration_upgrade_banner"></a>`SECURITY_CONFIGURATION_UPGRADE_BANNER` | Callout feature name for security_configuration_upgrade_banner. |
| <a id="usercalloutfeaturenameenumsecurity_newsletter_callout"></a>`SECURITY_NEWSLETTER_CALLOUT` | Callout feature name for security_newsletter_callout. |
| <a id="usercalloutfeaturenameenumsecurity_training_feature_promotion"></a>`SECURITY_TRAINING_FEATURE_PROMOTION` | Callout feature name for security_training_feature_promotion. |
+| <a id="usercalloutfeaturenameenumstorage_enforcement_banner_first_enforcement_threshold"></a>`STORAGE_ENFORCEMENT_BANNER_FIRST_ENFORCEMENT_THRESHOLD` | Callout feature name for storage_enforcement_banner_first_enforcement_threshold. |
+| <a id="usercalloutfeaturenameenumstorage_enforcement_banner_fourth_enforcement_threshold"></a>`STORAGE_ENFORCEMENT_BANNER_FOURTH_ENFORCEMENT_THRESHOLD` | Callout feature name for storage_enforcement_banner_fourth_enforcement_threshold. |
+| <a id="usercalloutfeaturenameenumstorage_enforcement_banner_second_enforcement_threshold"></a>`STORAGE_ENFORCEMENT_BANNER_SECOND_ENFORCEMENT_THRESHOLD` | Callout feature name for storage_enforcement_banner_second_enforcement_threshold. |
+| <a id="usercalloutfeaturenameenumstorage_enforcement_banner_third_enforcement_threshold"></a>`STORAGE_ENFORCEMENT_BANNER_THIRD_ENFORCEMENT_THRESHOLD` | Callout feature name for storage_enforcement_banner_third_enforcement_threshold. |
| <a id="usercalloutfeaturenameenumsuggest_pipeline"></a>`SUGGEST_PIPELINE` | Callout feature name for suggest_pipeline. |
| <a id="usercalloutfeaturenameenumsuggest_popover_dismissed"></a>`SUGGEST_POPOVER_DISMISSED` | Callout feature name for suggest_popover_dismissed. |
| <a id="usercalloutfeaturenameenumtabs_position_highlight"></a>`TABS_POSITION_HIGHLIGHT` | Callout feature name for tabs_position_highlight. |
diff --git a/doc/ci/unit_test_reports.md b/doc/ci/unit_test_reports.md
index 14a2a1c810f..14350ac884f 100644
--- a/doc/ci/unit_test_reports.md
+++ b/doc/ci/unit_test_reports.md
@@ -353,7 +353,7 @@ displays a list of test suites and cases reported from the XML file.
![Test Reports Widget](img/pipelines_junit_test_report_v13_10.png)
-You can view all the known test suites and click on each of these to see further
+You can view all the known test suites and select each of these to see further
details, including the cases that make up the suite.
You can also retrieve the reports via the [GitLab API](../api/pipelines.md#get-a-pipelines-test-report).
@@ -366,8 +366,7 @@ If parsing JUnit report XML results in an error, an indicator is shown next to t
![Test Reports With Errors](img/pipelines_junit_test_report_with_errors_v13_10.png)
-NOTE:
-GitLab.com has a 500,000 [test case parsing limit](../user/gitlab_com/#gitlab-cicd). Self-managed administrators can manage this setting on their instance.
+For test case parsing limits, see [Max test cases per unit test report](../user/gitlab_com/#gitlab-cicd).
GitLab does not parse very [large nodes](https://nokogiri.org/tutorials/parsing_an_html_xml_document.html#parse-options) of JUnit reports. There is [an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/268035) open to make this optional.
diff --git a/doc/development/fe_guide/vue3_migration.md b/doc/development/fe_guide/vue3_migration.md
index 6e994d5e95d..f174408c946 100644
--- a/doc/development/fe_guide/vue3_migration.md
+++ b/doc/development/fe_guide/vue3_migration.md
@@ -6,12 +6,10 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Migration to Vue 3
-Preparations for a Vue 3 migration are tracked in epic [&3174](https://gitlab.com/groups/gitlab-org/-/epics/3174)
+The migration from Vue 2 to 3 is tracked in epic [&6252](https://gitlab.com/groups/gitlab-org/-/epics/6252).
-In order to prepare for the eventual migration to Vue 3.x, we should not use the following deprecated features in the codebase:
-
-NOTE:
-Our linting rules block the use of these deprecated features.
+To ease migration to Vue 3.x, we have added [eslint rules](https://gitlab.com/gitlab-org/frontend/eslint-plugin/-/merge_requests/50)
+that prevent us from using the following deprecated features in the codebase.
## Vue filters
diff --git a/doc/development/index.md b/doc/development/index.md
index 8f13bd1accb..ff95240ce3a 100644
--- a/doc/development/index.md
+++ b/doc/development/index.md
@@ -275,6 +275,7 @@ See [database guidelines](database/index.md).
## Integration guides
+- [Integrations development guide](integrations/index.md)
- [Jira Connect app](integrations/jira_connect.md)
- [Security Scanners](integrations/secure.md)
- [Secure Partner Integration](integrations/secure_partner_integration.md)
diff --git a/doc/development/integrations/index.md b/doc/development/integrations/index.md
new file mode 100644
index 00000000000..34ac307c98a
--- /dev/null
+++ b/doc/development/integrations/index.md
@@ -0,0 +1,332 @@
+---
+stage: Ecosystem
+group: Integrations
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
+description: "GitLab's development guidelines for Integrations"
+---
+
+# Integrations development guide **(FREE)**
+
+This page provides development guidelines for implementing [GitLab integrations](../../user/project/integrations/index.md),
+which are part of our [main Rails project](https://gitlab.com/gitlab-org/gitlab).
+
+Also see our [direction page](https://about.gitlab.com/direction/ecosystem/integrations/) for an overview of our strategy around integrations.
+
+This guide is a work in progress. You're welcome to ping `@gitlab-org/ecosystem-stage/integrations`
+if you need clarification or spot any outdated information.
+
+## Add a new integration
+
+### Define the integration
+
+1. Add a new model in `app/models/integrations` extending from `Integration`.
+ - For example, `Integrations::FooBar` in `app/models/integrations/foo_bar.rb`.
+ - For certain types of integrations, you can also build on these base classes:
+ - `Integrations::BaseChatNotification`
+ - `Integrations::BaseIssueTracker`
+ - `Integrations::BaseMonitoring`
+ - `Integrations::BaseSlashCommands`
+ - For integrations that primarily trigger HTTP calls to external services, you can
+ also use the `Integrations::HasWebHook` concern. This reuses the [webhook functionality](../../user/project/integrations/webhooks.md)
+ in GitLab through an associated `ServiceHook` model, and automatically records request logs
+ which can be viewed in the integration settings.
+1. Add the integration's underscored name (`'foo_bar'`) to `Integration::INTEGRATION_NAMES`.
+1. Add the integration as an association on `Project`:
+
+ ```ruby
+ has_one :foo_bar_integration, class_name: 'Integrations::FooBar'
+ ```
+
+1. TEMPORARY: Accommodate the current migration to [rename "services" to "integrations"](#rename-services-to-integrations):
+ - Add the integration's camel-cased name (`'FooBar'`) to `Gitlab::Integrations::StiType::NAMESPACED_INTEGRATIONS`.
+
+### Define properties
+
+Integrations can define arbitrary properties to store their configuration with the class method `Integration.prop_accessor`.
+The values are stored as a serialized JSON hash in the `integrations.properties` column.
+
+For example:
+
+```ruby
+module Integrations
+ class FooBar < Integration
+ prop_accessor :url
+ prop_accessor :tags
+ end
+end
+```
+
+`Integration.prop_accessor` installs accessor methods on the class. Here we would have `#url`, `#url=` and `#url_changed?`, to manage the `url` field. Fields stored in `Integration#properties` should be accessed by these accessors directly on the model, just like other ActiveRecord attributes.
+
+You should always access the properties through their getters, and not interact with the `properties` hash directly.
+You **must not** write to the `properties` hash, you **must** use the generated setter method instead. Direct writes to this
+hash are not persisted.
+
+You should also define validations for all your properties.
+
+Also refer to the section [Customize the frontend form](#customize-the-frontend-form) below to see how these properties
+are exposed in the frontend form for the integration.
+
+There is an alternative approach using `Integration.data_field`, which you may see in other integrations.
+With data fields the values are stored in a separate table per integration. At the moment we don't recommend using this for new integrations.
+
+### Define trigger events
+
+Integrations are triggered by calling their `#execute` method in response to events in GitLab,
+which gets passed a payload hash with details about the event.
+
+The supported events have some overlap with [webhook events](../../user/project/integrations/webhook_events.md),
+and receive the same payload. You can specify the events you're interested in by overriding
+the class method `Integration.supported_events` in your model.
+
+The following events are supported for integrations:
+
+| Event type | Default | Value | Trigger
+|:-----------------------------------------------------------------------------------------------|:--------|:---------------------|:--
+| Alert event | | `alert` | A a new, unique alert is recorded.
+| Commit event | ✓ | `commit` | A commit is created or updated.
+| [Deployment event](../../user/project/integrations/webhook_events.md#deployment-events) | | `deployment` | A deployment starts or finishes.
+| [Issue event](../../user/project/integrations/webhook_events.md#issue-events) | ✓ | `issue` | An issue is created, updated, or closed.
+| [Confidential issue event](../../user/project/integrations/webhook_events.md#issue-events) | ✓ | `confidential_issue` | A confidential issue is created, updated, or closed.
+| [Job event](../../user/project/integrations/webhook_events.md#job-events) | | `job`
+| [Merge request event](../../user/project/integrations/webhook_events.md#merge-request-events) | ✓ | `merge_request` | A merge request is created, updated, or merged.
+| [Comment event](../../user/project/integrations/webhook_events.md#comment-events) | | `comment` | A new comment is added.
+| [Confidential comment event](../../user/project/integrations/webhook_events.md#comment-events) | | `confidential_note` | A new comment on a confidential issue is added.
+| [Pipeline event](../../user/project/integrations/webhook_events.md#pipeline-events) | | `pipeline` | A pipeline status changes.
+| [Push event](../../user/project/integrations/webhook_events.md#push-events) | ✓ | `push` | A push is made to the repository.
+| [Tag push event](../../user/project/integrations/webhook_events.md#tag-events) | ✓ | `tag_push` | New tags are pushed to the repository.
+| Vulnerability event **(ULTIMATE)** | | `vulnerability` | A new, unique vulnerability is recorded.
+| [Wiki page event](../../user/project/integrations/webhook_events.md#wiki-page-events) | ✓ | `wiki_page` | A wiki page is created or updated.
+
+#### Event examples
+
+This example defines an integration that responds to `commit` and `merge_request` events:
+
+```ruby
+module Integrations
+ class FooBar < Integration
+ def self.supported_events
+ %w[commit merge_request]
+ end
+ end
+end
+```
+
+An integration can also not respond to events, and implement custom functionality some other way:
+
+```ruby
+module Integrations
+ class FooBar < Integration
+ def self.supported_events
+ []
+ end
+ end
+end
+```
+
+### Customize the frontend form
+
+The frontend form is generated dynamically based on metadata defined in the model.
+
+By default, the integration form provides:
+
+- A checkbox to enable or disable the integration.
+- Checkboxes for each of the trigger events returned from `Integration#configurable_events`.
+
+You can also add help text at the top of the form by either overriding `Integration#help`,
+or providing a template in `app/views/projects/services/$INTEGRATION_NAME/_help.html.haml`.
+
+To add your custom properties to the form, you can define the metadata for them in `Integration#fields`.
+
+This method should return an array of hashes for each field, where the keys can be:
+
+| Key | Type | Required | Default | Description
+|:---------------|:--------|:---------|:-----------------------------|:--
+| `type:` | string | true | | The type of the form field. Can be `text`, `textarea`, `password`, `checkbox`, or `select`.
+| `name:` | string | true | | The property name for the form field. This must match a `prop_accessor` [defined on the class](#define-properties).
+| `required:` | boolean | false | `false` | Specify if the form field is required or optional.
+| `title:` | string | false | Capitalized value of `name:` | The label for the form field.
+| `placeholder:` | string | false | | A placeholder for the form field.
+| `help:` | string | false | | A help text that displays below the form field.
+
+#### Additional keys for `type: 'checkbox'`
+
+| Key | Type | Required | Default | Description
+|:------------------|:-------|:---------|:------------------|:--
+| `checkbox_label:` | string | false | Value of `title:` | A custom label that displays next to the checkbox.
+
+#### Additional keys for `type: 'select'`
+
+| Key | Type | Required | Default | Description
+|:-----------|:------|:---------|:--------|:--
+| `choices:` | array | true | | A nested array of `[label, value]` tuples.
+
+#### Additional keys for `type: 'password'`
+
+| Key | Type | Required | Default | Description
+|:----------------------------|:-------|:---------|:------------------|:--
+| `non_empty_password_title:` | string | false | Value of `title:` | An alternative label that displays when a value is already stored.
+| `non_empty_password_help:` | string | false | Value of `help:` | An alternative help text that displays when a value is already stored.
+
+#### Frontend form examples
+
+This example defines a required `url` field, and optional `username` and `password` fields:
+
+```ruby
+module Integrations
+ class FooBar < Integration
+ prop_accessor :url, :username, :password
+
+ def fields
+ [
+ {
+ type: 'text',
+ name: 'url',
+ title: s_('FooBarIntegration|Server URL'),
+ placeholder: 'https://example.com/',
+ required: true
+ },
+ {
+ type: 'text',
+ name: 'username',
+ title: s_('FooBarIntegration|Username'),
+ },
+ {
+ type: 'password',
+ name: 'password',
+ title: s_('FoobarIntegration|Password'
+ non_empty_password_title: s_('FooBarIntegration|Enter new password')
+ }
+ ]
+ end
+ end
+end
+```
+
+### Expose the integration in the API
+
+#### REST API
+
+To expose the integration in the [REST API](../../api/integrations.md):
+
+1. Add the integration's class (`::Integrations::FooBar`) to `API::Helpers::IntegrationsHelpers.integration_classes`.
+1. Add all properties that should be exposed to `API::Helpers::IntegrationsHelpers.integrations`.
+1. Update the reference documentation in `doc/api/integrations.md`, add a new section for your integration, and document all properties.
+
+You can also refer to our [REST API style guide](../api_styleguide.md).
+
+#### GraphQL API
+
+Integrations use the `Types::Projects::ServiceType` type by default,
+which only exposes the `type` and `active` properties.
+
+To expose additional properties, you can write a class implementing `ServiceType`:
+
+```ruby
+# in app/graphql/types/project/services/foo_bar_service_type.rb
+module Types
+ module Projects
+ module Services
+ class FooBarServiceType < BaseObject
+ graphql_name 'FooBarService'
+ implements(Types::Projects::ServiceType)
+ authorize :read_project
+
+ field :frobinity,
+ GraphQL::Types::Float,
+ null: true,
+ description: 'The level of frobinity.'
+
+ field :foo_label,
+ GraphQL::Types::String,
+ null: true,
+ description: 'The foo label to apply.'
+ end
+ end
+ end
+end
+```
+
+Each property you want to expose should have a field defined for it. You can also expose any public instance method of the integration.
+
+Contact a member of the Integrations team to discuss the best authorization.
+
+Reference documentation for GraphQL is automatically generated.
+
+You can also refer to our [GraphQL API style guide](../api_graphql_styleguide.md).
+
+## Availability of integrations
+
+By default, integrations are available on the project, group, and instance level.
+Most integrations only act in a project context, but can be still configured
+from the group and instance levels.
+
+For some integrations it can make sense to only make it available on the project level.
+To do that, the integration must be removed from `Integration::INTEGRATION_NAMES` and
+added to `Integration::PROJECT_SPECIFIC_INTEGRATION_NAMES` instead.
+
+When developing a new integration, we also recommend you gate the availability behind a
+[feature flag](../feature_flags/index.md) in `Integration.available_integration_names`.
+
+## Documentation
+
+You can provide help text in the integration form, including links to off-site documentation,
+as described above in [Customize the frontend form](#customize-the-frontend-form). Refer to
+our [usability guidelines](https://design.gitlab.com/usability/helping-users) for help text.
+
+For more detailed documentation, provide a page in `doc/user/project/integrations`,
+and link it from the [Integrations overview](../../user/project/integrations/overview.md).
+
+You can also refer to our general [documentation guidelines](../documentation/index.md).
+
+## Testing
+
+It is often sufficient to add tests for the integration model in `spec/models/integrations`,
+and a factory with example settings in `spec/factories/integrations.rb`.
+
+Each integration is also tested as part of generalized tests. For example, there are feature specs
+that verify that the settings form is rendering correctly for all integrations.
+
+If your integration implements any custom behavior, especially in the frontend, this should be
+covered by additional tests.
+
+You can also refer to our general [testing guidelines](../testing_guide/index.md).
+
+## Internationalization
+
+All UI strings should be prepared for translation by following our [internationalization guidelines](../i18n/externalization.md).
+
+The strings should use the integration name as [namespace](../i18n/externalization.md#namespaces), for example, `s_('FooBarIntegration|My string')`.
+
+## Ongoing migrations and refactorings
+
+The Integrations team is in the process of some larger migrations that developers should be aware of.
+
+### [Rename "services" to "integrations"](https://gitlab.com/groups/gitlab-org/-/epics/2504)
+
+The "integrations" in GitLab were historically called "services", which frequently caused
+confusion with our "service" classes in `app/services`. We sometimes also called
+them "project services" because they were initially only available on projects, which is
+not the case anymore.
+
+We decided to change the naming from "services" and "project services" to "integrations".
+This refactoring is an ongoing effort, and there are still references to the old names in some places.
+
+Developers should be especially aware that we still use the old class names for the STI column
+`integrations.type`. For example, a class `Integrations::FooBar` still stores
+the old name `FooBarService` in the database. This mapping is handled via `Gitlab::Integrations::StiType`
+and should be mostly transparent to the rest of the app.
+
+### [Consolidate integration settings](https://gitlab.com/groups/gitlab-org/-/epics/3955)
+
+We want to unify the way integration properties are defined.
+
+## Integration examples
+
+You can refer to these issues for examples of adding new integrations:
+
+- [Datadog](https://gitlab.com/gitlab-org/gitlab/-/issues/270123): Metrics collector, similar to the Prometheus integration.
+- [EWM/RTC](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36662): External issue tracker.
+- [Shimo](https://gitlab.com/gitlab-org/gitlab/-/issues/343386): External wiki, similar to the Confluence and External Wiki integrations.
+- [Webex Teams](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31543): Chat notifications.
+- [ZenTao](https://gitlab.com/gitlab-org/gitlab/-/issues/338178): External issue tracker with custom issue views, similar to the Jira integration.
diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md
index b5886b86754..6151955e87c 100644
--- a/doc/update/deprecations.md
+++ b/doc/update/deprecations.md
@@ -690,7 +690,7 @@ Before updating GitLab, review the details carefully to determine if you need to
changes to your code, settings, or workflow.
To simplify setting a test coverage pattern, in GitLab 15.0 the
-[project setting for test coverage parsing](https://docs.gitlab.com/ee/ci/pipelines/settings.html#add-test-coverage-results-to-a-merge-request-deprecated)
+[project setting for test coverage parsing](https://docs.gitlab.com/ee/ci/pipelines/settings.html#add-test-coverage-results-using-project-settings-deprecated)
is being removed.
Instead, using the project’s `.gitlab-ci.yml`, provide a regular expression with the `coverage` keyword to set
diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md
index 18379471bcf..0330d89aedc 100644
--- a/doc/user/admin_area/settings/continuous_integration.md
+++ b/doc/user/admin_area/settings/continuous_integration.md
@@ -55,31 +55,28 @@ can be set at:
- The instance level.
- [From GitLab 12.4](https://gitlab.com/gitlab-org/gitlab/-/issues/21688), the project and group level.
-The value is:
+For the setting on GitLab.com, see [Artifacts maximum size](../../gitlab_com/index.md#gitlab-cicd).
-- In *MB* and the default is 100MB per job.
-- [Set to 1G](../../gitlab_com/index.md#gitlab-cicd) on GitLab.com.
-
-To change it at the:
+The value is in MB and the default is 100MB per job. To change it at the:
- Instance level:
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Settings > CI/CD**.
1. Change the value of maximum artifacts size (in MB).
- 1. Click **Save changes** for the changes to take effect.
+ 1. Select **Save changes** for the changes to take effect.
- Group level (this overrides the instance setting):
1. Go to the group's **Settings > CI/CD > General Pipelines**.
1. Change the value of **maximum artifacts size (in MB)**.
- 1. Click **Save changes** for the changes to take effect.
+ 1. Select **Save changes** for the changes to take effect.
- Project level (this overrides the instance and group settings):
1. Go to the project's **Settings > CI/CD > General Pipelines**.
1. Change the value of **maximum artifacts size (in MB)**.
- 1. Click **Save changes** for the changes to take effect.
+ 1. Select **Save changes** for the changes to take effect.
NOTE:
The setting at all levels is only available to GitLab administrators.
@@ -94,7 +91,7 @@ and the default value is `30 days`.
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Settings > CI/CD**.
1. Change the value of default expiration time.
-1. Click **Save changes** for the changes to take effect.
+1. Select **Save changes** for the changes to take effect.
This setting is set per job and can be overridden in
[`.gitlab-ci.yml`](../../../ci/yaml/index.md#artifactsexpire_in).
@@ -126,7 +123,7 @@ To disable the setting:
1. On the left sidebar, select **Settings > CI/CD**.
1. Expand **Continuous Integration and Deployment**.
1. Clear the **Keep the latest artifacts for all jobs in the latest successful pipelines** checkbox.
-1. Click **Save changes**
+1. Select **Save changes**
When you disable the feature, the latest artifacts do not immediately expire.
A new pipeline must run before the latest artifacts can expire and be deleted.
@@ -156,7 +153,7 @@ After that time passes, the jobs are archived and no longer able to be
retried. Make it empty to never expire jobs. It has to be no less than 1 day,
for example: <code>15 days</code>, <code>1 month</code>, <code>2 years</code>.
-As of June 22, 2020 the [value is set](../../gitlab_com/index.md#gitlab-cicd) to 3 months on GitLab.com. Jobs created before that date were archived after September 22, 2020.
+For the value set for GitLab.com, see [Scheduled job archiving](../../gitlab_com/index.md#gitlab-cicd).
## Protect CI/CD variables by default
@@ -233,7 +230,7 @@ To select a CI/CD template for the required pipeline configuration:
1. On the left sidebar, select **Settings > CI/CD**.
1. Expand the **Required pipeline configuration** section.
1. Select a CI/CD template from the dropdown.
-1. Click **Save changes**.
+1. Select **Save changes**.
## Package Registry configuration
@@ -272,7 +269,7 @@ To set the maximum file size:
1. Expand the **Package Registry** section.
1. Find the package type you would like to adjust.
1. Enter the maximum file size, in bytes.
-1. Click **Save size limits**.
+1. Select **Save size limits**.
## Prevent users from registering runners
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
index 78c96fb9e9e..5cf6a505bee 100644
--- a/doc/user/gitlab_com/index.md
+++ b/doc/user/gitlab_com/index.md
@@ -133,20 +133,20 @@ Below are the current settings regarding [GitLab CI/CD](../../ci/index.md).
Any settings or feature limits not listed here are using the defaults listed in
the related documentation.
-| Setting | GitLab.com | Default |
-|-------------------------------------|-------------|---------|
-| Artifacts maximum size (compressed) | 1 GB | 100 MB |
-| Artifacts [expiry time](../../ci/yaml/index.md#artifactsexpire_in) | From June 22, 2020, deleted after 30 days unless otherwise specified (artifacts created before that date have no expiry). | deleted after 30 days unless otherwise specified |
-| Scheduled Pipeline Cron | `*/5 * * * *` | `3-59/10 * * * *` |
-| [Max jobs in active pipelines](../../administration/instance_limits.md#number-of-jobs-in-active-pipelines) | `500` for Free tier, unlimited otherwise | Unlimited |
-| [Max CI/CD subscriptions to a project](../../administration/instance_limits.md#number-of-cicd-subscriptions-to-a-project) | `2` | Unlimited |
-| [Max number of pipeline triggers in a project](../../administration/instance_limits.md#limit-the-number-of-pipeline-triggers) | `25000` for Free tier, Unlimited for all paid tiers | Unlimited |
-| [Max pipeline schedules in projects](../../administration/instance_limits.md#number-of-pipeline-schedules) | `10` for Free tier, `50` for all paid tiers | Unlimited |
-| [Max pipelines per schedule](../../administration/instance_limits.md#limit-the-number-of-pipelines-created-by-a-pipeline-schedule-per-day) | `24` for Free tier, `288` for all paid tiers | Unlimited |
-| [Scheduled Job Archival](../../user/admin_area/settings/continuous_integration.md#archive-jobs) | 3 months | Never |
-| Max test cases per [unit test report](../../ci/unit_test_reports.md) | `500_000` | Unlimited |
-| [Max registered runners](../../administration/instance_limits.md#number-of-registered-runners-per-scope) | Free tier: `50` per-group / `50` per-project <br/> All paid tiers: `1_000` per-group / `1_000` per-project | `1_000` per-group / `1_000` per-project |
-| [Limit dotenv variables](../../administration/instance_limits.md#limit-dotenv-variables) | Free tier: `50` / Premium tier: `100` / Ultimate tier: `150` | 150 |
+| Setting | GitLab.com | Default (self-managed) |
+|:-------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Artifacts maximum size (compressed) | 1 GB | See [Maximum artifacts size](../../user/admin_area/settings/continuous_integration.md#maximum-artifacts-size) |
+| Artifacts [expiry time](../../ci/yaml/index.md#artifactsexpire_in) | From June 22, 2020, deleted after 30 days unless otherwise specified (artifacts created before that date have no expiry). | See [Default artifacts expiration](../admin_area/settings/continuous_integration.md#default-artifacts-expiration) |
+| Scheduled Pipeline Cron | `*/5 * * * *` | See [Pipeline schedules advanced configuration](../../administration/cicd.md#change-maximum-scheduled-pipeline-frequency) |
+| Maximum jobs in active pipelines | `500` for Free tier, unlimited otherwise | See [Number of jobs in active pipelines](../../administration/instance_limits.md#number-of-jobs-in-active-pipelines) |
+| Maximum CI/CD subscriptions to a project | `2` | See [Number of CI/CD subscriptions to a project](../../administration/instance_limits.md#number-of-cicd-subscriptions-to-a-project) |
+| Maximum number of pipeline triggers in a project | `25000` for Free tier, Unlimited for all paid tiers | See [Limit the number of pipeline triggers](../../administration/instance_limits.md#limit-the-number-of-pipeline-triggers) |
+| Maximum pipeline schedules in projects | `10` for Free tier, `50` for all paid tiers | See [Number of pipeline schedules](../../administration/instance_limits.md#number-of-pipeline-schedules) |
+| Maximum pipelines per schedule | `24` for Free tier, `288` for all paid tiers | See [Limit the number of pipelines created by a pipeline schedule per day](../../administration/instance_limits.md#limit-the-number-of-pipelines-created-by-a-pipeline-schedule-per-day) |
+| Scheduled job archiving | 3 months (from June 22, 2020). Jobs created before that date were archived after September 22, 2020. | Never |
+| Maximum test cases per [unit test report](../../ci/unit_test_reports.md) | `500000` | Unlimited |
+| Maximum registered runners | Free tier: `50` per-group / `50` per-project<br/>All paid tiers: `1000` per-group / `1000` per-project | See [Number of registered runners per scope](../../administration/instance_limits.md#number-of-registered-runners-per-scope) |
+| Limit of dotenv variables | Free tier: `50` / Premium tier: `100` / Ultimate tier: `150` | See [Limit dotenv variables](../../administration/instance_limits.md#limit-dotenv-variables) |
## Package registry limits
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index a21fc378ece..9697ca955db 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -39625,7 +39625,7 @@ msgstr ""
msgid "UsageQuota|File attachments and smaller design graphics."
msgstr ""
-msgid "UsageQuota|From %{storage_enforcement_date} storage limits will apply to this namespace. View and manage your usage in %{strong_start}Group Settings &gt; Usage quotas%{strong_end}."
+msgid "UsageQuota|From %{storage_enforcement_date} storage limits will apply to this namespace. View and manage your usage in %{strong_start}%{namespace_type} settings &gt; Usage quotas%{strong_end}."
msgstr ""
msgid "UsageQuota|Git repository."
diff --git a/qa/Gemfile b/qa/Gemfile
index 05acab5653f..8dee3fb9326 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -29,6 +29,7 @@ gem 'influxdb-client', '~> 1.17'
gem 'terminal-table', '~> 3.0.0', require: false
gem 'slack-notifier', '~> 2.4', require: false
gem 'fog-google', '~> 1.17', require: false
+gem 'google-cloud-storage', '~> 1.36', require: false
gem 'confiner', '~> 0.2'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 369ab0860ed..ac683ad549d 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -65,6 +65,8 @@ GEM
deprecation_toolkit (1.5.1)
activesupport (>= 4.2)
diff-lcs (1.3)
+ digest-crc (0.6.4)
+ rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
equalizer (0.0.11)
@@ -150,8 +152,20 @@ GEM
google-apis-core (>= 0.4, < 2.a)
google-apis-storage_v1 (0.9.0)
google-apis-core (>= 0.4, < 2.a)
+ google-cloud-core (1.6.0)
+ google-cloud-env (~> 1.0)
+ google-cloud-errors (~> 1.0)
google-cloud-env (1.5.0)
faraday (>= 0.17.3, < 2.0)
+ google-cloud-errors (1.2.0)
+ google-cloud-storage (1.36.1)
+ addressable (~> 2.8)
+ digest-crc (~> 0.4)
+ google-apis-iamcredentials_v1 (~> 0.1)
+ google-apis-storage_v1 (~> 0.1)
+ google-cloud-core (~> 1.6)
+ googleauth (>= 0.16.2, < 2.a)
+ mini_mime (~> 1.0)
googleauth (1.1.0)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
@@ -368,6 +382,7 @@ DEPENDENCIES
faker (~> 2.19, >= 2.19.0)
fog-google (~> 1.17)
gitlab-qa
+ google-cloud-storage (~> 1.36)
influxdb-client (~> 1.17)
knapsack (~> 4.0)
octokit (~> 4.21)
diff --git a/qa/Rakefile b/qa/Rakefile
index b6fae0f685d..f43c84e3b39 100644
--- a/qa/Rakefile
+++ b/qa/Rakefile
@@ -60,13 +60,26 @@ task :delete_projects do
QA::Tools::DeleteProjects.new.run
end
-desc "Deletes resources created during E2E test runs"
-task :delete_test_resources, :file_pattern do |t, args|
- QA::Tools::DeleteTestResources.new(args[:file_pattern]).run
-end
-
desc "Deletes test users"
task :delete_test_users, [:delete_before, :dry_run, :exclude_users] do |t, args|
QA::Tools::DeleteTestUsers.new(args).run
end
+
+namespace :test_resources do
+ desc "Deletes resources created during E2E test runs"
+ task :delete, :file_pattern do |t, args|
+ args.with_defaults(file_pattern: QA::Runtime::Env.test_resources_created_filepath)
+ QA::Tools::TestResourcesHandler.new(args[:file_pattern]).run_delete
+ end
+
+ desc "Upload test resources JSON files to GCS"
+ task :upload, [:file_pattern, :environment_name] do |t, args|
+ QA::Tools::TestResourcesHandler.new(args[:file_pattern]).upload(args[:environment_name])
+ end
+
+ desc "Download test resources JSON files from GCS"
+ task :download, [:environment_name] do |t, args|
+ QA::Tools::TestResourcesHandler.new.download(args[:environment_name])
+ end
+end
# rubocop:enable Rails/RakeEnvironment
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index 876d3c0b42a..fe874b36d0e 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -449,11 +449,6 @@ module QA
running_in_ci? && enabled?(ENV['QA_EXPORT_TEST_METRICS'], default: true)
end
- def test_resources_created_filepath
- file_name = running_in_ci? ? "test-resources-#{SecureRandom.hex(3)}.json" : 'test-resources.json'
- ENV.fetch('QA_TEST_RESOURCES_CREATED_FILEPATH', File.join(Path.qa_root, 'tmp', file_name))
- end
-
def ee_activation_code
ENV['QA_EE_ACTIVATION_CODE']
end
diff --git a/qa/qa/tools/delete_test_resources.rb b/qa/qa/tools/delete_test_resources.rb
deleted file mode 100644
index 48316067421..00000000000
--- a/qa/qa/tools/delete_test_resources.rb
+++ /dev/null
@@ -1,106 +0,0 @@
-# frozen_string_literal: true
-
-# This script reads from test-resources JSON file to collect data about resources to delete
-# Filter out resources that cannot be deleted
-# Then deletes all deletable resources that E2E tests created
-#
-# Required environment variables: GITLAB_QA_ACCESS_TOKEN and GITLAB_ADDRESS
-# When in CI also requires: QA_TEST_RESOURCES_FILE_PATTERN
-# Run `rake delete_test_resources[<file_pattern>]`
-
-module QA
- module Tools
- class DeleteTestResources
- include Support::API
-
- IGNORED_RESOURCES = [
- 'QA::Resource::PersonalAccessToken',
- 'QA::Resource::CiVariable',
- 'QA::Resource::Repository::Commit',
- 'QA::EE::Resource::GroupIteration',
- 'QA::EE::Resource::Settings::Elasticsearch'
- ].freeze
-
- def initialize(file_pattern = Runtime::Env.test_resources_created_filepath)
- raise ArgumentError, "Please provide GITLAB_ADDRESS" unless ENV['GITLAB_ADDRESS']
- raise ArgumentError, "Please provide GITLAB_QA_ACCESS_TOKEN" unless ENV['GITLAB_QA_ACCESS_TOKEN']
-
- @api_client = Runtime::API::Client.new(ENV['GITLAB_ADDRESS'], personal_access_token: ENV['GITLAB_QA_ACCESS_TOKEN'])
- @file_pattern = file_pattern
- end
-
- def run
- failures = files.flat_map do |file|
- resources = read_file(file)
- next if resources.nil?
-
- filtered_resources = filter_resources(resources)
- delete_resources(filtered_resources)
- end
-
- return puts "\nDone" if failures.empty?
-
- puts "\nFailed to delete #{failures.size} resources:\n"
- puts failures
- end
-
- private
-
- def files
- Runtime::Logger.info('Gathering JSON files...')
- files = Dir.glob(@file_pattern)
- abort("There is no file with this pattern #{@file_pattern}") if files.empty?
-
- files.reject! { |file| File.zero?(file) }
-
- files
- end
-
- def read_file(file)
- JSON.parse(File.read(file))
- rescue JSON::ParserError
- Runtime::Logger.error("Failed to read #{file} - Invalid format")
- nil
- end
-
- def filter_resources(resources)
- Runtime::Logger.info('Filtering resources - Only keep deletable resources...')
-
- transformed_values = resources.transform_values! do |v|
- v.reject do |attributes|
- attributes['info'] == "with full_path 'gitlab-qa-sandbox-group'" ||
- attributes['http_method'] == 'get' && !attributes['info']&.include?("with username 'qa-") ||
- attributes['api_path'] == 'Cannot find resource API path'
- end
- end
-
- transformed_values.reject! { |k, v| v.empty? || IGNORED_RESOURCES.include?(k) }
- end
-
- def delete_resources(resources)
- Runtime::Logger.info('Nothing to delete.') && return if resources.nil?
-
- resources.each_with_object([]) do |(key, value), failures|
- value.each do |resource|
- next if resource_not_found?(resource['api_path'])
-
- resource_info = resource['info'] ? "#{key} - #{resource['info']}" : "#{key} at #{resource['api_path']}"
- delete_response = delete(Runtime::API::Request.new(@api_client, resource['api_path']).url)
-
- if delete_response.code == 202 || delete_response.code == 204
- Runtime::Logger.info("Deleting #{resource_info}... SUCCESS")
- else
- Runtime::Logger.info("Deleting #{resource_info}... FAILED")
- failures << resource_info
- end
- end
- end
- end
-
- def resource_not_found?(api_path)
- # if api path contains param "?hard_delete=<boolean>", remove it
- get(Runtime::API::Request.new(@api_client, api_path.split('?').first).url).code.eql? 404
- end
- end
- end
-end
diff --git a/qa/qa/tools/test_resource_data_processor.rb b/qa/qa/tools/test_resource_data_processor.rb
index b9c2540c681..a86c94b4914 100644
--- a/qa/qa/tools/test_resource_data_processor.rb
+++ b/qa/qa/tools/test_resource_data_processor.rb
@@ -49,10 +49,12 @@ module QA
# Otherwise create file and write data hash to file for the first time
#
# @return [void]
- def write_to_file
+ def write_to_file(suite_failed)
return if resources.empty?
- file = Pathname.new(Runtime::Env.test_resources_created_filepath)
+ start_str = suite_failed ? 'failed-test-resources' : 'test-resources'
+ file_name = Runtime::Env.running_in_ci? ? "#{start_str}-#{SecureRandom.hex(3)}.json" : "#{start_str}.json"
+ file = Pathname.new(File.join(Runtime::Path.qa_root, 'tmp', file_name))
FileUtils.mkdir_p(file.dirname)
data = resources.deep_dup
diff --git a/qa/qa/tools/test_resources_handler.rb b/qa/qa/tools/test_resources_handler.rb
new file mode 100644
index 00000000000..c298be25160
--- /dev/null
+++ b/qa/qa/tools/test_resources_handler.rb
@@ -0,0 +1,182 @@
+# frozen_string_literal: true
+
+require "google/cloud/storage"
+
+# This script handles resources created during E2E test runs
+#
+# Delete: find all matching file pattern, read file and delete resources
+# rake test_resources:delete
+# rake test_resources:delete[<file_pattern>]
+#
+# Upload: find all matching file pattern for failed test resources
+# upload these files to GCS bucket `failed-test-resources` under specific environment name
+# rake test_resources:upload[<file_pattern>,<environment_name>]
+#
+# Download: download JSON files under a given environment name (bucket directory)
+# save to local under `tmp/`
+# rake test_resources:download[<environment_name>]
+#
+# Required environment variables:
+# GITLAB_ADDRESS, required for delete task
+# GITLAB_QA_ACCESS_TOKEN, required for delete task
+# QA_TEST_RESOURCES_FILE_PATTERN, optional for delete task, required for upload task
+# QA_FAILED_TEST_RESOURCES_GCS_CREDENTIALS, required for upload task or download task
+
+module QA
+ module Tools
+ class TestResourcesHandler
+ include Support::API
+
+ IGNORED_RESOURCES = [
+ 'QA::Resource::PersonalAccessToken',
+ 'QA::Resource::CiVariable',
+ 'QA::Resource::Repository::Commit',
+ 'QA::EE::Resource::GroupIteration',
+ 'QA::EE::Resource::Settings::Elasticsearch'
+ ].freeze
+
+ PROJECT = 'gitlab-qa-resources'
+ BUCKET = 'failed-test-resources'
+
+ def initialize(file_pattern = nil)
+ @file_pattern = file_pattern
+ end
+
+ def run_delete
+ failures = files.flat_map do |file|
+ resources = read_file(file)
+ next if resources.nil?
+
+ filtered_resources = filter_resources(resources)
+ delete_resources(filtered_resources)
+ end
+
+ return puts "\nDone" if failures.empty?
+
+ puts "\nFailed to delete #{failures.size} resources:\n"
+ puts failures
+ end
+
+ # Upload resources from failed test suites to GCS bucket
+ # Files are organized by environment in which tests were executed
+ #
+ # E.g: staging/failed-test-resources-<randomhex>.json
+ def upload(environment_name)
+ return puts "\nNothing to upload!" if files.empty?
+
+ files.each do |file|
+ file_name = "#{environment_name}/#{file.split('/').last}"
+ Runtime::Logger.info("Uploading #{file_name}...")
+ gcs_bucket.create_file(file, file_name)
+ end
+
+ puts "\nDone"
+ end
+
+ # Download files from GCS bucket by environment name
+ # Delete the files afterward
+ def download(environment_name)
+ files_list = gcs_bucket.files(prefix: "#{environment_name}")
+
+ return puts "\nNothing to download!" if files_list.empty?
+
+ files_list.each do |file|
+ local_path = "tmp/#{file.name.split('/').last}"
+ Runtime::Logger.info("Downloading #{file.name} to #{local_path}")
+ file.download(local_path)
+
+ Runtime::Logger.info("Deleting #{file.name} from bucket")
+ file.delete
+ end
+
+ puts "\nDone"
+ end
+
+ private
+
+ def files
+ Runtime::Logger.info('Gathering JSON files...')
+ files = Dir.glob(@file_pattern)
+ abort("There is no file with this pattern #{@file_pattern}") if files.empty?
+
+ files.reject! { |file| File.zero?(file) }
+
+ files
+ end
+
+ def read_file(file)
+ JSON.parse(File.read(file))
+ rescue JSON::ParserError
+ Runtime::Logger.error("Failed to read #{file} - Invalid format")
+ nil
+ end
+
+ def filter_resources(resources)
+ Runtime::Logger.info('Filtering resources - Only keep deletable resources...')
+
+ transformed_values = resources.transform_values! do |v|
+ v.reject do |attributes|
+ attributes['info'] == "with full_path 'gitlab-qa-sandbox-group'" ||
+ attributes['http_method'] == 'get' && !attributes['info']&.include?("with username 'qa-") ||
+ attributes['api_path'] == 'Cannot find resource API path'
+ end
+ end
+
+ transformed_values.reject! { |k, v| v.empty? || IGNORED_RESOURCES.include?(k) }
+ end
+
+ def delete_resources(resources)
+ Runtime::Logger.info('Nothing to delete.') && return if resources.nil?
+
+ resources.each_with_object([]) do |(key, value), failures|
+ value.each do |resource|
+ next if resource_not_found?(resource['api_path'])
+
+ resource_info = resource['info'] ? "#{key} - #{resource['info']}" : "#{key} at #{resource['api_path']}"
+ delete_response = delete(Runtime::API::Request.new(api_client, resource['api_path']).url)
+
+ if delete_response.code == 202 || delete_response.code == 204
+ Runtime::Logger.info("Deleting #{resource_info}... SUCCESS")
+ else
+ Runtime::Logger.info("Deleting #{resource_info}... FAILED")
+ failures << resource_info
+ end
+ end
+ end
+ end
+
+ def resource_not_found?(api_path)
+ # if api path contains param "?hard_delete=<boolean>", remove it
+ get(Runtime::API::Request.new(api_client, api_path.split('?').first).url).code.eql? 404
+ end
+
+ def api_client
+ abort("\nPlease provide GITLAB_ADDRESS") unless ENV['GITLAB_ADDRESS']
+ abort("\nPlease provide GITLAB_QA_ACCESS_TOKEN") unless ENV['GITLAB_QA_ACCESS_TOKEN']
+
+ @api_client ||= Runtime::API::Client.new(ENV['GITLAB_ADDRESS'], personal_access_token: ENV['GITLAB_QA_ACCESS_TOKEN'])
+ end
+
+ def gcs_storage
+ @gcs_storage ||= Google::Cloud::Storage.new(
+ project_id: PROJECT,
+ credentials: json_key
+ )
+ rescue StandardError => e
+ abort("\nThere might be something wrong with the JSON key file - [ERROR] #{e}")
+ end
+
+ def gcs_bucket
+ @gcs_bucket ||= gcs_storage.bucket(BUCKET, skip_lookup: true)
+ end
+
+ # Path to GCS service account json key file
+ # Or the content of the key file as a hash
+ def json_key
+ abort("\nPlease provide QA_FAILED_TEST_RESOURCES_GCS_CREDENTIALS") unless ENV['QA_FAILED_TEST_RESOURCES_GCS_CREDENTIALS']
+
+ @json_key ||= ENV["QA_FAILED_TEST_RESOURCES_GCS_CREDENTIALS"]
+ end
+ end
+ end
+end
diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb
index 0f752ad96b7..22603497019 100644
--- a/qa/spec/runtime/env_spec.rb
+++ b/qa/spec/runtime/env_spec.rb
@@ -360,36 +360,4 @@ RSpec.describe QA::Runtime::Env do
end
end
end
-
- describe '.test_resources_created_filepath' do
- context 'when not in CI' do
- before do
- allow(described_class).to receive(:running_in_ci?).and_return(false)
- end
-
- it 'returns default path if QA_TEST_RESOURCES_CREATED_FILEPATH is not defined' do
- stub_env('QA_TEST_RESOURCES_CREATED_FILEPATH', nil)
-
- expect(described_class.test_resources_created_filepath).to include('tmp/test-resources.json')
- end
-
- it 'returns path if QA_TEST_RESOURCES_CREATED_FILEPATH is defined' do
- stub_env('QA_TEST_RESOURCES_CREATED_FILEPATH', 'path/to_file')
-
- expect(described_class.test_resources_created_filepath).to eq('path/to_file')
- end
- end
-
- context 'when in CI' do
- before do
- allow(described_class).to receive(:running_in_ci?).and_return(true)
- allow(SecureRandom).to receive(:hex).with(3).and_return('abc123')
- stub_env('QA_TEST_RESOURCES_CREATED_FILEPATH', nil)
- end
-
- it 'returns path with random hex in file name' do
- expect(described_class.test_resources_created_filepath).to include('tmp/test-resources-abc123.json')
- end
- end
- end
end
diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb
index 1e6d39224aa..655b0088feb 100644
--- a/qa/spec/spec_helper.rb
+++ b/qa/spec/spec_helper.rb
@@ -69,7 +69,7 @@ RSpec.configure do |config|
config.after(:suite) do |suite|
# Write all test created resources to JSON file
- QA::Tools::TestResourceDataProcessor.write_to_file
+ QA::Tools::TestResourceDataProcessor.write_to_file(suite.reporter.failed_examples.any?)
# If requested, confirm that resources were used appropriately (e.g., not left with changes that interfere with
# further reuse)
diff --git a/qa/spec/tools/test_resources_data_processor_spec.rb b/qa/spec/tools/test_resources_data_processor_spec.rb
index 5117d1d367f..2ae43974a0c 100644
--- a/qa/spec/tools/test_resources_data_processor_spec.rb
+++ b/qa/spec/tools/test_resources_data_processor_spec.rb
@@ -43,18 +43,30 @@ RSpec.describe QA::Tools::TestResourceDataProcessor do
end
describe '.write_to_file' do
- let(:resources_file) { Pathname.new(Faker::File.file_name(dir: 'tmp', ext: 'json')) }
+ using RSpec::Parameterized::TableSyntax
- before do
- stub_env('QA_TEST_RESOURCES_CREATED_FILEPATH', resources_file)
-
- allow(File).to receive(:write)
+ where(:ci, :suite_failed, :file_path) do
+ true | true | 'root/tmp/failed-test-resources-random.json'
+ true | false | 'root/tmp/test-resources-random.json'
+ false | true | 'root/tmp/failed-test-resources.json'
+ false | false | 'root/tmp/test-resources.json'
end
- it 'writes applicable resources to file' do
- processor.write_to_file
+ with_them do
+ let(:resources_file) { Pathname.new(file_path) }
+
+ before do
+ allow(QA::Runtime::Env).to receive(:running_in_ci?).and_return(ci)
+ allow(File).to receive(:write)
+ allow(QA::Runtime::Path).to receive(:qa_root).and_return('root')
+ allow(SecureRandom).to receive(:hex).with(any_args).and_return('random')
+ end
+
+ it 'writes applicable resources to file' do
+ processor.write_to_file(suite_failed)
- expect(File).to have_received(:write).with(resources_file, JSON.pretty_generate(result))
+ expect(File).to have_received(:write).with(resources_file, JSON.pretty_generate(result))
+ end
end
end
end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 1fb5567b928..08183badda1 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -474,4 +474,69 @@ RSpec.describe 'Group' do
fill_in 'confirm_name_input', with: confirm_with
click_button 'Confirm'
end
+
+ describe 'storage_enforcement_banner', :js do
+ let_it_be(:group) { create(:group) }
+ let_it_be_with_refind(:user) { create(:user) }
+
+ before_all do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ context 'with storage_enforcement_date set' do
+ let_it_be(:storage_enforcement_date) { Date.today + 30 }
+
+ before do
+ allow_next_found_instance_of(Group) do |g|
+ allow(g).to receive(:storage_enforcement_date).and_return(storage_enforcement_date)
+ end
+ end
+
+ it 'displays the banner in the group page' do
+ visit group_path(group)
+ expect_page_to_have_storage_enforcement_banner(storage_enforcement_date)
+ end
+
+ it 'does not display the banner in a paid group page' do
+ allow_next_found_instance_of(Group) do |g|
+ allow(g).to receive(:paid?).and_return(true)
+ end
+ visit group_path(group)
+ expect_page_not_to_have_storage_enforcement_banner
+ end
+
+ it 'does not display the banner if user has previously closed unless threshold has changed' do
+ visit group_path(group)
+ expect_page_to_have_storage_enforcement_banner(storage_enforcement_date)
+ find('.js-storage-enforcement-banner [data-testid="close-icon"]').click
+ page.refresh
+ expect_page_not_to_have_storage_enforcement_banner
+
+ storage_enforcement_date = Date.today + 13
+ allow_next_found_instance_of(Group) do |g|
+ allow(g).to receive(:storage_enforcement_date).and_return(storage_enforcement_date)
+ end
+ page.refresh
+ expect_page_to_have_storage_enforcement_banner(storage_enforcement_date)
+ end
+ end
+
+ context 'with storage_enforcement_date not set' do
+ # This test should break and be rewritten after the implementation of the storage_enforcement_date
+ # TBD: https://gitlab.com/gitlab-org/gitlab/-/issues/350632
+ it 'does not display the banner in the group page' do
+ visit group_path(group)
+ expect_page_not_to_have_storage_enforcement_banner
+ end
+ end
+ end
+
+ def expect_page_to_have_storage_enforcement_banner(storage_enforcement_date)
+ expect(page).to have_text "From #{storage_enforcement_date} storage limits will apply to this namespace"
+ end
+
+ def expect_page_not_to_have_storage_enforcement_banner
+ expect(page).not_to have_text "storage limits will apply to this namespace"
+ end
end
diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb
index d90ac439eee..7d545711997 100644
--- a/spec/features/profiles/user_visits_profile_spec.rb
+++ b/spec/features/profiles/user_visits_profile_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe 'User visits their profile' do
- let(:user) { create(:user) }
+ let_it_be_with_refind(:user) { create(:user) }
before do
sign_in(user)
@@ -87,4 +87,53 @@ RSpec.describe 'User visits their profile' do
end
end
end
+
+ describe 'storage_enforcement_banner', :js do
+ context 'with storage_enforcement_date set' do
+ let_it_be(:storage_enforcement_date) { Date.today + 30 }
+
+ before do
+ allow_next_found_instance_of(Namespaces::UserNamespace) do |g|
+ allow(g).to receive(:storage_enforcement_date).and_return(storage_enforcement_date)
+ end
+ end
+
+ it 'displays the banner in the profile page' do
+ visit(profile_path)
+ expect_page_to_have_storage_enforcement_banner(storage_enforcement_date)
+ end
+
+ it 'does not display the banner if user has previously closed unless threshold has changed' do
+ visit(profile_path)
+ expect_page_to_have_storage_enforcement_banner(storage_enforcement_date)
+ find('.js-storage-enforcement-banner [data-testid="close-icon"]').click
+ page.refresh
+ expect_page_not_to_have_storage_enforcement_banner
+
+ storage_enforcement_date = Date.today + 13
+ allow_next_found_instance_of(Namespaces::UserNamespace) do |g|
+ allow(g).to receive(:storage_enforcement_date).and_return(storage_enforcement_date)
+ end
+ page.refresh
+ expect_page_to_have_storage_enforcement_banner(storage_enforcement_date)
+ end
+ end
+
+ context 'with storage_enforcement_date not set' do
+ # This test should break and be rewritten after the implementation of the storage_enforcement_date
+ # TBD: https://gitlab.com/gitlab-org/gitlab/-/issues/350632
+ it 'does not display the banner in the group page' do
+ visit(profile_path)
+ expect_page_not_to_have_storage_enforcement_banner
+ end
+ end
+ end
+
+ def expect_page_to_have_storage_enforcement_banner(storage_enforcement_date)
+ expect(page).to have_text "From #{storage_enforcement_date} storage limits will apply to this namespace"
+ end
+
+ def expect_page_not_to_have_storage_enforcement_banner
+ expect(page).not_to have_text "storage limits will apply to this namespace"
+ end
end
diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js
index 1c2688e4df1..9ed0294e876 100644
--- a/spec/frontend/incidents/components/incidents_list_spec.js
+++ b/spec/frontend/incidents/components/incidents_list_spec.js
@@ -1,6 +1,7 @@
import { GlAlert, GlLoadingIcon, GlTable, GlAvatar, GlEmptyState } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import IncidentsList from '~/incidents/components/incidents_list.vue';
import {
I18N,
@@ -19,7 +20,7 @@ import mockIncidents from '../mocks/incidents.json';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn().mockName('visitUrlMock'),
- joinPaths: jest.fn(),
+ joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths,
mergeUrlParams: jest.fn(),
setUrlParams: jest.fn(),
updateHistory: jest.fn(),
@@ -49,48 +50,51 @@ describe('Incidents List', () => {
const findEmptyState = () => wrapper.find(GlEmptyState);
const findSeverity = () => wrapper.findAll(SeverityToken);
const findEscalationStatus = () => wrapper.findAll('[data-testid="incident-escalation-status"]');
+ const findIncidentLink = () => wrapper.findByTestId('incident-link');
function mountComponent({ data = {}, loading = false, provide = {} } = {}) {
- wrapper = mount(IncidentsList, {
- data() {
- return {
- incidents: [],
- incidentsCount: {},
- ...data,
- };
- },
- mocks: {
- $apollo: {
- queries: {
- incidents: {
- loading,
+ wrapper = extendedWrapper(
+ mount(IncidentsList, {
+ data() {
+ return {
+ incidents: [],
+ incidentsCount: {},
+ ...data,
+ };
+ },
+ mocks: {
+ $apollo: {
+ queries: {
+ incidents: {
+ loading,
+ },
},
},
},
- },
- provide: {
- projectPath: '/project/path',
- newIssuePath,
- incidentTemplateName,
- incidentType,
- issuePath: '/project/issues',
- publishedAvailable: true,
- emptyListSvgPath,
- textQuery: '',
- authorUsernameQuery: '',
- assigneeUsernameQuery: '',
- slaFeatureAvailable: true,
- canCreateIncident: true,
- incidentEscalationsAvailable: true,
- ...provide,
- },
- stubs: {
- GlButton: true,
- GlAvatar: true,
- GlEmptyState: true,
- ServiceLevelAgreementCell: true,
- },
- });
+ provide: {
+ projectPath: '/project/path',
+ newIssuePath,
+ incidentTemplateName,
+ incidentType,
+ issuePath: '/project/issues',
+ publishedAvailable: true,
+ emptyListSvgPath,
+ textQuery: '',
+ authorUsernameQuery: '',
+ assigneeUsernameQuery: '',
+ slaFeatureAvailable: true,
+ canCreateIncident: true,
+ incidentEscalationsAvailable: true,
+ ...provide,
+ },
+ stubs: {
+ GlButton: true,
+ GlAvatar: true,
+ GlEmptyState: true,
+ ServiceLevelAgreementCell: true,
+ },
+ }),
+ );
}
afterEach(() => {
@@ -160,6 +164,14 @@ describe('Incidents List', () => {
expect(findTimeAgo().length).toBe(mockIncidents.length);
});
+ it('renders a link to the incident as the incident title', () => {
+ const { title, iid } = mockIncidents[0];
+ const link = findIncidentLink();
+
+ expect(link.text()).toBe(title);
+ expect(link.attributes('href')).toContain(`issues/incident/${iid}`);
+ });
+
describe('Assignees', () => {
it('shows Unassigned when there are no assignees', () => {
expect(findAssignees().at(0).text()).toBe(I18N.unassigned);
diff --git a/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb b/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb
index dd9305d2197..11c0fa44110 100644
--- a/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb
+++ b/spec/graphql/mutations/boards/issues/issue_move_list_spec.rb
@@ -43,11 +43,10 @@ RSpec.describe Mutations::Boards::Issues::IssueMoveList do
context "when we only pass #{arg_name}" do
let(:move_params) { { arg_name => list1.id } }
- it 'raises an error' do
- expect { subject }.to raise_error(
- Gitlab::Graphql::Errors::ArgumentError,
- 'Both fromListId and toListId must be present'
- )
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, 'Both fromListId and toListId must be present') do
+ subject
+ end
end
end
end
@@ -55,11 +54,10 @@ RSpec.describe Mutations::Boards::Issues::IssueMoveList do
context 'when required arguments are missing' do
let(:move_params) { {} }
- it 'raises an error' do
- expect { subject }.to raise_error(
- Gitlab::Graphql::Errors::ArgumentError,
- "At least one of the arguments fromListId, toListId, afterId or beforeId is required"
- )
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, 'At least one of the arguments fromListId, toListId, afterId or beforeId is required') do
+ subject
+ end
end
end
diff --git a/spec/graphql/mutations/ci/runner/delete_spec.rb b/spec/graphql/mutations/ci/runner/delete_spec.rb
index 82e8203e6bb..fbd13c6d3ea 100644
--- a/spec/graphql/mutations/ci/runner/delete_spec.rb
+++ b/spec/graphql/mutations/ci/runner/delete_spec.rb
@@ -22,8 +22,10 @@ RSpec.describe Mutations::Ci::Runner::Delete do
end
context 'when the user cannot admin the runner' do
- it 'raises an error' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do
+ subject
+ end
end
end
diff --git a/spec/graphql/mutations/ci/runner/update_spec.rb b/spec/graphql/mutations/ci/runner/update_spec.rb
index 83150c3d7f6..0b3489d37dc 100644
--- a/spec/graphql/mutations/ci/runner/update_spec.rb
+++ b/spec/graphql/mutations/ci/runner/update_spec.rb
@@ -26,8 +26,10 @@ RSpec.describe Mutations::Ci::Runner::Update do
end
context 'when the user cannot admin the runner' do
- it 'raises an error' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do
+ subject
+ end
end
end
diff --git a/spec/graphql/mutations/release_asset_links/create_spec.rb b/spec/graphql/mutations/release_asset_links/create_spec.rb
index eb7cbb4b789..86a6c77fa3f 100644
--- a/spec/graphql/mutations/release_asset_links/create_spec.rb
+++ b/spec/graphql/mutations/release_asset_links/create_spec.rb
@@ -63,7 +63,9 @@ RSpec.describe Mutations::ReleaseAssetLinks::Create do
let!(:protected_tag) { create(:protected_tag, :maintainers_can_create, name: '*', project: project) }
it 'has an access error' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do
+ subject
+ end
end
end
end
@@ -71,16 +73,20 @@ RSpec.describe Mutations::ReleaseAssetLinks::Create do
context "when the user doesn't have access to the project" do
let(:current_user) { reporter }
- it 'raises an error' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do
+ subject
+ end
end
end
context "when the project doesn't exist" do
let(:project_path) { 'project/that/does/not/exist' }
- it 'raises an error' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do
+ subject
+ end
end
end
diff --git a/spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb b/spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb
index 8522542498d..59616815de0 100644
--- a/spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb
+++ b/spec/graphql/resolvers/ci/job_token_scope_resolver_spec.rb
@@ -54,10 +54,8 @@ RSpec.describe Resolvers::Ci::JobTokenScopeResolver do
project.add_user(current_user, :developer)
end
- it 'raises error' do
- expect do
- resolve_scope
- end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) { resolve_scope }
end
end
end
diff --git a/spec/graphql/resolvers/group_members/notification_email_resolver_spec.rb b/spec/graphql/resolvers/group_members/notification_email_resolver_spec.rb
new file mode 100644
index 00000000000..fcf67120b0e
--- /dev/null
+++ b/spec/graphql/resolvers/group_members/notification_email_resolver_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::GroupMembers::NotificationEmailResolver do
+ include GraphqlHelpers
+
+ describe '#resolve' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:developer) { create(:user) }
+
+ before do
+ group.add_developer(developer)
+ end
+
+ specify do
+ expect(described_class).to have_nullable_graphql_type(GraphQL::Types::String)
+ end
+
+ subject { batch_sync { resolve_notification_email(developer.group_members.first, current_user) }}
+
+ context 'when current_user is admin' do
+ let(:current_user) { create(:user, :admin) }
+
+ before do
+ allow(current_user).to receive(:can_admin_all_resources?).and_return(true)
+ end
+
+ it 'returns email' do
+ expect(subject).to eq(developer.email)
+ end
+ end
+
+ context 'when current_user is not admin' do
+ let(:current_user) { create(:user) }
+
+ it 'raises ResourceNotAvailable error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+ end
+
+ def resolve_notification_email(obj, user)
+ resolve(described_class, obj: obj, ctx: { current_user: user })
+ end
+end
diff --git a/spec/graphql/resolvers/group_milestones_resolver_spec.rb b/spec/graphql/resolvers/group_milestones_resolver_spec.rb
index acfc8313407..7abc779a63c 100644
--- a/spec/graphql/resolvers/group_milestones_resolver_spec.rb
+++ b/spec/graphql/resolvers/group_milestones_resolver_spec.rb
@@ -101,38 +101,38 @@ RSpec.describe Resolvers::GroupMilestonesResolver do
context 'by timeframe' do
context 'when start_date and end_date are present' do
context 'when start date is after end_date' do
- it 'raises error' do
- expect do
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, "startDate is after endDate") do
resolve_group_milestones(start_date: now, end_date: now - 2.days)
- end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, "startDate is after endDate")
+ end
end
end
end
context 'when only start_date is present' do
- it 'raises error' do
- expect do
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, /Both startDate and endDate/) do
resolve_group_milestones(start_date: now)
- end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, /Both startDate and endDate/)
+ end
end
end
context 'when only end_date is present' do
- it 'raises error' do
- expect do
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ArgumentError, /Both startDate and endDate/) do
resolve_group_milestones(end_date: now)
- end.to raise_error(Gitlab::Graphql::Errors::ArgumentError, /Both startDate and endDate/)
+ end
end
end
end
context 'when user cannot read milestones' do
- it 'raises error' do
+ it 'generates an error' do
unauthorized_user = create(:user)
- expect do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do
resolve_group_milestones({}, { current_user: unauthorized_user })
- end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
end
end
diff --git a/spec/graphql/types/group_member_type_spec.rb b/spec/graphql/types/group_member_type_spec.rb
index b1cb8e572ad..389295f3a39 100644
--- a/spec/graphql/types/group_member_type_spec.rb
+++ b/spec/graphql/types/group_member_type_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Types::GroupMemberType do
it 'has the expected fields' do
expected_fields = %w[
- access_level created_by created_at updated_at expires_at group
+ access_level created_by created_at updated_at expires_at group notification_email
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/helpers/storage_helper_spec.rb b/spec/helpers/storage_helper_spec.rb
index 82b78ed831c..6b743422b04 100644
--- a/spec/helpers/storage_helper_spec.rb
+++ b/spec/helpers/storage_helper_spec.rb
@@ -57,6 +57,8 @@ RSpec.describe StorageHelper do
let_it_be(:paid_group) { create(:group) }
before do
+ allow(helper).to receive(:can?).with(current_user, :admin_namespace, free_group).and_return(true)
+ allow(helper).to receive(:can?).with(current_user, :admin_namespace, paid_group).and_return(true)
allow(helper).to receive(:current_user) { current_user }
allow(Gitlab).to receive(:com?).and_return(true)
allow(paid_group).to receive(:paid?).and_return(true)
@@ -64,26 +66,37 @@ RSpec.describe StorageHelper do
describe "#storage_enforcement_banner_info" do
it 'returns nil when namespace is not free' do
- expect(storage_enforcement_banner_info(paid_group)).to be(nil)
+ expect(helper.storage_enforcement_banner_info(paid_group)).to be(nil)
end
it 'returns nil when storage_enforcement_date is not set' do
allow(free_group).to receive(:storage_enforcement_date).and_return(nil)
- expect(storage_enforcement_banner_info(free_group)).to be(nil)
+ expect(helper.storage_enforcement_banner_info(free_group)).to be(nil)
end
- it 'returns a hash when storage_enforcement_date is set' do
- storage_enforcement_date = Date.today + 30
- allow(free_group).to receive(:storage_enforcement_date).and_return(storage_enforcement_date)
-
- expect(storage_enforcement_banner_info(free_group)).to eql({
- text: "From #{storage_enforcement_date} storage limits will apply to this namespace. View and manage your usage in <strong>Group Settings &gt; Usage quotas</strong>.",
- variant: 'warning',
- callouts_feature_name: 'storage_enforcement_banner_second_enforcement_threshold',
- callouts_path: '/-/users/group_callouts',
- learn_more_link: '<a rel="noopener noreferrer" target="_blank" href="/help//">Learn more.</a>'
- })
+ describe 'when storage_enforcement_date is set' do
+ let_it_be(:storage_enforcement_date) { Date.today + 30 }
+
+ before do
+ allow(free_group).to receive(:storage_enforcement_date).and_return(storage_enforcement_date)
+ end
+
+ it 'returns nil when current_user do not have access usage quotas page' do
+ allow(helper).to receive(:can?).with(current_user, :admin_namespace, free_group).and_return(false)
+
+ expect(helper.storage_enforcement_banner_info(free_group)).to be(nil)
+ end
+
+ it 'returns a hash when current_user can access usage quotas page' do
+ expect(helper.storage_enforcement_banner_info(free_group)).to eql({
+ text: "From #{storage_enforcement_date} storage limits will apply to this namespace. View and manage your usage in <strong>Group settings &gt; Usage quotas</strong>.",
+ variant: 'warning',
+ callouts_feature_name: 'storage_enforcement_banner_second_enforcement_threshold',
+ callouts_path: '/-/users/group_callouts',
+ learn_more_link: '<a rel="noopener noreferrer" target="_blank" href="/help//">Learn more.</a>'
+ })
+ end
end
context 'when storage_enforcement_date is set and dismissed callout exists' do
@@ -96,7 +109,7 @@ RSpec.describe StorageHelper do
allow(free_group).to receive(:storage_enforcement_date).and_return(storage_enforcement_date)
end
- it { expect(storage_enforcement_banner_info(free_group)).to be(nil) }
+ it { expect(helper.storage_enforcement_banner_info(free_group)).to be(nil) }
end
context 'callouts_feature_name' do
@@ -106,7 +119,7 @@ RSpec.describe StorageHelper do
storage_enforcement_date = Date.today + days_from_now
allow(free_group).to receive(:storage_enforcement_date).and_return(storage_enforcement_date)
- storage_enforcement_banner_info(free_group)[:callouts_feature_name]
+ helper.storage_enforcement_banner_info(free_group)[:callouts_feature_name]
end
it 'returns first callouts_feature_name' do
diff --git a/spec/requests/api/graphql/group/group_members_spec.rb b/spec/requests/api/graphql/group/group_members_spec.rb
index 06afb5b9a49..78852622835 100644
--- a/spec/requests/api/graphql/group/group_members_spec.rb
+++ b/spec/requests/api/graphql/group/group_members_spec.rb
@@ -53,6 +53,30 @@ RSpec.describe 'getting group members information' do
end
end
+ context "when requesting member's notification email" do
+ context 'when current_user is admin' do
+ let_it_be(:admin_user) { create(:user, :admin) }
+
+ it 'returns notification email' do
+ fetch_members_notification_email(current_user: admin_user)
+ notification_emails = graphql_data_at(:group, :group_members, :edges, :node, :notification_email)
+
+ expect(notification_emails).to all be_present
+ expect(graphql_errors).to be_nil
+ end
+ end
+
+ context 'when current_user is not admin' do
+ it 'returns an error' do
+ fetch_members_notification_email
+
+ expect(graphql_errors.first)
+ .to include('path' => ['group', 'groupMembers', 'edges', 0, 'node', 'notificationEmail'],
+ 'message' => a_string_including("you don't have permission to perform this action"))
+ end
+ end
+ end
+
context 'member relations' do
let_it_be(:child_group) { create(:group, :public, parent: parent_group) }
let_it_be(:grandchild_group) { create(:group, :public, parent: child_group) }
@@ -117,6 +141,10 @@ RSpec.describe 'getting group members information' do
post_graphql(members_query(group.full_path, args), current_user: current_user)
end
+ def fetch_members_notification_email(group: parent_group, current_user: user)
+ post_graphql(member_notification_email_query(group.full_path), current_user: current_user)
+ end
+
def members_query(group_path, args = {})
members_node = <<~NODE
edges {
@@ -134,6 +162,24 @@ RSpec.describe 'getting group members information' do
)
end
+ def member_notification_email_query(group_path)
+ members_node = <<~NODE
+ edges {
+ node {
+ user {
+ id
+ }
+ notificationEmail
+ }
+ }
+ NODE
+
+ graphql_query_for("group",
+ { full_path: group_path },
+ [query_graphql_field("groupMembers", {}, members_node)]
+ )
+ end
+
def expect_array_response(*items)
expect(response).to have_gitlab_http_status(:success)
member_gids = graphql_data_at(:group, :group_members, :edges, :node, :user, :id)
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index 8b7d1c753d5..ff8908e531a 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -552,6 +552,12 @@ module GraphqlHelpers
expect(flattened_errors).to be_empty
end
+ # Helps migrate to the new GraphQL interpreter,
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/210556
+ def expect_graphql_error_to_be_created(error_class, match_message = nil)
+ expect { yield }.to raise_error(error_class, match_message)
+ end
+
def flattened_errors
Array.wrap(graphql_errors).flatten.compact
end
diff --git a/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb
index 14b2663a72c..21260e4d954 100644
--- a/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/mutations/security/ci_configuration_shared_examples.rb
@@ -29,8 +29,10 @@ RSpec.shared_examples_for 'graphql mutations security ci configuration' do
describe '#resolve' do
let(:result) { subject }
- it 'raises an error if the resource is not accessible to the user' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ it 'generates an error if the resource is not accessible to the user' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do
+ subject
+ end
end
context 'when user does not have enough permissions' do
@@ -38,8 +40,10 @@ RSpec.shared_examples_for 'graphql mutations security ci configuration' do
project.add_guest(user)
end
- it 'raises an error' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do
+ subject
+ end
end
end
@@ -48,8 +52,10 @@ RSpec.shared_examples_for 'graphql mutations security ci configuration' do
create(:project_empty_repo).add_maintainer(user)
end
- it 'raises an error' do
- expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ it 'generates an error' do
+ expect_graphql_error_to_be_created(Gitlab::Graphql::Errors::ResourceNotAvailable) do
+ subject
+ end
end
end