diff options
63 files changed, 765 insertions, 321 deletions
diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml index c3e6de76894..88e732c2e75 100644 --- a/.gitlab/ci/qa.gitlab-ci.yml +++ b/.gitlab/ci/qa.gitlab-ci.yml @@ -7,6 +7,7 @@ variables: USE_BUNDLE_INSTALL: "false" SETUP_DB: "false" + QA_EXPORT_TEST_METRICS: "false" before_script: - !reference [.default-before_script, before_script] - cd qa/ diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 1f552d1dace..00c6dc6d36b 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -1435,8 +1435,6 @@ changes: *code-qa-patterns when: manual allow_failure: true - - <<: *if-dot-com-gitlab-org-schedule - allow_failure: true .review:rules:danger: rules: diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue index d796b6e38db..857367a0721 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue @@ -32,7 +32,6 @@ export default { return { searchKey: '', labels: [], - currentHighlightItem: -1, localSelectedLabels: [...this.selectedLabels], }; }, @@ -79,16 +78,6 @@ export default { return Boolean(this.searchKey) && this.visibleLabels.length === 0; }, }, - watch: { - searchKey(value) { - // When there is search string present - // and there are matching results, - // highlight first item by default. - if (value && this.visibleLabels.length) { - this.currentHighlightItem = 0; - } - }, - }, created() { this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS); }, @@ -162,16 +151,15 @@ export default { /> <template v-else> <gl-dropdown-item - v-for="(label, index) in visibleLabels" + v-for="label in visibleLabels" :key="label.id" + :is-checked="isLabelSelected(label)" + :is-check-centered="true" + :is-check-item="true" data-testid="labels-list" @click.native.capture.stop="handleLabelClick(label)" > - <label-item - :label="label" - :is-label-set="isLabelSelected(label)" - :highlight="index === currentHighlightItem" - /> + <label-item :label="label" /> </gl-dropdown-item> <gl-dropdown-item v-show="showNoMatchingResultsMessage" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue index 7f330147c10..f27f0b4e34c 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue @@ -1,71 +1,21 @@ <script> -import { GlLink, GlIcon } from '@gitlab/ui'; - export default { - functional: true, props: { label: { type: Object, required: true, }, - isLabelSet: { - type: Boolean, - required: true, - }, - highlight: { - type: Boolean, - required: false, - default: false, - }, - }, - render(h, { props }) { - const { label, highlight, isLabelSet } = props; - - const labelColorBox = h('span', { - class: 'dropdown-label-box gl-flex-shrink-0 gl-top-0 gl-mr-3', - style: { - backgroundColor: label.color, - }, - attrs: { - 'data-testid': 'label-color-box', - }, - }); - - const checkedIcon = h(GlIcon, { - class: { - 'gl-mr-3 gl-flex-shrink-0': true, - hidden: !isLabelSet, - }, - props: { - name: 'mobile-issue-close', - }, - }); - - const noIcon = h('span', { - class: { - 'gl-mr-5 gl-pr-3': true, - hidden: isLabelSet, - }, - attrs: { - 'data-testid': 'no-icon', - }, - }); - - const labelTitle = h('span', label.title); - - const labelLink = h(GlLink, [noIcon, checkedIcon, labelColorBox, labelTitle]); - - return h( - 'li', - { - class: { - 'gl-display-block': true, - 'gl-text-left': true, - 'is-focused': highlight, - }, - }, - [labelLink], - ); }, }; </script> + +<template> + <div> + <span + class="dropdown-label-box gl-flex-shrink-0 gl-top-0 gl-mr-3" + :style="{ 'background-color': label.color }" + data-testid="label-color-box" + ></span> + <span>{{ label.title }}</span> + </div> +</template> diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb index 5134b484249..d9bd64f4c2e 100644 --- a/app/helpers/invite_members_helper.rb +++ b/app/helpers/invite_members_helper.rb @@ -9,14 +9,6 @@ module InviteMembersHelper Feature.enabled?(:invite_members_group_modal, project.group) && can?(current_user, :admin_project_member, project) end - def can_invite_group_for_project?(project) - # do not use the can_admin_project_member? helper here due to structure of the view and how membership_locked? - # is leveraged for inviting groups - Feature.enabled?(:invite_members_group_modal, project.group) && - can?(current_user, :admin_project_member, project) && - project.allowed_to_share_with_group? - end - def invite_accepted_notice(member) case member.source when Project diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb index d30d044788e..09fc1ab9d50 100644 --- a/app/helpers/profiles_helper.rb +++ b/app/helpers/profiles_helper.rb @@ -12,10 +12,6 @@ module ProfilesHelper ] end - def selected_commit_email(user) - user.read_attribute(:commit_email) || user.commit_email - end - def attribute_provider_label(attribute) user_synced_attributes_metadata = current_user.user_synced_attributes_metadata if user_synced_attributes_metadata&.synced?(attribute) diff --git a/app/models/concerns/enums/ci/commit_status.rb b/app/models/concerns/enums/ci/commit_status.rb index 16dec5fb081..bffed080d97 100644 --- a/app/models/concerns/enums/ci/commit_status.rb +++ b/app/models/concerns/enums/ci/commit_status.rb @@ -26,6 +26,7 @@ module Enums pipeline_loop_detected: 17, no_matching_runner: 18, # not used anymore, but cannot be deleted because of old data trace_size_exceeded: 19, + environment_creation_failure: 20, insufficient_bridge_permissions: 1_001, downstream_bridge_project_not_found: 1_002, invalid_bridge_trigger: 1_003, diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb index 5f5bbf13f92..f0f85c95ae9 100644 --- a/app/presenters/commit_status_presenter.rb +++ b/app/presenters/commit_status_presenter.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true class CommitStatusPresenter < Gitlab::View::Presenter::Delegated + include ActionView::Helpers::UrlHelper + CALLOUT_FAILURE_MESSAGES = { unknown_failure: 'There is an unknown failure, please try again', script_failure: nil, @@ -27,7 +29,12 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated user_blocked: 'The user who created this job is blocked', ci_quota_exceeded: 'No more CI minutes available', no_matching_runner: 'No matching runner available', - trace_size_exceeded: 'The job log size limit was reached' + trace_size_exceeded: 'The job log size limit was reached', + environment_creation_failure: 'This job could not be executed because it would create an environment with an invalid parameter.' + }.freeze + + TROUBLESHOOTING_DOC = { + environment_creation_failure: { path: 'ci/environments/index', anchor: 'a-deployment-job-failed-with-this-job-could-not-be-executed-because-it-would-create-an-environment-with-an-invalid-parameter-error' } }.freeze private_constant :CALLOUT_FAILURE_MESSAGES @@ -39,7 +46,13 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated end def callout_failure_message - self.class.callout_failure_messages.fetch(failure_reason.to_sym) + message = self.class.callout_failure_messages.fetch(failure_reason.to_sym) + + if doc = TROUBLESHOOTING_DOC[failure_reason.to_sym] + message += " #{link_to('How do I fix it?', help_page_path(doc[:path], anchor: doc[:anchor]))}" + end + + message end end diff --git a/app/views/profiles/_email_settings.html.haml b/app/views/profiles/_email_settings.html.haml index 6691d20c8f7..95306633556 100644 --- a/app/views/profiles/_email_settings.html.haml +++ b/app/views/profiles/_email_settings.html.haml @@ -11,6 +11,6 @@ - commit_email_link_url = help_page_path('user/profile/index', anchor: 'change-the-email-displayed-on-your-commits', target: '_blank') - commit_email_link_start = '<a href="%{url}">'.html_safe % { url: commit_email_link_url } - commit_email_docs_link = s_('Profiles|This email will be used for web based operations, such as edits and merges. %{commit_email_link_start}Learn more%{commit_email_link_end}').html_safe % { commit_email_link_start: commit_email_link_start, commit_email_link_end: '</a>'.html_safe } -= form.select :commit_email, options_for_select(commit_email_select_options(@user), selected: selected_commit_email(@user)), += form.select :commit_email, options_for_select(commit_email_select_options(@user), selected: @user.read_attribute(:commit_email)), { help: commit_email_docs_link }, control_class: 'select2 input-lg', disabled: email_change_disabled diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 0239e408e87..4cd91cc3be6 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -3,7 +3,7 @@ .row.gl-mt-3 .col-lg-12 - - if can_invite_members_for_project?(@project) || can_invite_group_for_project?(@project) + - if can_invite_members_for_project?(@project) .row .col-md-12.col-lg-6.gl-display-flex .gl-flex-direction-column.gl-flex-wrap.align-items-baseline diff --git a/config/feature_flags/development/groups_tokens_optional_encryption.yml b/config/feature_flags/development/groups_tokens_optional_encryption.yml index 25c172422f6..c34cae689c2 100644 --- a/config/feature_flags/development/groups_tokens_optional_encryption.yml +++ b/config/feature_flags/development/groups_tokens_optional_encryption.yml @@ -1,7 +1,7 @@ --- name: groups_tokens_optional_encryption introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/25532 -rollout_issue_url: +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/333862 milestone: '11.9' type: development group: group::runner diff --git a/config/feature_flags/development/projects_tokens_optional_encryption.yml b/config/feature_flags/development/projects_tokens_optional_encryption.yml index c9af986b6b7..6a0fed009ea 100644 --- a/config/feature_flags/development/projects_tokens_optional_encryption.yml +++ b/config/feature_flags/development/projects_tokens_optional_encryption.yml @@ -1,7 +1,7 @@ --- name: projects_tokens_optional_encryption introduced_by_url: https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/25532 -rollout_issue_url: +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/333864 milestone: '11.9' type: development group: group::runner diff --git a/config/feature_flags/development/surface_environment_creation_failure.yml b/config/feature_flags/development/surface_environment_creation_failure.yml new file mode 100644 index 00000000000..315d15dc9b3 --- /dev/null +++ b/config/feature_flags/development/surface_environment_creation_failure.yml @@ -0,0 +1,8 @@ +--- +name: surface_environment_creation_failure +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69537 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340169 +milestone: '14.3' +type: development +group: group::release +default_enabled: false diff --git a/config/feature_flags/development/surface_environment_creation_failure_override.yml b/config/feature_flags/development/surface_environment_creation_failure_override.yml new file mode 100644 index 00000000000..1e81015f259 --- /dev/null +++ b/config/feature_flags/development/surface_environment_creation_failure_override.yml @@ -0,0 +1,8 @@ +--- +name: surface_environment_creation_failure_override +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69537 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340169 +milestone: '14.3' +type: development +group: group::release +default_enabled: false diff --git a/doc/administration/redis/replication_and_failover_external.md b/doc/administration/redis/replication_and_failover_external.md index 234b1aa7fb9..c19e42a5f14 100644 --- a/doc/administration/redis/replication_and_failover_external.md +++ b/doc/administration/redis/replication_and_failover_external.md @@ -349,7 +349,7 @@ or a failover promotes a different **Primary** node. ```yaml production: - url: redis://:redi-password-goes-here@gitlab-redis/ + url: redis://:redis-password-goes-here@gitlab-redis/ sentinels: - host: 10.0.0.1 diff --git a/doc/administration/reference_architectures/10k_users.md b/doc/administration/reference_architectures/10k_users.md index f9397e6dbca..d74da8aa574 100644 --- a/doc/administration/reference_architectures/10k_users.md +++ b/doc/administration/reference_architectures/10k_users.md @@ -1550,7 +1550,7 @@ To configure the Praefect nodes, on each one: # Configure the Consul agent consul['enable'] = true ## Enable service discovery for Prometheus - consul['monitoring_service_discovery'] = true + consul['monitoring_service_discovery'] = true # START user configuration # Please set the real values as explained in Required Information section diff --git a/doc/administration/reference_architectures/1k_users.md b/doc/administration/reference_architectures/1k_users.md index 18e34711953..ea40e150e58 100644 --- a/doc/administration/reference_architectures/1k_users.md +++ b/doc/administration/reference_architectures/1k_users.md @@ -13,7 +13,7 @@ full list of reference architectures, see If you need to serve up to 1,000 users and you don't have strict availability requirements, a single-node solution with [frequent backups](index.md#automated-backups) is appropriate for -many organizations . +many organizations. > - **Supported users (approximate):** 1,000 > - **High Availability:** No. For a highly-available environment, you can diff --git a/doc/api/graphql/audit_report.md b/doc/api/graphql/audit_report.md index ba9967f85f2..76f3da2d6ea 100644 --- a/doc/api/graphql/audit_report.md +++ b/doc/api/graphql/audit_report.md @@ -4,7 +4,7 @@ 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 --- -# Set up an Audit Report with GraphQL +# Set up an Audit Report with GraphQL **(FREE)** This page describes how you can use the GraphiQL explorer to set up an audit report for a specific subset of users. diff --git a/doc/api/graphql/custom_emoji.md b/doc/api/graphql/custom_emoji.md index cb5c0275e08..7307abc0568 100644 --- a/doc/api/graphql/custom_emoji.md +++ b/doc/api/graphql/custom_emoji.md @@ -4,7 +4,7 @@ group: Project Management 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 --- -# Use custom emojis with GraphQL **(FREE SELF)** +# Use custom emojis with GraphQL **(FREE)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37911) in GitLab 13.6 > - [Deployed behind a feature flag](../../user/feature_flags.md), disabled by default. @@ -93,7 +93,7 @@ For more information on: ## Enable or disable custom emoji API **(FREE SELF)** -Custom emoji is under development and but ready for production use. It is +Custom emoji is under development but ready for production use. It is deployed behind a feature flag that is **disabled by default**. [GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md) can enable it. diff --git a/doc/api/graphql/index.md b/doc/api/graphql/index.md index e77e6102594..3523276bdf5 100644 --- a/doc/api/graphql/index.md +++ b/doc/api/graphql/index.md @@ -4,7 +4,7 @@ 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 --- -# GraphQL API +# GraphQL API **(FREE)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/19008) in GitLab 11.0 (enabled by feature flag `graphql`). > - [Always enabled](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/30444) in GitLab 12.1. diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 4622618db42..da2133e1f37 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1437,6 +1437,7 @@ Input type: `DastProfileCreateInput` | ---- | ---- | ----------- | | <a id="mutationdastprofilecreatebranchname"></a>`branchName` | [`String`](#string) | Associated branch. | | <a id="mutationdastprofilecreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationdastprofilecreatedastprofileschedule"></a>`dastProfileSchedule` | [`DastProfileScheduleInput`](#dastprofilescheduleinput) | Represents a DAST Profile Schedule. Results in an error if `dast_on_demand_scans_scheduler` feature flag is disabled. | | <a id="mutationdastprofilecreatedastscannerprofileid"></a>`dastScannerProfileId` | [`DastScannerProfileID!`](#dastscannerprofileid) | ID of the scanner profile to be associated. | | <a id="mutationdastprofilecreatedastsiteprofileid"></a>`dastSiteProfileId` | [`DastSiteProfileID!`](#dastsiteprofileid) | ID of the site profile to be associated. | | <a id="mutationdastprofilecreatedescription"></a>`description` | [`String`](#string) | Description of the profile. Defaults to an empty string. | @@ -8564,6 +8565,7 @@ Represents a DAST Profile. | Name | Type | Description | | ---- | ---- | ----------- | | <a id="dastprofilebranch"></a>`branch` | [`DastProfileBranch`](#dastprofilebranch) | The associated branch. | +| <a id="dastprofiledastprofileschedule"></a>`dastProfileSchedule` | [`DastProfileSchedule`](#dastprofileschedule) | Associated profile schedule. Will always return `null` if `dast_on_demand_scans_scheduler` feature flag is disabled. | | <a id="dastprofiledastscannerprofile"></a>`dastScannerProfile` | [`DastScannerProfile`](#dastscannerprofile) | The associated scanner profile. | | <a id="dastprofiledastsiteprofile"></a>`dastSiteProfile` | [`DastSiteProfile`](#dastsiteprofile) | The associated site profile. | | <a id="dastprofiledescription"></a>`description` | [`String`](#string) | The description of the scan. | @@ -8582,6 +8584,32 @@ Represents a DAST Profile Branch. | <a id="dastprofilebranchexists"></a>`exists` | [`Boolean`](#boolean) | Indicates whether or not the branch exists. | | <a id="dastprofilebranchname"></a>`name` | [`String`](#string) | The name of the branch. | +### `DastProfileCadence` + +Represents DAST Profile Cadence. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="dastprofilecadenceduration"></a>`duration` | [`Int`](#int) | Duration of the DAST profile cadence. | +| <a id="dastprofilecadenceunit"></a>`unit` | [`DastProfileCadenceUnit`](#dastprofilecadenceunit) | Unit for the duration of DAST profile cadence. | + +### `DastProfileSchedule` + +Represents a DAST profile schedule. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="dastprofilescheduleactive"></a>`active` | [`Boolean`](#boolean) | Status of the DAST profile schedule. | +| <a id="dastprofileschedulecadence"></a>`cadence` | [`DastProfileCadence`](#dastprofilecadence) | Cadence of the DAST profile schedule. | +| <a id="dastprofilescheduleid"></a>`id` | [`DastProfileScheduleID!`](#dastprofilescheduleid) | ID of the DAST profile schedule. | +| <a id="dastprofileschedulenextrunat"></a>`nextRunAt` | [`Time`](#time) | Next run time of the DAST profile schedule in the given timezone. | +| <a id="dastprofileschedulestartsat"></a>`startsAt` | [`Time`](#time) | Start time of the DAST profile schedule in the given timezone. | +| <a id="dastprofilescheduletimezone"></a>`timezone` | [`String`](#string) | Time zone of the start time of the DAST profile schedule. | + ### `DastScannerProfile` Represents a DAST scanner profile. @@ -15179,6 +15207,17 @@ Status of a container repository. | <a id="containerrepositorystatusdelete_failed"></a>`DELETE_FAILED` | Delete Failed status. | | <a id="containerrepositorystatusdelete_scheduled"></a>`DELETE_SCHEDULED` | Delete Scheduled status. | +### `DastProfileCadenceUnit` + +Unit for the duration of Dast Profile Cadence. + +| Value | Description | +| ----- | ----------- | +| <a id="dastprofilecadenceunitday"></a>`DAY` | DAST Profile Cadence duration in days. | +| <a id="dastprofilecadenceunitmonth"></a>`MONTH` | DAST Profile Cadence duration in months. | +| <a id="dastprofilecadenceunitweek"></a>`WEEK` | DAST Profile Cadence duration in weeks. | +| <a id="dastprofilecadenceunityear"></a>`YEAR` | DAST Profile Cadence duration in years. | + ### `DastScanTypeEnum` | Value | Description | @@ -16431,6 +16470,12 @@ A `DastProfileID` is a global ID. It is encoded as a string. An example `DastProfileID` is: `"gid://gitlab/Dast::Profile/1"`. +### `DastProfileScheduleID` + +A `DastProfileScheduleID` is a global ID. It is encoded as a string. + +An example `DastProfileScheduleID` is: `"gid://gitlab/Dast::ProfileSchedule/1"`. + ### `DastScannerProfileID` A `DastScannerProfileID` is a global ID. It is encoded as a string. @@ -17396,6 +17441,30 @@ Field that are available while modifying the custom mapping attributes for an HT | <a id="complianceframeworkinputname"></a>`name` | [`String`](#string) | New name for the compliance framework. | | <a id="complianceframeworkinputpipelineconfigurationfullpath"></a>`pipelineConfigurationFullPath` | [`String`](#string) | Full path of the compliance pipeline configuration stored in a project repository, such as `.gitlab/.compliance-gitlab-ci.yml@compliance/hipaa` **(ULTIMATE)**. | +### `DastProfileCadenceInput` + +Represents DAST Profile Cadence. + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="dastprofilecadenceinputduration"></a>`duration` | [`Int`](#int) | Duration of the DAST Profile Cadence. | +| <a id="dastprofilecadenceinputunit"></a>`unit` | [`DastProfileCadenceUnit`](#dastprofilecadenceunit) | Unit for the duration of DAST Profile Cadence. | + +### `DastProfileScheduleInput` + +Input type for DAST Profile Schedules. + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="dastprofilescheduleinputactive"></a>`active` | [`Boolean`](#boolean) | Status of a Dast Profile Schedule. | +| <a id="dastprofilescheduleinputcadence"></a>`cadence` | [`DastProfileCadenceInput`](#dastprofilecadenceinput) | Cadence of a Dast Profile Schedule. | +| <a id="dastprofilescheduleinputstartsat"></a>`startsAt` | [`Time`](#time) | Start time of a Dast Profile Schedule. | +| <a id="dastprofilescheduleinputtimezone"></a>`timezone` | [`String`](#string) | Time Zone for the Start time of a Dast Profile Schedule. | + ### `DastSiteProfileAuthInput` Input type for DastSiteProfile authentication. diff --git a/doc/api/graphql/removed_items.md b/doc/api/graphql/removed_items.md index 1c425d5f1d5..0048148ab11 100644 --- a/doc/api/graphql/removed_items.md +++ b/doc/api/graphql/removed_items.md @@ -12,7 +12,7 @@ According to our [process for removing items](index.md#deprecation-and-removal-p ## GitLab 14.0 -Fields removed in [GitLab 14.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63293): +Fields [removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63293) in GitLab 14.0: ### GraphQL Mutations @@ -38,7 +38,7 @@ Fields removed in [GitLab 14.0](https://gitlab.com/gitlab-org/gitlab/-/merge_req ## GitLab 13.6 -Fields removed in [GitLab 13.6](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44866): +Fields [removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44866) in GitLab 13.6: | Field name | GraphQL type | Deprecated in | Use instead | |----------------------|--------------------------|---------------|----------------------------| diff --git a/doc/api/graphql/users_example.md b/doc/api/graphql/users_example.md index 8fbfb67d166..0658a9402e7 100644 --- a/doc/api/graphql/users_example.md +++ b/doc/api/graphql/users_example.md @@ -4,7 +4,7 @@ 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 --- -# Query users with GraphQL +# Query users with GraphQL **(FREE)** This page describes how you can use the GraphiQL explorer to query users. diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 8875e4daa87..3fba95c1fb3 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -7,7 +7,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # API V3 to API V4 WARNING: -The GitLab API v3 was removed in [GitLab 11.0](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/36819). +The GitLab API v3 was [removed](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/36819) in GitLab 11.0. For information about the current version of the GitLab API, read the [API documentation](index.md). diff --git a/doc/api/version.md b/doc/api/version.md index b23930e70f9..b076993f00e 100644 --- a/doc/api/version.md +++ b/doc/api/version.md @@ -4,7 +4,7 @@ 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 --- -# Version API +# Version API **(FREE)** > Introduced in GitLab 8.13. diff --git a/doc/api/vulnerabilities.md b/doc/api/vulnerabilities.md index b18a837de26..1c6f7a760e6 100644 --- a/doc/api/vulnerabilities.md +++ b/doc/api/vulnerabilities.md @@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Vulnerabilities API **(ULTIMATE)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10242) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.6. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10242) in GitLab 12.6. NOTE: The former Vulnerabilities API was renamed to Vulnerability Findings API diff --git a/doc/api/vulnerability_exports.md b/doc/api/vulnerability_exports.md index efe0ea000a7..9395a4ee5de 100644 --- a/doc/api/vulnerability_exports.md +++ b/doc/api/vulnerability_exports.md @@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Vulnerability export API **(ULTIMATE)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/197494) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10. [Updated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30397) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.0. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/197494) in GitLab 12.10. [Updated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30397) in GitLab 13.0. WARNING: This API is in an alpha stage and considered unstable. diff --git a/doc/api/vulnerability_findings.md b/doc/api/vulnerability_findings.md index c7f045a67a0..dfc6074a1aa 100644 --- a/doc/api/vulnerability_findings.md +++ b/doc/api/vulnerability_findings.md @@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Vulnerability Findings API **(ULTIMATE)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/19029) in GitLab Ultimate 12.5. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/19029) in GitLab 12.5. NOTE: This API resource is renamed from Vulnerabilities to Vulnerability Findings because the Vulnerabilities are reserved diff --git a/doc/ci/environments/index.md b/doc/ci/environments/index.md index f0204180d8a..f7db790925b 100644 --- a/doc/ci/environments/index.md +++ b/doc/ci/environments/index.md @@ -816,3 +816,41 @@ To ensure the `action: stop` can always run when needed, you can: action: stop when: manual ``` + +### A deployment job failed with "This job could not be executed because it would create an environment with an invalid parameter" error + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/21182) in GitLab 14.3. + +FLAG: +On self-managed GitLab, by default this bug fix is not available. To make it available per project or for your entire instance, ask an administrator to [enable the `surface_environment_creation_failure` flag](../../administration/feature_flags.md). On GitLab.com, this bug fix is not available, but will be rolled out shortly. + +If your project is configured to [create a dynamic environment](#create-a-dynamic-environment), +such as a [Review App](../review_apps/index.md), you might encounter this error +because the dynamically generated parameter is invalid for the system. + +For example, if you have the following in your `.gitlab-ci.yml`: + +```yaml +review: + script: deploy review app + environment: review/$CI_COMMIT_REF_NAME +``` + +When you create a new merge request with a branch name `bug-fix!`, +the `review` job tries to create an environment with `review/bug-fix!`. +However, the `!` is an invalid character for environments, so the +deployment job fails since it was about to run without an environment. + +To fix this, you can: + +- Re-create your feature branch without the invalid characters, + such as `bug-fix`. +- Replace the `CI_COMMIT_REF_NAME` + [predefined variable](../variables/predefined_variables.md) with + `CI_COMMIT_REF_SLUG` which strips any invalid characters: + + ```yaml + review: + script: deploy review app + environment: review/$CI_COMMIT_REF_SLUG + ``` diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md index 5df2cb1ec72..c78179e9693 100644 --- a/doc/user/application_security/security_dashboard/index.md +++ b/doc/user/application_security/security_dashboard/index.md @@ -166,22 +166,17 @@ To add projects to the Security Center: After you add projects, the security dashboard and vulnerability report display the vulnerabilities found in those projects' default branches. -## Keeping the dashboards up to date +## Keep dashboards up to date -The Security Dashboard displays information from the results of the most recent -security scan on the [default branch](../../project/repository/branches/default.md), -which means that security scans are performed every time the branch is updated. - -If the default branch is updated infrequently, scans are run infrequently and the -information on the Security Dashboard can become outdated as new vulnerabilities -are discovered. +The Security Dashboard displays results of the most recent security scan on the +[default branch](../../project/repository/branches/default.md). By default, security scans are run +only when the default branch is updated. Information on the Security Dashboard may not reflect +newly-discovered vulnerabilities. To ensure the information on the Security Dashboard is regularly updated, -[configure a scheduled pipeline](../../../ci/pipelines/schedules.md) to run a -daily security scan. This updates the information displayed on the Security -Dashboard regardless of how often the default branch is updated. - -That way, reports are created even if no code change happens. +[configure a scheduled pipeline](../../../ci/pipelines/schedules.md) to run a daily security scan. +This updates the information displayed on the Security Dashboard regardless of how often the default +branch is updated. WARNING: Running Dependency Scanning from a scheduled pipeline might result in false negatives if your @@ -191,12 +186,6 @@ can occur because the dependency version resolved during the scan might differ f resolved when your project was built and released, in a previous pipeline. Java projects can't have lock files. Python projects can have lock files, but GitLab Secure tools don't support them. -## Security scans using Auto DevOps - -When using [Auto DevOps](../../../topics/autodevops/index.md), use -[special environment variables](../../../topics/autodevops/customize.md#cicd-variables) -to configure daily security scans. - <!-- ## Troubleshooting Include any troubleshooting steps that you can foresee. If you know beforehand what issues diff --git a/doc/user/project/integrations/img/zentao_product_id.png b/doc/user/project/integrations/img/zentao_product_id.png Binary files differnew file mode 100644 index 00000000000..b6fb8e1fb1a --- /dev/null +++ b/doc/user/project/integrations/img/zentao_product_id.png diff --git a/doc/user/project/integrations/overview.md b/doc/user/project/integrations/overview.md index 13def74450c..de82fb793f7 100644 --- a/doc/user/project/integrations/overview.md +++ b/doc/user/project/integrations/overview.md @@ -60,6 +60,7 @@ Click on the service links to see further configuration instructions and details | [Unify Circuit](unify_circuit.md) | Receive events notifications. | **{dotted-circle}** No | | [Webex Teams](webex_teams.md) | Receive events notifications. | **{dotted-circle}** No | | [YouTrack](youtrack.md) | Use YouTrack as the issue tracker. | **{dotted-circle}** No | +| [ZenTao](zentao.md) | Use ZenTao as the issue tracker. | **{dotted-circle}** No | ## Push hooks limit diff --git a/doc/user/project/integrations/zentao.md b/doc/user/project/integrations/zentao.md new file mode 100644 index 00000000000..ab8a7829139 --- /dev/null +++ b/doc/user/project/integrations/zentao.md @@ -0,0 +1,40 @@ +--- +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 +--- + +# ZenTao product integration **(PREMIUM)** + +[ZenTao](https://www.zentao.net/) is a web-based project management platform. + +## Configure ZenTao + +This integration requires a ZenTao API secret key. + +Complete these steps in ZenTao: + +1. Go to your **Admin** page and select **Develop > Application**. +1. Select **Add Application**. +1. Under **Name** and **Code**, enter a name and a code for the new secret key. +1. Under **Account**, select an existing account name. +1. Select **Save**. +1. Copy the generated key to use in GitLab. + +## Configure GitLab + +Complete these steps in GitLab: + +1. Go to your project and select **Settings > Integrations**. +1. Select **ZenTao**. +1. Turn on the **Active** toggle under **Enable Integration**. +1. Provide the ZenTao configuration information: + - **ZenTao Web URL**: The base URL of the ZenTao instance web interface you're linking to this GitLab project (for example, `example.zentao.net`). + - **ZenTao API URL** (optional): The base URL to the ZenTao instance API. Defaults to the Web URL value if not set. + - **ZenTao API token**: Use the key you generated when you [configured ZenTao](#configure-zentao). + - **ZenTao Product ID**: To display issues from a single ZenTao product in a given GitLab project. The Product ID can be found in the ZenTao product page under **Settings > Overview**. + + ![ZenTao settings page](img/zentao_product_id.png) + +1. To verify the ZenTao connection is working, select **Test settings**. +1. Select **Save changes**. diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb index 934bf22d8ad..9ad5d6538b7 100644 --- a/lib/gitlab/ci/pipeline/seed/build.rb +++ b/lib/gitlab/ci/pipeline/seed/build.rb @@ -106,10 +106,15 @@ module Gitlab environment = Seed::Environment.new(build).to_resource - # If there is a validation error on environment creation, such as - # the name contains invalid character, the build falls back to a - # non-environment job. unless environment.persisted? + if Feature.enabled?(:surface_environment_creation_failure, build.project, default_enabled: :yaml) && + Feature.disabled?(:surface_environment_creation_failure_override, build.project) + return { status: :failed, failure_reason: :environment_creation_failure } + end + + # If there is a validation error on environment creation, such as + # the name contains invalid character, the build falls back to a + # non-environment job. Gitlab::ErrorTracking.track_exception( EnvironmentCreationFailure.new, project_id: build.project_id, diff --git a/lib/gitlab/ci/status/build/failed.rb b/lib/gitlab/ci/status/build/failed.rb index dbbb9a01dab..b9525ce97a8 100644 --- a/lib/gitlab/ci/status/build/failed.rb +++ b/lib/gitlab/ci/status/build/failed.rb @@ -32,7 +32,8 @@ module Gitlab user_blocked: 'pipeline user was blocked', ci_quota_exceeded: 'no more CI minutes available', no_matching_runner: 'no matching runner available', - trace_size_exceeded: 'log size limit exceeded' + trace_size_exceeded: 'log size limit exceeded', + environment_creation_failure: 'environment creation failure' }.freeze private_constant :REASONS diff --git a/qa/Gemfile b/qa/Gemfile index d3d0fe95e49..cc2355cdfa3 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -24,6 +24,7 @@ gem 'rspec-parameterized', '~> 0.4.2' gem 'octokit', '~> 4.21' gem 'webdrivers', '~> 4.6' gem 'zeitwerk', '~> 2.4' +gem 'influxdb-client', '~> 1.17' gem 'chemlab', '~> 0.7' gem 'chemlab-library-www-gitlab-com', '~> 0.1' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 1df4550a2ba..5f33afaa77b 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -88,6 +88,7 @@ GEM i18n (1.8.10) concurrent-ruby (~> 1.0) ice_nine (0.11.2) + influxdb-client (1.17.0) knapsack (1.17.1) rake launchy (2.4.3) @@ -220,6 +221,7 @@ DEPENDENCIES deprecation_toolkit (~> 1.5.1) faker (~> 2.19, >= 2.19.0) gitlab-qa + influxdb-client (~> 1.17) knapsack (~> 1.17) octokit (~> 4.21) parallel (~> 1.19) diff --git a/qa/qa/resource/group_base.rb b/qa/qa/resource/group_base.rb index b937b704613..a1e5b19f409 100644 --- a/qa/qa/resource/group_base.rb +++ b/qa/qa/resource/group_base.rb @@ -30,6 +30,22 @@ module QA end end + # Get group milestones + # + # @return [Array<QA::Resource::GroupMilestone>] + def milestones + parse_body(api_get_from("#{api_get_path}/milestones")).map do |milestone| + GroupMilestone.init do |resource| + resource.api_client = api_client + resource.group = self + resource.id = milestone[:id] + resource.iid = milestone[:iid] + resource.title = milestone[:title] + resource.description = milestone[:description] + end + end + end + # API get path # # @return [String] diff --git a/qa/qa/resource/group_milestone.rb b/qa/qa/resource/group_milestone.rb index 1fb07fdbd0b..880ca2b9721 100644 --- a/qa/qa/resource/group_milestone.rb +++ b/qa/qa/resource/group_milestone.rb @@ -3,11 +3,14 @@ module QA module Resource class GroupMilestone < Base - attr_writer :start_date, :due_date - - attribute :id - attribute :title - attribute :description + attributes :id, + :iid, + :title, + :description, + :start_date, + :due_date, + :updated_at, + :created_at attribute :group do Group.fabricate_via_api! do |resource| @@ -20,6 +23,21 @@ module QA @description = "My awesome group milestone." end + def fabricate! + group.visit! + + Page::Group::Menu.perform(&:go_to_milestones) + Page::Group::Milestone::Index.perform(&:click_new_milestone_link) + + Page::Group::Milestone::New.perform do |new_milestone| + new_milestone.set_title(@title) + new_milestone.set_description(@description) + new_milestone.set_start_date(@start_date) if @start_date + new_milestone.set_due_date(@due_date) if @due_date + new_milestone.click_create_milestone_button + end + end + def api_get_path "/groups/#{group.id}/milestones/#{id}" end @@ -38,19 +56,36 @@ module QA end end - def fabricate! - group.visit! + # Object comparison + # + # @param [QA::Resource::GroupMilestone] other + # @return [Boolean] + def ==(other) + other.is_a?(GroupMilestone) && comparable_milestone == other.comparable_milestone + end - Page::Group::Menu.perform(&:go_to_milestones) - Page::Group::Milestone::Index.perform(&:click_new_milestone_link) + # Override inspect for a better rspec failure diff output + # + # @return [String] + def inspect + JSON.pretty_generate(comparable_milestone) + end - Page::Group::Milestone::New.perform do |new_milestone| - new_milestone.set_title(@title) - new_milestone.set_description(@description) - new_milestone.set_start_date(@start_date) if @start_date - new_milestone.set_due_date(@due_date) if @due_date - new_milestone.click_create_milestone_button - end + protected + + # Return subset of fields for comparing milestones + # + # @return [Hash] + def comparable_milestone + reload! unless api_response + + api_response.slice( + :title, + :description, + :state, + :due_date, + :start_date + ) end end end diff --git a/qa/qa/resource/label_base.rb b/qa/qa/resource/label_base.rb index 8f6534cb451..b1af0e23561 100644 --- a/qa/qa/resource/label_base.rb +++ b/qa/qa/resource/label_base.rb @@ -64,9 +64,9 @@ module QA JSON.pretty_generate(comparable_label) end - # protected + protected - # Return subset of fields for comparing groups + # Return subset of fields for comparing labels # # @return [Hash] def comparable_label diff --git a/qa/qa/runtime/allure_report.rb b/qa/qa/runtime/allure_report.rb index 0630e9d333c..5b0456dc607 100644 --- a/qa/qa/runtime/allure_report.rb +++ b/qa/qa/runtime/allure_report.rb @@ -73,7 +73,7 @@ module QA def configure_rspec RSpec.configure do |config| config.add_formatter(AllureRspecFormatter) - config.add_formatter(QA::Support::AllureMetadataFormatter) + config.add_formatter(QA::Support::Formatters::AllureMetadataFormatter) end end diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index 16c8c4aff3e..cdfa95457c7 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -403,6 +403,10 @@ module QA ENV['GITLAB_TLS_CERTIFICATE'] end + def export_metrics? + running_in_ci? && enabled?(ENV['QA_EXPORT_TEST_METRICS'], default: true) + end + private def remote_grid_credentials diff --git a/qa/qa/specs/features/api/1_manage/bulk_import_group_spec.rb b/qa/qa/specs/features/api/1_manage/bulk_import_group_spec.rb index 925d50a6639..1422dd5a029 100644 --- a/qa/qa/specs/features/api/1_manage/bulk_import_group_spec.rb +++ b/qa/qa/specs/features/api/1_manage/bulk_import_group_spec.rb @@ -5,6 +5,7 @@ module QA describe 'Bulk group import' do let!(:staging?) { Runtime::Scenario.gitlab_address.include?('staging.gitlab.com') } + let(:import_wait_duration) { { max_duration: 300, sleep_interval: 2 } } let(:admin_api_client) { Runtime::API::Client.as_admin } let(:user) do Resource::User.fabricate_via_api! do |usr| @@ -14,7 +15,6 @@ module QA end let(:api_client) { Runtime::API::Client.new(user: user) } - let(:personal_access_token) { api_client.personal_access_token } let(:sandbox) do Resource::Sandbox.fabricate_via_api! do |group| @@ -29,22 +29,6 @@ module QA end end - let(:subgroup) do - Resource::Group.fabricate_via_api! do |group| - group.api_client = api_client - group.sandbox = source_group - group.path = "subgroup-for-import-#{SecureRandom.hex(4)}" - end - end - - let(:imported_subgroup) do - Resource::Group.init do |group| - group.api_client = api_client - group.sandbox = imported_group - group.path = subgroup.path - end - end - let(:imported_group) do Resource::BulkImportGroup.fabricate_via_api! do |group| group.api_client = api_client @@ -57,33 +41,79 @@ module QA Runtime::Feature.enable(:top_level_group_creation_enabled) if staging? sandbox.add_member(user, Resource::Members::AccessLevel::MAINTAINER) + end - Resource::GroupLabel.fabricate_via_api! do |label| - label.api_client = api_client - label.group = source_group - label.title = "source-group-#{SecureRandom.hex(4)}" + context 'with subgroups and labels' do + let(:subgroup) do + Resource::Group.fabricate_via_api! do |group| + group.api_client = api_client + group.sandbox = source_group + group.path = "subgroup-for-import-#{SecureRandom.hex(4)}" + end end - Resource::GroupLabel.fabricate_via_api! do |label| - label.api_client = api_client - label.group = subgroup - label.title = "subgroup-#{SecureRandom.hex(4)}" + + let(:imported_subgroup) do + Resource::Group.init do |group| + group.api_client = api_client + group.sandbox = imported_group + group.path = subgroup.path + end + end + + before do + Resource::GroupLabel.fabricate_via_api! do |label| + label.api_client = api_client + label.group = source_group + label.title = "source-group-#{SecureRandom.hex(4)}" + end + Resource::GroupLabel.fabricate_via_api! do |label| + label.api_client = api_client + label.group = subgroup + label.title = "subgroup-#{SecureRandom.hex(4)}" + end + end + + it( + 'successfully imports groups and labels', + testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1873' + ) do + expect { imported_group.import_status }.to eventually_eq('finished').within(import_wait_duration) + + aggregate_failures do + expect(imported_group.reload!).to eq(source_group) + expect(imported_group.labels).to include(*source_group.labels) + + expect(imported_subgroup.reload!).to eq(subgroup) + expect(imported_subgroup.labels).to include(*subgroup.labels) + end end end - it( - 'imports group with subgroups and labels', - testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1873' - ) do - expect { imported_group.import_status }.to( - eventually_eq('finished').within(max_duration: 300, sleep_interval: 2) - ) + context 'with milestones' do + let(:source_milestone) do + Resource::GroupMilestone.fabricate_via_api! do |milestone| + milestone.api_client = api_client + milestone.group = source_group + end + end + + before do + source_milestone + end - aggregate_failures do - expect(imported_group.reload!).to eq(source_group) - expect(imported_group.labels).to include(*source_group.labels) + it( + 'successfully imports group milestones', + testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/2245' + ) do + expect { imported_group.import_status }.to eventually_eq('finished').within(import_wait_duration) - expect(imported_subgroup.reload!).to eq(subgroup) - expect(imported_subgroup.labels).to include(*subgroup.labels) + imported_milestone = imported_group.reload!.milestones.find { |ml| ml.title == source_milestone.title } + aggregate_failures do + expect(imported_milestone).to eq(source_milestone) + expect(imported_milestone.iid).to eq(source_milestone.iid) + expect(imported_milestone.created_at).to eq(source_milestone.created_at) + expect(imported_milestone.updated_at).to eq(source_milestone.updated_at) + end end end diff --git a/qa/qa/specs/helpers/rspec.rb b/qa/qa/specs/helpers/rspec.rb index 853dfbfd1b6..3e97dbd118a 100644 --- a/qa/qa/specs/helpers/rspec.rb +++ b/qa/qa/specs/helpers/rspec.rb @@ -19,13 +19,22 @@ module QA # expanding into the global state # See: https://github.com/rspec/rspec-core/issues/2603 def describe_successfully(*args, &describe_body) - reporter = ::RSpec.configuration.reporter - - example_group = RSpec.describe(*args, &describe_body) + example_group = ::RSpec.describe(*args, &describe_body) ran_successfully = example_group.run reporter expect(ran_successfully).to eq true example_group end + + def send_stop_notification + reporter.notify( + :stop, + ::RSpec::Core::Notifications::ExamplesNotification.new(reporter) + ) + end + + def reporter + ::RSpec.configuration.reporter + end end end end diff --git a/qa/qa/support/allure_metadata_formatter.rb b/qa/qa/support/allure_metadata_formatter.rb deleted file mode 100644 index 98b7077b0ae..00000000000 --- a/qa/qa/support/allure_metadata_formatter.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -require 'rspec/core' -require "rspec/core/formatters/base_formatter" - -module QA - module Support - class AllureMetadataFormatter < ::RSpec::Core::Formatters::BaseFormatter - ::RSpec::Core::Formatters.register( - self, - :example_started - ) - - # Starts example - # @param [RSpec::Core::Notifications::ExampleNotification] example_notification - # @return [void] - def example_started(example_notification) - example = example_notification.example - - quarantine_issue = example.metadata.dig(:quarantine, :issue) - example.issue('Quarantine issue', quarantine_issue) if quarantine_issue - - spec_file = example.file_path.split('/').last - example.issue( - 'Failure issues', - "https://gitlab.com/gitlab-org/gitlab/-/issues?scope=all&state=opened&search=#{spec_file}" - ) - return unless Runtime::Env.running_in_ci? - - example.add_link(name: "Job(#{Runtime::Env.ci_job_name})", url: Runtime::Env.ci_job_url) - end - end - end -end diff --git a/qa/qa/support/formatters/allure_metadata_formatter.rb b/qa/qa/support/formatters/allure_metadata_formatter.rb new file mode 100644 index 00000000000..10769ba5c57 --- /dev/null +++ b/qa/qa/support/formatters/allure_metadata_formatter.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module QA + module Support + module Formatters + class AllureMetadataFormatter < ::RSpec::Core::Formatters::BaseFormatter + ::RSpec::Core::Formatters.register( + self, + :example_started + ) + + # Starts example + # @param [RSpec::Core::Notifications::ExampleNotification] example_notification + # @return [void] + def example_started(example_notification) + example = example_notification.example + + quarantine_issue = example.metadata.dig(:quarantine, :issue) + example.issue('Quarantine issue', quarantine_issue) if quarantine_issue + + spec_file = example.file_path.split('/').last + example.issue( + 'Failure issues', + "https://gitlab.com/gitlab-org/gitlab/-/issues?scope=all&state=opened&search=#{spec_file}" + ) + return unless Runtime::Env.running_in_ci? + + example.add_link(name: "Job(#{Runtime::Env.ci_job_name})", url: Runtime::Env.ci_job_url) + end + end + end + end +end diff --git a/qa/qa/specs/helpers/context_formatter.rb b/qa/qa/support/formatters/context_formatter.rb index 26db7c3b67e..c8991561f45 100644 --- a/qa/qa/specs/helpers/context_formatter.rb +++ b/qa/qa/support/formatters/context_formatter.rb @@ -1,13 +1,10 @@ # frozen_string_literal: true -require 'rspec/core' -require "rspec/core/formatters/base_formatter" - module QA - module Specs - module Helpers + module Support + module Formatters class ContextFormatter < ::RSpec::Core::Formatters::BaseFormatter - include ContextSelector + include Specs::Helpers::ContextSelector ::RSpec::Core::Formatters.register( self, diff --git a/qa/qa/support/formatters/formatters.rb b/qa/qa/support/formatters/formatters.rb new file mode 100644 index 00000000000..f0abf98001f --- /dev/null +++ b/qa/qa/support/formatters/formatters.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'rspec/core' +require 'rspec/core/formatters/base_formatter' + +module QA + module Support + module Formatters + end + end +end diff --git a/qa/qa/specs/helpers/quarantine_formatter.rb b/qa/qa/support/formatters/quarantine_formatter.rb index c42debee07c..c5d16988dbd 100644 --- a/qa/qa/specs/helpers/quarantine_formatter.rb +++ b/qa/qa/support/formatters/quarantine_formatter.rb @@ -1,13 +1,10 @@ # frozen_string_literal: true -require 'rspec/core' -require "rspec/core/formatters/base_formatter" - module QA - module Specs - module Helpers + module Support + module Formatters class QuarantineFormatter < ::RSpec::Core::Formatters::BaseFormatter - include Quarantine + include Specs::Helpers::Quarantine ::RSpec::Core::Formatters.register( self, diff --git a/qa/qa/support/formatters/test_stats_formatter.rb b/qa/qa/support/formatters/test_stats_formatter.rb new file mode 100644 index 00000000000..91fdc902e2d --- /dev/null +++ b/qa/qa/support/formatters/test_stats_formatter.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +module QA + module Support + module Formatters + class TestStatsFormatter < RSpec::Core::Formatters::BaseFormatter + RSpec::Core::Formatters.register(self, :stop) + + # Finish test execution + # + # @param [RSpec::Core::Notifications::ExamplesNotification] notification + # @return [void] + def stop(notification) + return log(:warn, 'Missing QA_INFLUXDB_URL, skipping metrics export!') unless influxdb_url + return log(:warn, 'Missing QA_INFLUXDB_TOKEN, skipping metrics export!') unless influxdb_token + + data = notification.examples.map { |example| test_stats(example) }.compact + influx_client.create_write_api.write(data: data) + log(:info, "Pushed #{data.length} entries to influxdb") + rescue StandardError => e + log(:error, "Failed to push data to influxdb, error: #{e}") + end + + private + + # InfluxDb client + # + # @return [InfluxDB2::Client] + def influx_client + @influx_client ||= InfluxDB2::Client.new( + influxdb_url, + influxdb_token, + bucket: 'e2e-test-stats', + org: 'gitlab-qa', + use_ssl: false, + precision: InfluxDB2::WritePrecision::NANOSECOND + ) + end + + # InfluxDb instance url + # + # @return [String] + def influxdb_url + @influxdb_url ||= ENV['QA_INFLUXDB_URL'] + end + + # Influxdb token + # + # @return [String] + def influxdb_token + @influxdb_token ||= ENV['QA_INFLUXDB_TOKEN'] + end + + # Transform example to influxdb compatible metrics data + # https://github.com/influxdata/influxdb-client-ruby#data-format + # + # @param [RSpec::Core::Example] example + # @return [Hash] + def test_stats(example) + { + name: 'test-stats', + time: time, + tags: { + name: example.full_description, + file_path: example.metadata[:file_path].gsub('./qa/specs/features', ''), + status: example.execution_result.status, + reliable: example.metadata.key?(:reliable).to_s, + quarantined: example.metadata.key?(:quarantine).to_s, + retried: ((example.metadata[:retry_attempts] || 0) > 0).to_s, + job_name: job_name, + merge_request: merge_request, + run_type: ENV['QA_RUN_TYPE'] + }, + fields: { + id: example.id, + run_time: (example.execution_result.run_time * 1000).round, + retry_attempts: example.metadata[:retry_attempts] || 0, + job_url: QA::Runtime::Env.ci_job_url, + pipeline_id: ENV['CI_PIPELINE_ID'] + } + } + rescue StandardError => e + log(:error, "Failed to transform example '#{example.id}', error: #{e}") + nil + end + + # Single common timestamp for all exported example metrics to keep data points consistently grouped + # + # @return [Time] + def time + @time ||= DateTime.strptime(ENV['CI_PIPELINE_CREATED_AT']).to_time + end + + # Is a merge request execution + # + # @return [String] + def merge_request + @merge_request ||= (!!ENV['CI_MERGE_REQUEST_IID'] || !!ENV['TOP_UPSTREAM_MERGE_REQUEST_IID']).to_s + end + + # Base ci job name + # + # @return [String] + def job_name + @job_name ||= QA::Runtime::Env.ci_job_name.gsub(%r{ \d{1,2}/\d{1,2}}, '') + end + + # Print log message + # + # @param [Symbol] level + # @param [String] message + # @return [void] + def log(level, message) + QA::Runtime::Logger.public_send(level, "influxdb exporter: #{message}") + end + end + end + end +end diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb index 6bc889ebc49..4f0f93bf020 100644 --- a/qa/spec/spec_helper.rb +++ b/qa/spec/spec_helper.rb @@ -16,17 +16,15 @@ QA::Runtime::Browser.configure! QA::Runtime::AllureReport.configure! QA::Runtime::Scenario.from_env(QA::Runtime::Env.runtime_scenario_attributes) -Dir[::File.join(__dir__, "support/helpers/*.rb")].sort.each { |f| require f } -Dir[::File.join(__dir__, "support/matchers/*.rb")].sort.each { |f| require f } -Dir[::File.join(__dir__, "support/shared_contexts/*.rb")].sort.each { |f| require f } Dir[::File.join(__dir__, "support/shared_examples/*.rb")].sort.each { |f| require f } RSpec.configure do |config| config.include QA::Support::Matchers::EventuallyMatcher config.include QA::Support::Matchers::HaveMatcher - config.add_formatter QA::Specs::Helpers::ContextFormatter - config.add_formatter QA::Specs::Helpers::QuarantineFormatter + config.add_formatter QA::Support::Formatters::ContextFormatter + config.add_formatter QA::Support::Formatters::QuarantineFormatter + config.add_formatter QA::Support::Formatters::TestStatsFormatter if QA::Runtime::Env.export_metrics? config.before do |example| QA::Runtime::Logger.debug("\nStarting test: #{example.full_description}\n") diff --git a/qa/spec/specs/allure_report_spec.rb b/qa/spec/specs/allure_report_spec.rb index d17fb8e41d0..34116ca6cbd 100644 --- a/qa/spec/specs/allure_report_spec.rb +++ b/qa/spec/specs/allure_report_spec.rb @@ -68,7 +68,8 @@ describe QA::Runtime::AllureReport do it 'adds rspec and metadata formatter' do expect(rspec_config).to have_received(:add_formatter).with(AllureRspecFormatter).ordered - expect(rspec_config).to have_received(:add_formatter).with(QA::Support::AllureMetadataFormatter).ordered + expect(rspec_config).to have_received(:add_formatter) + .with(QA::Support::Formatters::AllureMetadataFormatter).ordered end it 'configures screenshot saving' do diff --git a/qa/spec/specs/helpers/context_selector_spec.rb b/qa/spec/specs/helpers/context_selector_spec.rb index 1492008972d..0152fee6f5b 100644 --- a/qa/spec/specs/helpers/context_selector_spec.rb +++ b/qa/spec/specs/helpers/context_selector_spec.rb @@ -10,7 +10,7 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do QA::Runtime::Scenario.define(:gitlab_address, 'https://staging.gitlab.com') RSpec::Core::Sandbox.sandboxed do |config| - config.formatter = QA::Specs::Helpers::ContextFormatter + config.formatter = QA::Support::Formatters::ContextFormatter # If there is an example-within-an-example, we want to make sure the inner example # does not get a reference to the outer example (the real spec) if it calls diff --git a/qa/spec/specs/helpers/quarantine_spec.rb b/qa/spec/specs/helpers/quarantine_spec.rb index f064aa0b9af..8ea375cdb05 100644 --- a/qa/spec/specs/helpers/quarantine_spec.rb +++ b/qa/spec/specs/helpers/quarantine_spec.rb @@ -8,7 +8,7 @@ RSpec.describe QA::Specs::Helpers::Quarantine do around do |ex| RSpec::Core::Sandbox.sandboxed do |config| - config.formatter = QA::Specs::Helpers::QuarantineFormatter + config.formatter = QA::Support::Formatters::QuarantineFormatter # If there is an example-within-an-example, we want to make sure the inner example # does not get a reference to the outer example (the real spec) if it calls diff --git a/qa/spec/support/allure_metadata_formatter_spec.rb b/qa/spec/support/formatters/allure_metadata_formatter_spec.rb index f01e5c9f5f8..631d2eda54f 100644 --- a/qa/spec/support/allure_metadata_formatter_spec.rb +++ b/qa/spec/support/formatters/allure_metadata_formatter_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Support::AllureMetadataFormatter do +describe QA::Support::Formatters::AllureMetadataFormatter do include QA::Support::Helpers::StubEnv let(:formatter) { described_class.new(StringIO.new) } diff --git a/qa/spec/support/formatters/test_stats_formatter_spec.rb b/qa/spec/support/formatters/test_stats_formatter_spec.rb new file mode 100644 index 00000000000..30ffa5839e6 --- /dev/null +++ b/qa/spec/support/formatters/test_stats_formatter_spec.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +require 'rspec/core/sandbox' + +describe QA::Support::Formatters::TestStatsFormatter do + include QA::Support::Helpers::StubEnv + include QA::Specs::Helpers::RSpec + + let(:url) { "http://influxdb.net" } + let(:token) { "token" } + let(:ci_timestamp) { "2021-02-23T20:58:41Z" } + let(:ci_job_name) { "test-job 1/5" } + let(:ci_job_url) { "url" } + let(:ci_pipeline_id) { "123" } + let(:run_type) { 'staging-full' } + let(:influx_client) { instance_double('InfluxDB2::Client', create_write_api: influx_write_api) } + let(:influx_write_api) { instance_double('InfluxDB2::WriteApi', write: nil) } + + let(:influx_client_args) do + { + bucket: 'e2e-test-stats', + org: 'gitlab-qa', + use_ssl: false, + precision: InfluxDB2::WritePrecision::NANOSECOND + } + end + + let(:data) do + { + name: 'test-stats', + time: DateTime.strptime(ci_timestamp).to_time, + tags: { + name: "stats export #{spec_name}", + file_path: './spec/support/formatters/test_stats_formatter_spec.rb', + status: :passed, + reliable: reliable, + quarantined: quarantined, + retried: "false", + job_name: "test-job", + merge_request: "false", + run_type: run_type + }, + fields: { + id: './spec/support/formatters/test_stats_formatter_spec.rb[1:1]', + run_time: 0, + retry_attempts: 0, + job_url: ci_job_url, + pipeline_id: ci_pipeline_id + } + } + end + + def run_spec(&spec) + describe_successfully('stats export', &spec) + send_stop_notification + end + + around do |example| + RSpec::Core::Sandbox.sandboxed do |config| + config.formatter = QA::Support::Formatters::TestStatsFormatter + + config.before(:context) { RSpec.current_example = nil } + + example.run + end + end + + before do + allow(InfluxDB2::Client).to receive(:new).with(url, token, **influx_client_args) { influx_client } + end + + context "without influxdb variables configured" do + it "skips export without influxdb url" do + stub_env('QA_INFLUXDB_URL', nil) + stub_env('QA_INFLUXDB_TOKEN', nil) + + run_spec do + it('skips export') {} + end + + expect(influx_client).not_to have_received(:create_write_api) + end + + it "skips export without influxdb token" do + stub_env('QA_INFLUXDB_URL', url) + stub_env('QA_INFLUXDB_TOKEN', nil) + + run_spec do + it('skips export') {} + end + + expect(influx_client).not_to have_received(:create_write_api) + end + end + + context 'with influxdb variables configured' do + let(:spec_name) { 'exports data' } + let(:run_type) { ci_job_name.gsub(%r{ \d{1,2}/\d{1,2}}, '') } + + before do + stub_env('QA_INFLUXDB_URL', url) + stub_env('QA_INFLUXDB_TOKEN', token) + stub_env('CI_PIPELINE_CREATED_AT', ci_timestamp) + stub_env('CI_JOB_URL', ci_job_url) + stub_env('CI_JOB_NAME', ci_job_name) + stub_env('CI_PIPELINE_ID', ci_pipeline_id) + stub_env('CI_MERGE_REQUEST_IID', nil) + stub_env('TOP_UPSTREAM_MERGE_REQUEST_IID', nil) + stub_env('QA_RUN_TYPE', run_type) + end + + context 'with reliable spec' do + let(:reliable) { 'true' } + let(:quarantined) { 'false' } + + it 'exports data to influxdb' do + run_spec do + it('exports data', :reliable) {} + end + + expect(influx_write_api).to have_received(:write).with(data: [data]) + end + end + + context 'with quarantined spec' do + let(:reliable) { 'false' } + let(:quarantined) { 'true' } + + it 'exports data to influxdb' do + run_spec do + it('exports data', :quarantine) {} + end + + expect(influx_write_api).to have_received(:write).with(data: [data]) + end + end + end +end diff --git a/spec/features/issues/resource_label_events_spec.rb b/spec/features/issues/resource_label_events_spec.rb index 33edf2f0b63..e08410efc0b 100644 --- a/spec/features/issues/resource_label_events_spec.rb +++ b/spec/features/issues/resource_label_events_spec.rb @@ -38,7 +38,7 @@ RSpec.describe 'List issue resource label events', :js do click_on 'Edit' wait_for_requests - labels.each { |label| click_link label } + labels.each { |label| click_on label } send_keys(:escape) wait_for_requests diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb index 3153bef966e..454fd50ac24 100644 --- a/spec/features/profiles/user_edit_profile_spec.rb +++ b/spec/features/profiles/user_edit_profile_spec.rb @@ -53,6 +53,19 @@ RSpec.describe 'User edit profile' do expect(page).to have_content('Profile was successfully updated') end + it 'does not set secondary emails without user input' do + fill_in 'user_organization', with: 'GitLab' + submit_settings + + user.reload + expect(page).to have_field('user_commit_email', with: '') + expect(page).to have_field('user_public_email', with: '') + + User::SECONDARY_EMAIL_ATTRIBUTES.each do |attribute| + expect(user.read_attribute(attribute)).to be_blank + end + end + it 'shows an error if the full name contains an emoji', :js do simulate_input('#user_name', 'Martin 😀') submit_settings diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/label_item_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/label_item_spec.js index 23810339833..6e8841411a2 100644 --- a/spec/frontend/vue_shared/components/sidebar/labels_select_widget/label_item_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/labels_select_widget/label_item_spec.js @@ -1,4 +1,3 @@ -import { GlIcon, GlLink } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue'; @@ -6,16 +5,10 @@ import { mockRegularLabel } from './mock_data'; const mockLabel = { ...mockRegularLabel, set: true }; -const createComponent = ({ - label = mockLabel, - isLabelSet = mockLabel.set, - highlight = true, -} = {}) => +const createComponent = ({ label = mockLabel } = {}) => shallowMount(LabelItem, { propsData: { label, - isLabelSet, - highlight, }, }); @@ -31,45 +24,6 @@ describe('LabelItem', () => { }); describe('template', () => { - it('renders gl-link component', () => { - expect(wrapper.find(GlLink).exists()).toBe(true); - }); - - it('renders component root with class `is-focused` when `highlight` prop is true', () => { - const wrapperTemp = createComponent({ - highlight: true, - }); - - expect(wrapperTemp.classes()).toContain('is-focused'); - - wrapperTemp.destroy(); - }); - - it('renders visible gl-icon component when `isLabelSet` prop is true', () => { - const wrapperTemp = createComponent({ - isLabelSet: true, - }); - - const iconEl = wrapperTemp.find(GlIcon); - - expect(iconEl.isVisible()).toBe(true); - expect(iconEl.props('name')).toBe('mobile-issue-close'); - - wrapperTemp.destroy(); - }); - - it('renders visible span element as placeholder instead of gl-icon when `isLabelSet` prop is false', () => { - const wrapperTemp = createComponent({ - isLabelSet: false, - }); - - const placeholderEl = wrapperTemp.find('[data-testid="no-icon"]'); - - expect(placeholderEl.isVisible()).toBe(true); - - wrapperTemp.destroy(); - }); - it('renders label color element', () => { const colorEl = wrapper.find('[data-testid="label-color-box"]'); diff --git a/spec/helpers/profiles_helper_spec.rb b/spec/helpers/profiles_helper_spec.rb index c015937c299..c3a3c2a0178 100644 --- a/spec/helpers/profiles_helper_spec.rb +++ b/spec/helpers/profiles_helper_spec.rb @@ -24,20 +24,6 @@ RSpec.describe ProfilesHelper do end end - describe '#selected_commit_email' do - let(:user) { create(:user) } - - it 'returns main email when commit email attribute is nil' do - expect(helper.selected_commit_email(user)).to eq(user.email) - end - - it 'returns DB stored commit_email' do - user.update!(commit_email: Gitlab::PrivateCommitEmail::TOKEN) - - expect(helper.selected_commit_email(user)).to eq(Gitlab::PrivateCommitEmail::TOKEN) - end - end - describe '#email_provider_label' do it "returns nil for users without external email" do user = create(:user) diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 0c28515b574..3aa6b2e3c05 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -440,17 +440,30 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do context 'when the environment name is invalid' do let(:attributes) { { name: 'deploy', ref: 'master', environment: '!!!' } } - it_behaves_like 'non-deployment job' - it_behaves_like 'ensures environment inexistence' + it 'fails the job with a failure reason and does not create an environment' do + expect(subject).to be_failed + expect(subject).to be_environment_creation_failure + expect(subject.metadata.expanded_environment_name).to be_nil + expect(Environment.exists?(name: expected_environment_name)).to eq(false) + end + + context 'when surface_environment_creation_failure feature flag is disabled' do + before do + stub_feature_flags(surface_environment_creation_failure: false) + end - it 'tracks an exception' do - expect(Gitlab::ErrorTracking).to receive(:track_exception) - .with(an_instance_of(described_class::EnvironmentCreationFailure), - project_id: project.id, - reason: %q{Name can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.', and spaces, but it cannot start or end with '/'}) - .once + it_behaves_like 'non-deployment job' + it_behaves_like 'ensures environment inexistence' - subject + it 'tracks an exception' do + expect(Gitlab::ErrorTracking).to receive(:track_exception) + .with(an_instance_of(described_class::EnvironmentCreationFailure), + project_id: project.id, + reason: %q{Name can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.', and spaces, but it cannot start or end with '/'}) + .once + + subject + end end end end diff --git a/spec/presenters/commit_status_presenter_spec.rb b/spec/presenters/commit_status_presenter_spec.rb index 4b2441d656e..f0bf1b860e4 100644 --- a/spec/presenters/commit_status_presenter_spec.rb +++ b/spec/presenters/commit_status_presenter_spec.rb @@ -15,6 +15,25 @@ RSpec.describe CommitStatusPresenter do expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated) end + describe '#callout_failure_message' do + subject { presenter.callout_failure_message } + + context 'when troubleshooting doc is available' do + let(:failure_reason) { :environment_creation_failure } + + before do + build.failure_reason = failure_reason + end + + it 'appends the troubleshooting link' do + doc = described_class::TROUBLESHOOTING_DOC[failure_reason] + + expect(subject).to eq("#{described_class.callout_failure_messages[failure_reason]} " \ + "<a href=\"#{presenter.help_page_path(doc[:path], anchor: doc[:anchor])}\">How do I fix it?</a>") + end + end + end + describe 'covers all failure reasons' do let(:message) { presenter.callout_failure_message } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6ccfabca101..4f393aed377 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -308,6 +308,10 @@ RSpec.configure do |config| # For more information check https://gitlab.com/gitlab-org/gitlab/-/issues/339348 stub_feature_flags(new_header_search: false) + # Disable the override flag in order to enable the feature by default. + # See https://docs.gitlab.com/ee/development/feature_flags/#selectively-disable-by-actor + stub_feature_flags(surface_environment_creation_failure_override: false) + allow(Gitlab::GitalyClient).to receive(:can_use_disk?).and_return(enable_rugged) else unstub_all_feature_flags |