From 90726a8ccc9df6d9b5ff4f5e1eb31d015c1db8e2 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 12 May 2022 06:07:53 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../javascripts/integrations/constants.js | 10 + .../edit/components/integration_form.vue | 26 +- .../edit/components/jira_issues_fields.vue | 104 ++++---- .../javascripts/integrations/edit/index.js | 2 + .../components/time_tracking/report.vue | 4 +- .../components/time_tracking/time_tracker.vue | 1 + .../components/user_popover/user_popover.vue | 12 +- app/finders/packages/build_infos_finder.rb | 46 +++- .../build_infos_for_many_packages_finder.rb | 92 ------- .../resolvers/group_packages_resolver.rb | 4 - .../resolvers/package_pipelines_resolver.rb | 65 +---- .../resolvers/project_packages_resolver.rb | 8 - app/helpers/integrations_helper.rb | 1 + app/models/integrations/jira.rb | 3 +- .../namespace_ci_cd_setting_policy.rb | 5 + app/views/projects/tags/_tag.html.haml | 2 +- .../development/follow_in_user_popover.yml | 8 - .../packages_graphql_pipelines_resolver.yml | 8 - data/removals/15_0/15-0-type.yml | 10 + doc/api/graphql/reference/index.md | 29 +++ doc/update/removals.md | 10 + doc/user/group/iterations/index.md | 33 ++- lib/api/environments.rb | 2 + .../backfill_project_namespaces.rb | 2 +- lib/gitlab/database/migration_helpers.rb | 12 +- .../batched_background_migration_helpers.rb | 8 + lib/gitlab/gon_helper.rb | 1 - locale/gitlab.pot | 6 + package.json | 4 +- .../packages/build_infos_finder_spec.rb | 120 +++++++-- ...ild_infos_for_many_packages_finder_spec.rb | 136 ----------- .../edit/components/integration_form_spec.js | 24 +- .../components/jira_issues_fields_spec.js | 49 +--- spec/frontend/integrations/edit/mock_data.js | 8 + spec/frontend/user_popovers_spec.js | 6 - .../user_popover/user_popover_spec.js | 188 +++++++------- .../package_pipelines_resolver_spec.rb | 229 ++++++++---------- spec/helpers/integrations_helper_spec.rb | 1 + .../gitlab/database/migration_helpers_spec.rb | 39 ++- ...tched_background_migration_helpers_spec.rb | 20 ++ ...nalize_project_namespaces_backfill_spec.rb | 10 +- spec/models/integrations/jira_spec.rb | 5 + spec/requests/api/environments_spec.rb | 7 + .../projects/tags/index.html.haml_spec.rb | 2 +- yarn.lock | 16 +- 45 files changed, 647 insertions(+), 731 deletions(-) delete mode 100644 app/finders/packages/build_infos_for_many_packages_finder.rb create mode 100644 app/policies/namespace_ci_cd_setting_policy.rb delete mode 100644 config/feature_flags/development/follow_in_user_popover.yml delete mode 100644 config/feature_flags/development/packages_graphql_pipelines_resolver.yml create mode 100644 data/removals/15_0/15-0-type.yml delete mode 100644 spec/finders/packages/build_infos_for_many_packages_finder_spec.rb diff --git a/app/assets/javascripts/integrations/constants.js b/app/assets/javascripts/integrations/constants.js index c5ed5bb08a9..b9975eed716 100644 --- a/app/assets/javascripts/integrations/constants.js +++ b/app/assets/javascripts/integrations/constants.js @@ -37,3 +37,13 @@ export const integrationFormSectionComponents = { [integrationFormSections.JIRA_TRIGGER]: 'IntegrationSectionJiraTrigger', [integrationFormSections.JIRA_ISSUES]: 'IntegrationSectionJiraIssues', }; + +export const billingPlans = { + PREMIUM: 'premium', + ULTIMATE: 'ultimate', +}; + +export const billingPlanNames = { + [billingPlans.PREMIUM]: s__('BillingPlans|Premium'), + [billingPlans.ULTIMATE]: s__('BillingPlans|Ultimate'), +}; diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue index 661299920c7..9f43360fb73 100644 --- a/app/assets/javascripts/integrations/edit/components/integration_form.vue +++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue @@ -1,5 +1,11 @@ @@ -214,7 +223,20 @@ export default { >
-

{{ section.title }}

+

+ {{ section.title + }} + {{ $options.billingPlanNames[section.plan] }} + +

diff --git a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue index f00339c92fa..584d23e17e1 100644 --- a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue +++ b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue @@ -2,7 +2,6 @@ import { GlFormGroup, GlFormCheckbox, GlFormInput } from '@gitlab/ui'; import { mapGetters } from 'vuex'; import { s__, __ } from '~/locale'; -import JiraUpgradeCta from './jira_upgrade_cta.vue'; export default { name: 'JiraIssuesFields', @@ -10,7 +9,6 @@ export default { GlFormGroup, GlFormCheckbox, GlFormInput, - JiraUpgradeCta, JiraIssueCreationVulnerabilities: () => import('ee_component/integrations/edit/components/jira_issue_creation_vulnerabilities.vue'), }, @@ -45,11 +43,6 @@ export default { required: false, default: null, }, - upgradePlanPath: { - type: String, - required: false, - default: '', - }, isValidated: { type: Boolean, required: false, @@ -64,6 +57,9 @@ export default { }, computed: { ...mapGetters(['isInheriting']), + checkboxDisabled() { + return !this.showJiraIssuesIntegration || this.isInheriting; + }, validProjectKey() { return !this.enableJiraIssues || Boolean(this.projectKey) || !this.isValidated; }, @@ -85,64 +81,48 @@ export default { diff --git a/app/assets/javascripts/integrations/edit/index.js b/app/assets/javascripts/integrations/edit/index.js index 9a9aae36657..92e6ca509c3 100644 --- a/app/assets/javascripts/integrations/edit/index.js +++ b/app/assets/javascripts/integrations/edit/index.js @@ -23,6 +23,7 @@ function parseDatasetToProps(data) { projectKey, upgradePlanPath, learnMorePath, + aboutPricingUrl, triggerEvents, sections, fields, @@ -82,6 +83,7 @@ function parseDatasetToProps(data) { upgradePlanPath, }, learnMorePath, + aboutPricingUrl, triggerEvents: JSON.parse(triggerEvents), sections: JSON.parse(sections, { deep: true }), fields: convertObjectPropsToCamelCase(JSON.parse(fields), { deep: true }), diff --git a/app/assets/javascripts/sidebar/components/time_tracking/report.vue b/app/assets/javascripts/sidebar/components/time_tracking/report.vue index 5d4031ac68b..d9797961d40 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/report.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/report.vue @@ -78,9 +78,9 @@ export default { }, }, fields: [ - { key: 'spentAt', label: __('Spent At'), sortable: true }, + { key: 'spentAt', label: __('Spent At'), sortable: true, tdClass: 'gl-w-quarter' }, { key: 'user', label: __('User'), sortable: true }, - { key: 'timeSpent', label: __('Time Spent'), sortable: true }, + { key: 'timeSpent', label: __('Time Spent'), sortable: true, tdClass: 'gl-w-15' }, { key: 'summary', label: __('Summary / Note'), sortable: true }, ], }; diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue index ea57dedc4ab..e7d47634af2 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue @@ -249,6 +249,7 @@ export default { diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue index 01a0b134b7f..7d1b80826ae 100644 --- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue +++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue @@ -78,17 +78,7 @@ export default { return !this.userIsLoading && this.user.username !== gon.current_username; }, shouldRenderToggleFollowButton() { - return ( - /* - * We're using `gon` to access feature flag because this component - * gets initialized dynamically multiple times from `user_popovers.js` - * for each user link present on the page, and using `glFeatureFlagMixin()` - * doesn't inject available feature flags into the component during init. - */ - gon?.features?.followInUserPopover && - this.isNotCurrentUser && - typeof this.user?.isFollowed !== 'undefined' - ); + return this.isNotCurrentUser && typeof this.user?.isFollowed !== 'undefined'; }, toggleFollowButtonText() { if (this.toggleFollowLoading) return null; diff --git a/app/finders/packages/build_infos_finder.rb b/app/finders/packages/build_infos_finder.rb index 92ad5888eb9..a346c3d754b 100644 --- a/app/finders/packages/build_infos_finder.rb +++ b/app/finders/packages/build_infos_finder.rb @@ -2,33 +2,55 @@ module Packages class BuildInfosFinder + include ActiveRecord::ConnectionAdapters::Quoting + MAX_PAGE_SIZE = 100 - def initialize(package, params) - @package = package + def initialize(package_ids, params) + @package_ids = package_ids @params = params end def execute - build_infos = @package.build_infos.without_empty_pipelines - build_infos = apply_order(build_infos) - build_infos = apply_limit(build_infos) - apply_cursor(build_infos) + return Packages::BuildInfo.none if @package_ids.blank? + + # This is a highly custom query that + # will not be re-used elsewhere + # rubocop: disable CodeReuse/ActiveRecord + query = Packages::Package.id_in(@package_ids) + .select('build_infos.*') + .from([Packages::Package.arel_table, lateral_query.arel.lateral.as('build_infos')]) + .order('build_infos.id DESC') + + # We manually select build_infos fields from the lateral query. + # Thus, we need to instruct ActiveRecord that returned rows are + # actually Packages::BuildInfo objects + Packages::BuildInfo.find_by_sql(query.to_sql) + # rubocop: enable CodeReuse/ActiveRecord end private - def apply_order(build_infos) - order_direction = :desc - order_direction = :asc if last + def lateral_query + order_direction = last ? :asc : :desc - build_infos.order_by_pipeline_id(order_direction) + # This is a highly custom query that + # will not be re-used elsewhere + # rubocop: disable CodeReuse/ActiveRecord + where_condition = Packages::BuildInfo.arel_table[:package_id] + .eq(Arel.sql("#{Packages::Package.table_name}.id")) + build_infos = ::Packages::BuildInfo.without_empty_pipelines + .where(where_condition) + .order(id: order_direction) + .limit(max_rows_per_package_id) + # rubocop: enable CodeReuse/ActiveRecord + apply_cursor(build_infos) end - def apply_limit(build_infos) + def max_rows_per_package_id limit = [first, last, max_page_size, MAX_PAGE_SIZE].compact.min limit += 1 if support_next_page - build_infos.limit(limit) + limit end def apply_cursor(build_infos) diff --git a/app/finders/packages/build_infos_for_many_packages_finder.rb b/app/finders/packages/build_infos_for_many_packages_finder.rb deleted file mode 100644 index 8f9805f51d0..00000000000 --- a/app/finders/packages/build_infos_for_many_packages_finder.rb +++ /dev/null @@ -1,92 +0,0 @@ -# frozen_string_literal: true - -module Packages - # TODO rename to BuildInfosFinder when cleaning up packages_graphql_pipelines_resolver - # https://gitlab.com/gitlab-org/gitlab/-/issues/358432 - class BuildInfosForManyPackagesFinder - include ActiveRecord::ConnectionAdapters::Quoting - - MAX_PAGE_SIZE = 100 - - def initialize(package_ids, params) - @package_ids = package_ids - @params = params - end - - def execute - return Packages::BuildInfo.none if @package_ids.blank? - - # This is a highly custom query that - # will not be re-used elsewhere - # rubocop: disable CodeReuse/ActiveRecord - query = Packages::Package.id_in(@package_ids) - .select('build_infos.*') - .from([Packages::Package.arel_table, lateral_query.arel.lateral.as('build_infos')]) - .order('build_infos.id DESC') - - # We manually select build_infos fields from the lateral query. - # Thus, we need to instruct ActiveRecord that returned rows are - # actually Packages::BuildInfo objects - Packages::BuildInfo.find_by_sql(query.to_sql) - # rubocop: enable CodeReuse/ActiveRecord - end - - private - - def lateral_query - order_direction = last ? :asc : :desc - - # This is a highly custom query that - # will not be re-used elsewhere - # rubocop: disable CodeReuse/ActiveRecord - where_condition = Packages::BuildInfo.arel_table[:package_id] - .eq(Arel.sql("#{Packages::Package.table_name}.id")) - build_infos = ::Packages::BuildInfo.without_empty_pipelines - .where(where_condition) - .order(id: order_direction) - .limit(max_rows_per_package_id) - # rubocop: enable CodeReuse/ActiveRecord - apply_cursor(build_infos) - end - - def max_rows_per_package_id - limit = [first, last, max_page_size, MAX_PAGE_SIZE].compact.min - limit += 1 if support_next_page - limit - end - - def apply_cursor(build_infos) - if before - build_infos.with_pipeline_id_greater_than(before) - elsif after - build_infos.with_pipeline_id_less_than(after) - else - build_infos - end - end - - def first - @params[:first] - end - - def last - @params[:last] - end - - def max_page_size - @params[:max_page_size] - end - - def before - @params[:before] - end - - def after - @params[:after] - end - - def support_next_page - @params[:support_next_page] - end - end -end diff --git a/app/graphql/resolvers/group_packages_resolver.rb b/app/graphql/resolvers/group_packages_resolver.rb index 8cb1b36e2c0..b48e0b75190 100644 --- a/app/graphql/resolvers/group_packages_resolver.rb +++ b/app/graphql/resolvers/group_packages_resolver.rb @@ -16,10 +16,6 @@ module Resolvers }).freeze def ready?(**args) - # TODO remove when cleaning up packages_graphql_pipelines_resolver - # https://gitlab.com/gitlab-org/gitlab/-/issues/358432 - context.scoped_set!(:packages_access_level, :group) - context[self.class] ||= { executions: 0 } context[self.class][:executions] += 1 raise GraphQL::ExecutionError, "Packages can be requested only for one group at a time" if context[self.class][:executions] > 1 diff --git a/app/graphql/resolvers/package_pipelines_resolver.rb b/app/graphql/resolvers/package_pipelines_resolver.rb index 55e8d1b3d36..9ff77f02547 100644 --- a/app/graphql/resolvers/package_pipelines_resolver.rb +++ b/app/graphql/resolvers/package_pipelines_resolver.rb @@ -16,45 +16,13 @@ module Resolvers # number of build infos returned for _each_ package when using the new finder. MAX_PAGE_SIZE = 20 - def resolve(first: nil, last: nil, after: nil, before: nil, lookahead:) - case detect_mode - when :object_field - package.pipelines - when :new_finder - resolve_with_new_finder(first: first, last: last, after: after, before: before, lookahead: lookahead) - else - resolve_with_old_finder(first: first, last: last, after: after, before: before, lookahead: lookahead) - end - end - - # we manage the pagination manually, so opt out of the connection field extension - def self.field_options - super.merge( - connection: false, - extras: [:lookahead] - ) - end - - private - - # TODO remove when cleaning up packages_graphql_pipelines_resolver - # https://gitlab.com/gitlab-org/gitlab/-/issues/358432 - def detect_mode - return :new_finder if Feature.enabled?(:packages_graphql_pipelines_resolver) - return :object_field if context[:packages_access_level] == :group || context[:packages_access_level] == :project - - :old_finder - end - # This returns a promise for a connection of promises for pipelines: # Lazy[Connection[Lazy[Pipeline]]] structure - # TODO rename to #resolve when cleaning up packages_graphql_pipelines_resolver - # https://gitlab.com/gitlab-org/gitlab/-/issues/358432 - def resolve_with_new_finder(first:, last:, after:, before:, lookahead:) + def resolve(first: nil, last: nil, after: nil, before: nil, lookahead:) default_value = default_value_for(first: first, last: last, after: after, before: before) BatchLoader::GraphQL.for(package.id) .batch(default_value: default_value) do |package_ids, loader| - build_infos = ::Packages::BuildInfosForManyPackagesFinder.new( + build_infos = ::Packages::BuildInfosFinder.new( package_ids, first: first, last: last, @@ -73,6 +41,16 @@ module Resolvers end end + # we manage the pagination manually, so opt out of the connection field extension + def self.field_options + super.merge( + connection: false, + extras: [:lookahead] + ) + end + + private + def lazy_load_pipeline(id) ::Gitlab::Graphql::Loaders::BatchModelLoader.new(::Ci::Pipeline, id) .find @@ -89,25 +67,6 @@ module Resolvers ) end - # TODO remove when cleaning up packages_graphql_pipelines_resolver - # https://gitlab.com/gitlab-org/gitlab/-/issues/358432 - def resolve_with_old_finder(first:, last:, after:, before:, lookahead:) - finder = ::Packages::BuildInfosFinder.new( - package, - first: first, - last: last, - after: decode_cursor(after), - before: decode_cursor(before), - max_page_size: context.schema.default_max_page_size, - support_next_page: lookahead.selects?(:page_info) - ) - - build_infos = finder.execute - - # this .pluck_pipeline_ids can load max 101 pipelines ids - ::Ci::Pipeline.id_in(build_infos.pluck_pipeline_ids) - end - def decode_cursor(encoded) return unless encoded diff --git a/app/graphql/resolvers/project_packages_resolver.rb b/app/graphql/resolvers/project_packages_resolver.rb index 67b4734972a..284d3c594da 100644 --- a/app/graphql/resolvers/project_packages_resolver.rb +++ b/app/graphql/resolvers/project_packages_resolver.rb @@ -5,14 +5,6 @@ module Resolvers class ProjectPackagesResolver < PackagesBaseResolver # The GraphQL type is defined in the extended class - # TODO remove when cleaning up packages_graphql_pipelines_resolver - # https://gitlab.com/gitlab-org/gitlab/-/issues/358432 - def ready?(**args) - context.scoped_set!(:packages_access_level, :project) - - super - end - def resolve(sort:, **filters) return unless packages_available? diff --git a/app/helpers/integrations_helper.rb b/app/helpers/integrations_helper.rb index b960ed46ba9..862938ac961 100644 --- a/app/helpers/integrations_helper.rb +++ b/app/helpers/integrations_helper.rb @@ -112,6 +112,7 @@ module IntegrationsHelper enable_comments: integration.comment_on_event_enabled.to_s, comment_detail: integration.comment_detail, learn_more_path: integrations_help_page_path, + about_pricing_url: Gitlab::Saas.about_pricing_url, trigger_events: trigger_events_for_integration(integration), sections: integration.sections.to_json, fields: fields_for_integration(integration), diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb index d78a13db10b..992bd01bf5f 100644 --- a/app/models/integrations/jira.rb +++ b/app/models/integrations/jira.rb @@ -168,7 +168,8 @@ module Integrations sections.push({ type: SECTION_TYPE_JIRA_ISSUES, title: _('Issues'), - description: jira_issues_section_description + description: jira_issues_section_description, + plan: 'premium' }) end diff --git a/app/policies/namespace_ci_cd_setting_policy.rb b/app/policies/namespace_ci_cd_setting_policy.rb new file mode 100644 index 00000000000..d883526b86d --- /dev/null +++ b/app/policies/namespace_ci_cd_setting_policy.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class NamespaceCiCdSettingPolicy < BasePolicy # rubocop:disable Gitlab/NamespacedClass + delegate { @subject.namespace } +end diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index 0ee3b89b629..cac4a771d51 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -21,7 +21,7 @@ .text-secondary = sprite_icon("rocket", size: 12) = _("Release") - = link_to release.name, project_releases_path(@project, anchor: release.tag), class: 'gl-text-blue-600!' + = link_to release.name, project_release_path(@project, release.tag), class: 'gl-text-blue-600!' - if tag.message.present? %pre.wrap diff --git a/config/feature_flags/development/follow_in_user_popover.yml b/config/feature_flags/development/follow_in_user_popover.yml deleted file mode 100644 index 579e8507ac1..00000000000 --- a/config/feature_flags/development/follow_in_user_popover.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: follow_in_user_popover -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76050 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/355070 -milestone: '14.9' -type: development -group: group::workspace -default_enabled: false diff --git a/config/feature_flags/development/packages_graphql_pipelines_resolver.yml b/config/feature_flags/development/packages_graphql_pipelines_resolver.yml deleted file mode 100644 index 6509b9896b4..00000000000 --- a/config/feature_flags/development/packages_graphql_pipelines_resolver.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: packages_graphql_pipelines_resolver -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82496 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/358432 -milestone: '14.10' -type: development -group: group::package -default_enabled: false diff --git a/data/removals/15_0/15-0-type.yml b/data/removals/15_0/15-0-type.yml new file mode 100644 index 00000000000..bb1b764f823 --- /dev/null +++ b/data/removals/15_0/15-0-type.yml @@ -0,0 +1,10 @@ +- name: "Remove `type` and `types` keyword from CI/CD configuration" + announcement_milestone: "14.6" + announcement_date: "2021-12-22" + removal_milestone: "15.0" + removal_date: "2022-05-22" + breaking_change: true + reporter: dhershkovitch + body: | # Do not modify this line, instead modify the lines below. + The `type` and `types` CI/CD keywords is removed in GitLab 15.0, so pipelines that use these keywords fail with a syntax error. Switch to `stage` and `stages`, which have the same behavior. +# The following items are not published on the docs page, but may be used in the future. diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 2c12f99ad06..1ee08281db4 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -3677,6 +3677,26 @@ Input type: `MergeRequestUpdateInput` | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | `mergeRequest` | [`MergeRequest`](#mergerequest) | Merge request after mutation. | +### `Mutation.namespaceCiCdSettingsUpdate` + +Input type: `NamespaceCiCdSettingsUpdateInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `allowStaleRunnerPruning` | [`Boolean`](#boolean) | Indicates if stale runners directly belonging to this namespace should be periodically pruned. | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `fullPath` | [`ID!`](#id) | Full path of the namespace the settings belong to. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `ciCdSettings` | [`NamespaceCiCdSetting!`](#namespacecicdsetting) | CI/CD settings after mutation. | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | + ### `Mutation.namespaceIncreaseStorageTemporarily` Input type: `NamespaceIncreaseStorageTemporarilyInput` @@ -13857,6 +13877,15 @@ four standard [pagination arguments](#connection-pagination-arguments): | `actionScanTypes` | [`[SecurityReportTypeEnum!]`](#securityreporttypeenum) | Filters policies by the action scan type. Only these scan types are supported: `dast`, `secret_detection`, `cluster_image_scanning`, `container_scanning`, `sast`. | | `relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. | +### `NamespaceCiCdSetting` + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `allowStaleRunnerPruning` | [`Boolean`](#boolean) | Indicates if stale runners directly belonging to this namespace should be periodically pruned. | +| `namespace` | [`Namespace`](#namespace) | Namespace the CI/CD settings belong to. | + ### `NetworkPolicy` Represents the network policy. diff --git a/doc/update/removals.md b/doc/update/removals.md index e1d87ef4616..3842637489f 100644 --- a/doc/update/removals.md +++ b/doc/update/removals.md @@ -198,6 +198,16 @@ changes to your code, settings, or workflow. In GitLab 14.5, we introduced the command `gitlab-ctl promote` to promote any Geo secondary node to a primary during a failover. This command replaces `gitlab-ctl promote-to-primary-node` which was only usable for single-node Geo sites. `gitlab-ctl promote-to-primary-node` has been removed in GitLab 15.0. +### Remove `type` and `types` keyword from CI/CD configuration + +WARNING: +This feature was changed or removed in 15.0 +as a [breaking change](https://docs.gitlab.com/ee/development/contributing/#breaking-changes). +Before updating GitLab, review the details carefully to determine if you need to make any +changes to your code, settings, or workflow. + +The `type` and `types` CI/CD keywords is removed in GitLab 15.0, so pipelines that use these keywords fail with a syntax error. Switch to `stage` and `stages`, which have the same behavior. + ### Remove dependency_proxy_for_private_groups feature flag WARNING: diff --git a/doc/user/group/iterations/index.md b/doc/user/group/iterations/index.md index a5944314a50..43dcbdf2996 100644 --- a/doc/user/group/iterations/index.md +++ b/doc/user/group/iterations/index.md @@ -174,6 +174,7 @@ To group issues by label: > - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5077) in GitLab 14.1. > - Deployed behind a [feature flag](../../feature_flags.md), named `iteration_cadences`, disabled by default. +> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/354977) in GitLab 15.0: All scheduled iterations must start on the same day of the week as the cadence start day. Start date of cadence cannot be edited after the first iteration starts. FLAG: On self-managed GitLab, by default this feature is not available. To make it available, ask an @@ -200,7 +201,8 @@ To create an iteration cadence: 1. Select **New iteration cadence**. 1. Complete the fields. - Enter the title and description of the iteration cadence. - - Enter the start date of the iteration cadence. + - Enter the start date of the iteration cadence. Iterations will be scheduled to + begin on the same day of the week as the day of the week of the start date. - From the **Duration** dropdown list, select how many weeks each iteration should last. - From the **Future iterations** dropdown list, select how many future iterations should be created and maintained by GitLab. @@ -277,6 +279,35 @@ If you attempt to set a new start date, the conversion fails with an error messa If your manual cadence is empty, converting it to use automatic scheduling is effectively the same as creating a new automated cadence. +GitLab will start scheduling new iterations on the same day of the week as the start date, +starting from the nearest such day from the current date. + During the conversion process GitLab does not delete or modify existing **ongoing** or **closed** iterations. If you have iterations with start dates in the future, they are updated to fit your cadence settings. + +#### Converted cadences example + +For example, suppose it's Friday, April 15, and you have three iterations in a manual cadence: + +- Monday, April 4 - Friday, April 8 (closed) +- Tuesday, April 12 - Friday, April 15 (ongoing) +- Tuesday, May 3 - Friday, May 6 (upcoming) + +On Friday, April 15, you convert the manual cadence +to automate scheduling iterations every week, up to two upcoming iterations. + +The first iteration is closed, and the second iteration is ongoing, +so they aren't deleted or modified in the conversion process. + +To observe the weekly duration, the third iteration is updated so that it: + +- Starts on Monday, April 18 - which is the nearest date that is Monday. +- Ends on Sunday, April 24. + +Finally, to always have two upcoming iterations, an additional iteration is scheduled: + +- Monday, April 4 - Friday, April 8 (closed) +- Tuesday, April 12 - Friday, April 15 (ongoing) +- Monday, April 18 - Sunday, April 24 (upcoming) +- Monday, April 25 - Sunday, May 1 (upcoming) diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 31c8ae73a0c..11f1cab0c72 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -30,6 +30,8 @@ module API environments = ::Environments::EnvironmentsFinder.new(user_project, current_user, params).execute present paginate(environments), with: Entities::Environment, current_user: current_user + rescue ::Environments::EnvironmentsFinder::InvalidStatesError => exception + bad_request!(exception.message) end desc 'Creates a new environment' do diff --git a/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces.rb b/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces.rb index 91a0db08d5b..2b27bad3497 100644 --- a/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces.rb +++ b/lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces.rb @@ -55,7 +55,7 @@ module Gitlab end def cleanup_gin_index(table_name) - index_names = ApplicationRecord.connection.select_values("select indexname::text from pg_indexes where tablename = '#{table_name}' and indexdef ilike '%gin%'") + index_names = ApplicationRecord.connection.select_values("select indexname::text from pg_indexes where tablename = '#{table_name}' and indexdef ilike '%using gin%'") index_names.each do |index_name| ApplicationRecord.connection.execute("select gin_clean_pending_list('#{index_name}')") diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index a8d60d62e66..80f675c825d 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -943,7 +943,7 @@ module Gitlab execute("DELETE FROM batched_background_migrations WHERE #{conditions}") end - def ensure_batched_background_migration_is_finished(job_class_name:, table_name:, column_name:, job_arguments:) + def ensure_batched_background_migration_is_finished(job_class_name:, table_name:, column_name:, job_arguments:, finalize: true) migration = Gitlab::Database::BackgroundMigration::BatchedMigration .for_configuration(job_class_name, table_name, column_name, job_arguments).first @@ -954,9 +954,13 @@ module Gitlab job_arguments: job_arguments } - if migration.nil? - Gitlab::AppLogger.warn "Could not find batched background migration for the given configuration: #{configuration}" - elsif !migration.finished? + return Gitlab::AppLogger.warn "Could not find batched background migration for the given configuration: #{configuration}" if migration.nil? + + return if migration.finished? + + Gitlab::Database::BackgroundMigration::BatchedMigrationRunner.finalize(job_class_name, table_name, column_name, job_arguments, connection: connection) if finalize + + unless migration.reload.finished? # rubocop:disable Cop/ActiveRecordAssociationReload raise "Expected batched background migration for the given configuration to be marked as 'finished', " \ "but it is '#{migration.status_name}':" \ "\t#{configuration}" \ diff --git a/lib/gitlab/database/migrations/batched_background_migration_helpers.rb b/lib/gitlab/database/migrations/batched_background_migration_helpers.rb index 0261ade0fe7..75dc4da4bb9 100644 --- a/lib/gitlab/database/migrations/batched_background_migration_helpers.rb +++ b/lib/gitlab/database/migrations/batched_background_migration_helpers.rb @@ -122,6 +122,14 @@ module Gitlab migration.save! migration end + + def finalize_batched_background_migration(job_class_name:, table_name:, column_name:, job_arguments:) + migration = Gitlab::Database::BackgroundMigration::BatchedMigration.find_for_configuration(job_class_name, table_name, column_name, job_arguments) + + raise 'Could not find batched background migration' if migration.nil? + + Gitlab::Database::BackgroundMigration::BatchedMigrationRunner.finalize(job_class_name, table_name, column_name, job_arguments, connection: connection) + end end end end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 872156039c0..98570c02e3d 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -59,7 +59,6 @@ module Gitlab push_frontend_feature_flag(:source_editor_toolbar) push_frontend_feature_flag(:gl_avatar_for_all_user_avatars) push_frontend_feature_flag(:mr_attention_requests, current_user) - push_frontend_feature_flag(:follow_in_user_popover, current_user) end # Exposes the state of a feature flag to the frontend code. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index aba8e1415e3..937fbff7246 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -5770,6 +5770,9 @@ msgstr "" msgid "BillingPlans|Portfolio management" msgstr "" +msgid "BillingPlans|Premium" +msgstr "" + msgid "BillingPlans|Pricing page" msgstr "" @@ -5812,6 +5815,9 @@ msgstr "" msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}." msgstr "" +msgid "BillingPlans|Ultimate" +msgstr "" + msgid "BillingPlans|Upgrade to Premium" msgstr "" diff --git a/package.json b/package.json index f5642506772..05635a2c081 100644 --- a/package.json +++ b/package.json @@ -56,8 +56,8 @@ "@babel/preset-env": "^7.10.1", "@gitlab/at.js": "1.5.7", "@gitlab/favicon-overlay": "2.0.0", - "@gitlab/svgs": "2.12.0", - "@gitlab/ui": "40.0.0", + "@gitlab/svgs": "2.14.0", + "@gitlab/ui": "40.2.0", "@gitlab/visual-review-tools": "1.7.1", "@rails/actioncable": "6.1.4-7", "@rails/ujs": "6.1.4-7", diff --git a/spec/finders/packages/build_infos_finder_spec.rb b/spec/finders/packages/build_infos_finder_spec.rb index 23425de4316..6e7f0623030 100644 --- a/spec/finders/packages/build_infos_finder_spec.rb +++ b/spec/finders/packages/build_infos_finder_spec.rb @@ -9,7 +9,20 @@ RSpec.describe ::Packages::BuildInfosFinder do let_it_be(:build_infos) { create_list(:package_build_info, 5, :with_pipeline, package: package) } let_it_be(:build_info_with_empty_pipeline) { create(:package_build_info, package: package) } - let(:finder) { described_class.new(package, params) } + let_it_be(:other_package) { create(:package) } + let_it_be(:other_build_infos) { create_list(:package_build_info, 5, :with_pipeline, package: other_package) } + let_it_be(:other_build_info_with_empty_pipeline) { create(:package_build_info, package: other_package) } + + let_it_be(:all_build_infos) { build_infos + other_build_infos } + + let(:finder) { described_class.new(packages, params) } + let(:packages) { nil } + let(:first) { nil } + let(:last) { nil } + let(:after) { nil } + let(:before) { nil } + let(:max_page_size) { nil } + let(:support_next_page) { false } let(:params) do { first: first, @@ -24,41 +37,100 @@ RSpec.describe ::Packages::BuildInfosFinder do describe '#execute' do subject { finder.execute } - where(:first, :last, :after_index, :before_index, :max_page_size, :support_next_page, :expected_build_infos_indexes) do - # F L AI BI MPS SNP - nil | nil | nil | nil | nil | false | [4, 3, 2, 1, 0] - nil | nil | nil | nil | 10 | false | [4, 3, 2, 1, 0] - nil | nil | nil | nil | 2 | false | [4, 3] - 2 | nil | nil | nil | nil | false | [4, 3] - 2 | nil | nil | nil | nil | true | [4, 3, 2] - 2 | nil | 3 | nil | nil | false | [2, 1] - 2 | nil | 3 | nil | nil | true | [2, 1, 0] - 3 | nil | 4 | nil | 2 | false | [3, 2] - 3 | nil | 4 | nil | 2 | true | [3, 2, 1] - nil | 2 | nil | nil | nil | false | [0, 1] - nil | 2 | nil | nil | nil | true | [0, 1, 2] - nil | 2 | nil | 1 | nil | false | [2, 3] - nil | 2 | nil | 1 | nil | true | [2, 3, 4] - nil | 3 | nil | 0 | 2 | false | [1, 2] - nil | 3 | nil | 0 | 2 | true | [1, 2, 3] - end - - with_them do + shared_examples 'returning the expected build infos' do let(:expected_build_infos) do expected_build_infos_indexes.map do |idx| - build_infos[idx] + all_build_infos[idx] end end let(:after) do - build_infos[after_index].pipeline_id if after_index + all_build_infos[after_index].pipeline_id if after_index end let(:before) do - build_infos[before_index].pipeline_id if before_index + all_build_infos[before_index].pipeline_id if before_index end it { is_expected.to eq(expected_build_infos) } end + + context 'with nil packages' do + let(:packages) { nil } + + it { is_expected.to be_empty } + end + + context 'with [] packages' do + let(:packages) { [] } + + it { is_expected.to be_empty } + end + + context 'with empy scope packages' do + let(:packages) { Packages::Package.none } + + it { is_expected.to be_empty } + end + + context 'with a single package' do + let(:packages) { package.id } + + # rubocop: disable Layout/LineLength + where(:first, :last, :after_index, :before_index, :max_page_size, :support_next_page, :expected_build_infos_indexes) do + # F L AI BI MPS SNP + nil | nil | nil | nil | nil | false | [4, 3, 2, 1, 0] + nil | nil | nil | nil | 10 | false | [4, 3, 2, 1, 0] + nil | nil | nil | nil | 2 | false | [4, 3] + 2 | nil | nil | nil | nil | false | [4, 3] + 2 | nil | nil | nil | nil | true | [4, 3, 2] + 2 | nil | 3 | nil | nil | false | [2, 1] + 2 | nil | 3 | nil | nil | true | [2, 1, 0] + 3 | nil | 4 | nil | 2 | false | [3, 2] + 3 | nil | 4 | nil | 2 | true | [3, 2, 1] + nil | 2 | nil | nil | nil | false | [1, 0] + nil | 2 | nil | nil | nil | true | [2, 1, 0] + nil | 2 | nil | 1 | nil | false | [3, 2] + nil | 2 | nil | 1 | nil | true | [4, 3, 2] + nil | 3 | nil | 0 | 2 | false | [2, 1] + nil | 3 | nil | 0 | 2 | true | [3, 2, 1] + end + # rubocop: enable Layout/LineLength + + with_them do + it_behaves_like 'returning the expected build infos' + end + end + + context 'with many packages' do + let(:packages) { [package.id, other_package.id] } + + # using after_index/before_index when receiving multiple packages doesn't + # make sense but we still verify here that the behavior is coherent. + # rubocop: disable Layout/LineLength + where(:first, :last, :after_index, :before_index, :max_page_size, :support_next_page, :expected_build_infos_indexes) do + # F L AI BI MPS SNP + nil | nil | nil | nil | nil | false | [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] + nil | nil | nil | nil | 10 | false | [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] + nil | nil | nil | nil | 2 | false | [9, 8, 4, 3] + 2 | nil | nil | nil | nil | false | [9, 8, 4, 3] + 2 | nil | nil | nil | nil | true | [9, 8, 7, 4, 3, 2] + 2 | nil | 3 | nil | nil | false | [2, 1] + 2 | nil | 3 | nil | nil | true | [2, 1, 0] + 3 | nil | 4 | nil | 2 | false | [3, 2] + 3 | nil | 4 | nil | 2 | true | [3, 2, 1] + nil | 2 | nil | nil | nil | false | [6, 5, 1, 0] + nil | 2 | nil | nil | nil | true | [7, 6, 5, 2, 1, 0] + nil | 2 | nil | 1 | nil | false | [6, 5, 3, 2] + nil | 2 | nil | 1 | nil | true | [7, 6, 5, 4, 3, 2] + nil | 3 | nil | 0 | 2 | false | [6, 5, 2, 1] + nil | 3 | nil | 0 | 2 | true | [7, 6, 5, 3, 2, 1] + end + + with_them do + it_behaves_like 'returning the expected build infos' + end + # rubocop: enable Layout/LineLength + end end end diff --git a/spec/finders/packages/build_infos_for_many_packages_finder_spec.rb b/spec/finders/packages/build_infos_for_many_packages_finder_spec.rb deleted file mode 100644 index f3c79d0c825..00000000000 --- a/spec/finders/packages/build_infos_for_many_packages_finder_spec.rb +++ /dev/null @@ -1,136 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ::Packages::BuildInfosForManyPackagesFinder do - using RSpec::Parameterized::TableSyntax - - let_it_be(:package) { create(:package) } - let_it_be(:build_infos) { create_list(:package_build_info, 5, :with_pipeline, package: package) } - let_it_be(:build_info_with_empty_pipeline) { create(:package_build_info, package: package) } - - let_it_be(:other_package) { create(:package) } - let_it_be(:other_build_infos) { create_list(:package_build_info, 5, :with_pipeline, package: other_package) } - let_it_be(:other_build_info_with_empty_pipeline) { create(:package_build_info, package: other_package) } - - let_it_be(:all_build_infos) { build_infos + other_build_infos } - - let(:finder) { described_class.new(packages, params) } - let(:packages) { nil } - let(:first) { nil } - let(:last) { nil } - let(:after) { nil } - let(:before) { nil } - let(:max_page_size) { nil } - let(:support_next_page) { false } - let(:params) do - { - first: first, - last: last, - after: after, - before: before, - max_page_size: max_page_size, - support_next_page: support_next_page - } - end - - describe '#execute' do - subject { finder.execute } - - shared_examples 'returning the expected build infos' do - let(:expected_build_infos) do - expected_build_infos_indexes.map do |idx| - all_build_infos[idx] - end - end - - let(:after) do - all_build_infos[after_index].pipeline_id if after_index - end - - let(:before) do - all_build_infos[before_index].pipeline_id if before_index - end - - it { is_expected.to eq(expected_build_infos) } - end - - context 'with nil packages' do - let(:packages) { nil } - - it { is_expected.to be_empty } - end - - context 'with [] packages' do - let(:packages) { [] } - - it { is_expected.to be_empty } - end - - context 'with empy scope packages' do - let(:packages) { Packages::Package.none } - - it { is_expected.to be_empty } - end - - context 'with a single package' do - let(:packages) { package.id } - - # rubocop: disable Layout/LineLength - where(:first, :last, :after_index, :before_index, :max_page_size, :support_next_page, :expected_build_infos_indexes) do - # F L AI BI MPS SNP - nil | nil | nil | nil | nil | false | [4, 3, 2, 1, 0] - nil | nil | nil | nil | 10 | false | [4, 3, 2, 1, 0] - nil | nil | nil | nil | 2 | false | [4, 3] - 2 | nil | nil | nil | nil | false | [4, 3] - 2 | nil | nil | nil | nil | true | [4, 3, 2] - 2 | nil | 3 | nil | nil | false | [2, 1] - 2 | nil | 3 | nil | nil | true | [2, 1, 0] - 3 | nil | 4 | nil | 2 | false | [3, 2] - 3 | nil | 4 | nil | 2 | true | [3, 2, 1] - nil | 2 | nil | nil | nil | false | [1, 0] - nil | 2 | nil | nil | nil | true | [2, 1, 0] - nil | 2 | nil | 1 | nil | false | [3, 2] - nil | 2 | nil | 1 | nil | true | [4, 3, 2] - nil | 3 | nil | 0 | 2 | false | [2, 1] - nil | 3 | nil | 0 | 2 | true | [3, 2, 1] - end - # rubocop: enable Layout/LineLength - - with_them do - it_behaves_like 'returning the expected build infos' - end - end - - context 'with many packages' do - let(:packages) { [package.id, other_package.id] } - - # using after_index/before_index when receiving multiple packages doesn't - # make sense but we still verify here that the behavior is coherent. - # rubocop: disable Layout/LineLength - where(:first, :last, :after_index, :before_index, :max_page_size, :support_next_page, :expected_build_infos_indexes) do - # F L AI BI MPS SNP - nil | nil | nil | nil | nil | false | [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] - nil | nil | nil | nil | 10 | false | [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] - nil | nil | nil | nil | 2 | false | [9, 8, 4, 3] - 2 | nil | nil | nil | nil | false | [9, 8, 4, 3] - 2 | nil | nil | nil | nil | true | [9, 8, 7, 4, 3, 2] - 2 | nil | 3 | nil | nil | false | [2, 1] - 2 | nil | 3 | nil | nil | true | [2, 1, 0] - 3 | nil | 4 | nil | 2 | false | [3, 2] - 3 | nil | 4 | nil | 2 | true | [3, 2, 1] - nil | 2 | nil | nil | nil | false | [6, 5, 1, 0] - nil | 2 | nil | nil | nil | true | [7, 6, 5, 2, 1, 0] - nil | 2 | nil | 1 | nil | false | [6, 5, 3, 2] - nil | 2 | nil | 1 | nil | true | [7, 6, 5, 4, 3, 2] - nil | 3 | nil | 0 | 2 | false | [6, 5, 2, 1] - nil | 3 | nil | 0 | 2 | true | [7, 6, 5, 3, 2, 1] - end - - with_them do - it_behaves_like 'returning the expected build infos' - end - # rubocop: enable Layout/LineLength - end - end -end diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js index ca481e009cf..a2bdece821f 100644 --- a/spec/frontend/integrations/edit/components/integration_form_spec.js +++ b/spec/frontend/integrations/edit/components/integration_form_spec.js @@ -1,4 +1,4 @@ -import { GlForm } from '@gitlab/ui'; +import { GlBadge, GlForm } from '@gitlab/ui'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import * as Sentry from '@sentry/browser'; @@ -18,11 +18,18 @@ import { integrationLevels, I18N_SUCCESSFUL_CONNECTION_MESSAGE, I18N_DEFAULT_ERROR_MESSAGE, + billingPlans, + billingPlanNames, } from '~/integrations/constants'; import { createStore } from '~/integrations/edit/store'; import httpStatus from '~/lib/utils/http_status'; import { refreshCurrentPage } from '~/lib/utils/url_utility'; -import { mockIntegrationProps, mockField, mockSectionConnection } from '../mock_data'; +import { + mockIntegrationProps, + mockField, + mockSectionConnection, + mockSectionJiraIssues, +} from '../mock_data'; jest.mock('@sentry/browser'); jest.mock('~/lib/utils/url_utility'); @@ -72,6 +79,7 @@ describe('IntegrationForm', () => { const findInstanceOrGroupSaveButton = () => wrapper.findByTestId('save-button-instance-group'); const findTestButton = () => wrapper.findByTestId('test-button'); const findTriggerFields = () => wrapper.findComponent(TriggerFields); + const findGlBadge = () => wrapper.findComponent(GlBadge); const findGlForm = () => wrapper.findComponent(GlForm); const findRedirectToField = () => wrapper.findByTestId('redirect-to-field'); const findDynamicField = () => wrapper.findComponent(DynamicField); @@ -327,9 +335,21 @@ describe('IntegrationForm', () => { expect(connectionSection.find('h4').text()).toBe(mockSectionConnection.title); expect(connectionSection.find('p').text()).toBe(mockSectionConnection.description); + expect(findGlBadge().exists()).toBe(false); expect(findConnectionSectionComponent().exists()).toBe(true); }); + it('renders GlBadge when `plan` is present', () => { + createComponent({ + customStateProps: { + sections: [mockSectionConnection, mockSectionJiraIssues], + }, + }); + + expect(findGlBadge().exists()).toBe(true); + expect(findGlBadge().text()).toMatchInterpolatedText(billingPlanNames[billingPlans.PREMIUM]); + }); + it('passes only fields with section type', () => { const sectionFields = [ { name: 'username', type: 'text', section: mockSectionConnection.type }, diff --git a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js index 94e370a485f..b4c5d4f9957 100644 --- a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js +++ b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js @@ -10,7 +10,6 @@ describe('JiraIssuesFields', () => { let wrapper; const defaultProps = { - showJiraIssuesIntegration: true, showJiraVulnerabilitiesIntegration: true, upgradePlanPath: 'https://gitlab.com', }; @@ -42,8 +41,6 @@ describe('JiraIssuesFields', () => { findEnableCheckbox().find('[type=checkbox]').attributes('disabled'); const findProjectKey = () => wrapper.findComponent(GlFormInput); const findProjectKeyFormGroup = () => wrapper.findByTestId('project-key-form-group'); - const findPremiumUpgradeCTA = () => wrapper.findByTestId('premium-upgrade-cta'); - const findUltimateUpgradeCTA = () => wrapper.findByTestId('ultimate-upgrade-cta'); const findJiraForVulnerabilities = () => wrapper.findByTestId('jira-for-vulnerabilities'); const setEnableCheckbox = async (isEnabled = true) => findEnableCheckbox().vm.$emit('input', isEnabled); @@ -55,19 +52,16 @@ describe('JiraIssuesFields', () => { describe('template', () => { describe.each` - showJiraIssuesIntegration | showJiraVulnerabilitiesIntegration - ${false} | ${false} - ${false} | ${true} - ${true} | ${false} - ${true} | ${true} + showJiraIssuesIntegration + ${false} + ${true} `( - 'when `showJiraIssuesIntegration` is $jiraIssues and `showJiraVulnerabilitiesIntegration` is $jiraVulnerabilities', - ({ showJiraIssuesIntegration, showJiraVulnerabilitiesIntegration }) => { + 'when showJiraIssuesIntegration = $showJiraIssuesIntegration', + ({ showJiraIssuesIntegration }) => { beforeEach(() => { createComponent({ props: { showJiraIssuesIntegration, - showJiraVulnerabilitiesIntegration, }, }); }); @@ -77,39 +71,12 @@ describe('JiraIssuesFields', () => { expect(findEnableCheckbox().exists()).toBe(true); expect(findEnableCheckboxDisabled()).toBeUndefined(); }); - - it('does not render the Premium CTA', () => { - expect(findPremiumUpgradeCTA().exists()).toBe(false); - }); - - if (!showJiraVulnerabilitiesIntegration) { - it.each` - scenario | enableJiraIssues - ${'when "Enable Jira issues" is checked, renders Ultimate upgrade CTA'} | ${true} - ${'when "Enable Jira issues" is unchecked, does not render Ultimate upgrade CTA'} | ${false} - `('$scenario', async ({ enableJiraIssues }) => { - if (enableJiraIssues) { - await setEnableCheckbox(); - } - expect(findUltimateUpgradeCTA().exists()).toBe(enableJiraIssues); - }); - } } else { - it('does not render enable checkbox', () => { - expect(findEnableCheckbox().exists()).toBe(false); - }); - - it('renders the Premium CTA', () => { - const premiumUpgradeCTA = findPremiumUpgradeCTA(); - - expect(premiumUpgradeCTA.exists()).toBe(true); - expect(premiumUpgradeCTA.props('upgradePlanPath')).toBe(defaultProps.upgradePlanPath); + it('renders enable checkbox as disabled', () => { + expect(findEnableCheckbox().exists()).toBe(true); + expect(findEnableCheckboxDisabled()).toBe('disabled'); }); } - - it('does not render the Ultimate CTA', () => { - expect(findUltimateUpgradeCTA().exists()).toBe(false); - }); }, ); diff --git a/spec/frontend/integrations/edit/mock_data.js b/spec/frontend/integrations/edit/mock_data.js index 36850a0a33a..ac0c7d244e3 100644 --- a/spec/frontend/integrations/edit/mock_data.js +++ b/spec/frontend/integrations/edit/mock_data.js @@ -37,3 +37,11 @@ export const mockSectionConnection = { title: 'Connection details', description: 'Learn more on how to configure this integration.', }; + +export const mockSectionJiraIssues = { + type: 'jira_issues', + title: 'Issues', + description: + 'Work on Jira issues without leaving GitLab. Add a Jira menu to access a read-only list of your Jira issues. Learn more.', + plan: 'premium', +}; diff --git a/spec/frontend/user_popovers_spec.js b/spec/frontend/user_popovers_spec.js index e703d283540..3aa49eb8027 100644 --- a/spec/frontend/user_popovers_spec.js +++ b/spec/frontend/user_popovers_spec.js @@ -54,12 +54,6 @@ describe('User Popovers', () => { .mockImplementation((userId) => userStatusCacheSpy(userId)); jest.spyOn(UsersCache, 'updateById'); - window.gon = { - features: { - followInUserPopover: true, - }, - }; - popovers = initUserPopovers(document.querySelectorAll(selector)); }); diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js index 5595a44672a..20298e95107 100644 --- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js +++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js @@ -51,9 +51,7 @@ describe('User Popover Component', () => { const findUserLocalTime = () => wrapper.findByTestId('user-popover-local-time'); const findToggleFollowButton = () => wrapper.findByTestId('toggle-follow-button'); - const createWrapper = (props = {}, { followInUserPopover = true } = {}) => { - gon.features.followInUserPopover = followInUserPopover; - + const createWrapper = (props = {}) => { wrapper = mountExtended(UserPopover, { propsData: { ...DEFAULT_PROPS, @@ -304,132 +302,122 @@ describe('User Popover Component', () => { }); }); - describe('follow actions with `followInUserPopover` flag enabled', () => { - describe("when current user doesn't follow the user", () => { - beforeEach(() => createWrapper()); + describe("when current user doesn't follow the user", () => { + beforeEach(() => createWrapper()); - it('renders the Follow button with the correct variant', () => { - expect(findToggleFollowButton().text()).toBe('Follow'); - expect(findToggleFollowButton().props('variant')).toBe('confirm'); + it('renders the Follow button with the correct variant', () => { + expect(findToggleFollowButton().text()).toBe('Follow'); + expect(findToggleFollowButton().props('variant')).toBe('confirm'); + }); + + describe('when clicking', () => { + it('follows the user', async () => { + followUser.mockResolvedValue({}); + + await findToggleFollowButton().trigger('click'); + + expect(findToggleFollowButton().props('loading')).toBe(true); + + await axios.waitForAll(); + + expect(wrapper.emitted().follow.length).toBe(1); + expect(wrapper.emitted().unfollow).toBeFalsy(); }); - describe('when clicking', () => { - it('follows the user', async () => { - followUser.mockResolvedValue({}); + describe('when an error occurs', () => { + beforeEach(() => { + followUser.mockRejectedValue({}); - await findToggleFollowButton().trigger('click'); - - expect(findToggleFollowButton().props('loading')).toBe(true); - - await axios.waitForAll(); - - expect(wrapper.emitted().follow.length).toBe(1); - expect(wrapper.emitted().unfollow).toBeFalsy(); + findToggleFollowButton().trigger('click'); }); - describe('when an error occurs', () => { - beforeEach(() => { - followUser.mockRejectedValue({}); + it('shows an error message', async () => { + await axios.waitForAll(); - findToggleFollowButton().trigger('click'); + expect(createFlash).toHaveBeenCalledWith({ + message: 'An error occurred while trying to follow this user, please try again.', + error: {}, + captureError: true, }); + }); - it('shows an error message', async () => { - await axios.waitForAll(); + it('emits no events', async () => { + await axios.waitForAll(); - expect(createFlash).toHaveBeenCalledWith({ - message: 'An error occurred while trying to follow this user, please try again.', - error: {}, - captureError: true, - }); - }); - - it('emits no events', async () => { - await axios.waitForAll(); - - expect(wrapper.emitted().follow).toBe(undefined); - expect(wrapper.emitted().unfollow).toBe(undefined); - }); + expect(wrapper.emitted().follow).toBe(undefined); + expect(wrapper.emitted().unfollow).toBe(undefined); }); }); }); + }); - describe('when current user follows the user', () => { - beforeEach(() => createWrapper({ user: { ...DEFAULT_PROPS.user, isFollowed: true } })); + describe('when current user follows the user', () => { + beforeEach(() => createWrapper({ user: { ...DEFAULT_PROPS.user, isFollowed: true } })); - it('renders the Unfollow button with the correct variant', () => { - expect(findToggleFollowButton().text()).toBe('Unfollow'); - expect(findToggleFollowButton().props('variant')).toBe('default'); + it('renders the Unfollow button with the correct variant', () => { + expect(findToggleFollowButton().text()).toBe('Unfollow'); + expect(findToggleFollowButton().props('variant')).toBe('default'); + }); + + describe('when clicking', () => { + it('unfollows the user', async () => { + unfollowUser.mockResolvedValue({}); + + findToggleFollowButton().trigger('click'); + + await axios.waitForAll(); + + expect(wrapper.emitted().follow).toBe(undefined); + expect(wrapper.emitted().unfollow.length).toBe(1); }); - describe('when clicking', () => { - it('unfollows the user', async () => { - unfollowUser.mockResolvedValue({}); + describe('when an error occurs', () => { + beforeEach(async () => { + unfollowUser.mockRejectedValue({}); findToggleFollowButton().trigger('click'); await axios.waitForAll(); + }); + it('shows an error message', () => { + expect(createFlash).toHaveBeenCalledWith({ + message: 'An error occurred while trying to unfollow this user, please try again.', + error: {}, + captureError: true, + }); + }); + + it('emits no events', () => { expect(wrapper.emitted().follow).toBe(undefined); - expect(wrapper.emitted().unfollow.length).toBe(1); + expect(wrapper.emitted().unfollow).toBe(undefined); }); - - describe('when an error occurs', () => { - beforeEach(async () => { - unfollowUser.mockRejectedValue({}); - - findToggleFollowButton().trigger('click'); - - await axios.waitForAll(); - }); - - it('shows an error message', () => { - expect(createFlash).toHaveBeenCalledWith({ - message: 'An error occurred while trying to unfollow this user, please try again.', - error: {}, - captureError: true, - }); - }); - - it('emits no events', () => { - expect(wrapper.emitted().follow).toBe(undefined); - expect(wrapper.emitted().unfollow).toBe(undefined); - }); - }); - }); - }); - - describe('when the current user is the user', () => { - beforeEach(() => { - gon.current_username = DEFAULT_PROPS.user.username; - createWrapper(); - }); - - it("doesn't render the toggle follow button", () => { - expect(findToggleFollowButton().exists()).toBe(false); - }); - }); - - describe('when API does not support `isFollowed`', () => { - beforeEach(() => { - const user = { - ...DEFAULT_PROPS.user, - isFollowed: undefined, - }; - - createWrapper({ user }); - }); - - it('does not render the toggle follow button', () => { - expect(findToggleFollowButton().exists()).toBe(false); }); }); }); - describe('follow actions with `followInUserPopover` flag disabled', () => { - beforeEach(() => createWrapper({}, { followInUserPopover: false })); + describe('when the current user is the user', () => { + beforeEach(() => { + gon.current_username = DEFAULT_PROPS.user.username; + createWrapper(); + }); - it('doesn’t render the toggle follow button', () => { + it("doesn't render the toggle follow button", () => { + expect(findToggleFollowButton().exists()).toBe(false); + }); + }); + + describe('when API does not support `isFollowed`', () => { + beforeEach(() => { + const user = { + ...DEFAULT_PROPS.user, + isFollowed: undefined, + }; + + createWrapper({ user }); + }); + + it('does not render the toggle follow button', () => { expect(findToggleFollowButton().exists()).toBe(false); }); }); diff --git a/spec/graphql/resolvers/package_pipelines_resolver_spec.rb b/spec/graphql/resolvers/package_pipelines_resolver_spec.rb index 26ed4ba8990..a52dee59bc6 100644 --- a/spec/graphql/resolvers/package_pipelines_resolver_spec.rb +++ b/spec/graphql/resolvers/package_pipelines_resolver_spec.rb @@ -33,138 +33,115 @@ RSpec.describe Resolvers::PackagePipelinesResolver do end end - shared_examples 'returning the expected pipelines' do + it 'contains the expected pipelines' do + expect_to_contain_exactly(*pipelines) + end + + context 'with valid after' do + let(:pagination_args) { { first: 1, after: encode_cursor(id: pipelines[1].id) } } + it 'contains the expected pipelines' do - expect_to_contain_exactly(*pipelines) + expect_to_contain_exactly(pipelines[0]) + end + end + + context 'with valid before' do + let(:pagination_args) { { last: 1, before: encode_cursor(id: pipelines[1].id) } } + + it 'contains the expected pipelines' do + expect_to_contain_exactly(pipelines[2]) + end + end + + context 'with invalid after' do + let(:pagination_args) { { first: 1, after: 'not_json_string' } } + + it 'generates an argument error' do + expect(returned_errors).to include('Please provide a valid cursor') + end + end + + context 'with invalid after key' do + let(:pagination_args) { { first: 1, after: encode_cursor(foo: 3) } } + + it 'generates an argument error' do + expect(returned_errors).to include('Please provide a valid cursor') + end + end + + context 'with invalid before' do + let(:pagination_args) { { last: 1, before: 'not_json_string' } } + + it 'generates an argument error' do + expect(returned_errors).to include('Please provide a valid cursor') + end + end + + context 'with invalid before key' do + let(:pagination_args) { { last: 1, before: encode_cursor(foo: 3) } } + + it 'generates an argument error' do + expect(returned_errors).to include('Please provide a valid cursor') + end + end + + context 'with unauthorized user' do + let_it_be(:user) { create(:user) } + + it 'returns nothing' do + expect(returned_pipelines).to be_nil + end + end + + context 'with many packages' do + let_it_be_with_reload(:other_package) { create(:package, project: package.project) } + let_it_be(:other_pipelines) { create_list(:ci_pipeline, 3, project: package.project) } + + let(:returned_pipelines) do + graphql_dig_at(subject, 'data', 'project', 'packages', 'nodes', 'pipelines', 'nodes') end - context 'with valid after' do - let(:pagination_args) { { first: 1, after: encode_cursor(id: pipelines[1].id) } } - - it 'contains the expected pipelines' do - expect_to_contain_exactly(pipelines[0]) - end - end - - context 'with valid before' do - let(:pagination_args) { { last: 1, before: encode_cursor(id: pipelines[1].id) } } - - it 'contains the expected pipelines' do - expect_to_contain_exactly(pipelines[2]) - end - end - - context 'with invalid after' do - let(:pagination_args) { { first: 1, after: 'not_json_string' } } - - it 'generates an argument error' do - expect(returned_errors).to include('Please provide a valid cursor') - end - end - - context 'with invalid after key' do - let(:pagination_args) { { first: 1, after: encode_cursor(foo: 3) } } - - it 'generates an argument error' do - expect(returned_errors).to include('Please provide a valid cursor') - end - end - - context 'with invalid before' do - let(:pagination_args) { { last: 1, before: 'not_json_string' } } - - it 'generates an argument error' do - expect(returned_errors).to include('Please provide a valid cursor') - end - end - - context 'with invalid before key' do - let(:pagination_args) { { last: 1, before: encode_cursor(foo: 3) } } - - it 'generates an argument error' do - expect(returned_errors).to include('Please provide a valid cursor') - end - end - - context 'with unauthorized user' do - let_it_be(:user) { create(:user) } - - it 'returns nothing' do - expect(returned_pipelines).to be_nil - end - end - - context 'with many packages' do - let_it_be_with_reload(:other_package) { create(:package, project: package.project) } - let_it_be(:other_pipelines) { create_list(:ci_pipeline, 3, project: package.project) } - - let(:returned_pipelines) do - graphql_dig_at(subject, 'data', 'project', 'packages', 'nodes', 'pipelines', 'nodes') - end - - let(:query) do - pipelines_query = query_graphql_field('pipelines', pagination_args, 'nodes { id }') - <<~QUERY - { - project(fullPath: "#{package.project.full_path}") { - packages { - nodes { #{pipelines_query} } - } + let(:query) do + pipelines_query = query_graphql_field('pipelines', pagination_args, 'nodes { id }') + <<~QUERY + { + project(fullPath: "#{package.project.full_path}") { + packages { + nodes { #{pipelines_query} } } } - QUERY - end - - before do - other_pipelines.each do |pipeline| - create(:package_build_info, package: other_package, pipeline: pipeline) - end - end - - it 'contains the expected pipelines' do - expect_to_contain_exactly(*(pipelines + other_pipelines)) - end - - it 'handles n+1 situations' do - control = ActiveRecord::QueryRecorder.new do - GitlabSchema.execute(query, context: { current_user: user }) - end - - create_package_with_pipelines(package.project) - - expectation = expect { GitlabSchema.execute(query, context: { current_user: user }) } - - if Feature.enabled?(:packages_graphql_pipelines_resolver) - expectation.not_to exceed_query_limit(control) - else - expectation.to exceed_query_limit(control) - end - end - - def create_package_with_pipelines(project) - extra_package = create(:package, project: project) - create_list(:ci_pipeline, 3, project: project).each do |pipeline| - create(:package_build_info, package: extra_package, pipeline: pipeline) - end - end + } + QUERY end - end - context 'with packages_graphql_pipelines_resolver enabled' do before do - expect_detect_mode([:new_finder]) + other_pipelines.each do |pipeline| + create(:package_build_info, package: other_package, pipeline: pipeline) + end end - it_behaves_like 'returning the expected pipelines' - end - - context 'with packages_graphql_pipelines_resolver disabled' do - before do - stub_feature_flags(packages_graphql_pipelines_resolver: false) - expect_detect_mode([:old_finder, :object_field]) + it 'contains the expected pipelines' do + expect_to_contain_exactly(*(pipelines + other_pipelines)) end - it_behaves_like 'returning the expected pipelines' + it 'handles n+1 situations' do + control = ActiveRecord::QueryRecorder.new do + GitlabSchema.execute(query, context: { current_user: user }) + end + + create_package_with_pipelines(package.project) + + expectation = expect { GitlabSchema.execute(query, context: { current_user: user }) } + + expectation.not_to exceed_query_limit(control) + end + + def create_package_with_pipelines(project) + extra_package = create(:package, project: project) + create_list(:ci_pipeline, 3, project: project).each do |pipeline| + create(:package_build_info, package: extra_package, pipeline: pipeline) + end + end end def encode_cursor(json) @@ -178,18 +155,6 @@ RSpec.describe Resolvers::PackagePipelinesResolver do entities = pipelines.map { |pipeline| a_graphql_entity_for(pipeline) } expect(returned_pipelines).to match_array(entities) end - - def expect_detect_mode(modes) - allow_next_instance_of(described_class) do |resolver| - detect_mode_method = resolver.method(:detect_mode) - allow(resolver).to receive(:detect_mode) do - result = detect_mode_method.call - - expect(modes).to include(result) - result - end - end - end end describe '.field options' do diff --git a/spec/helpers/integrations_helper_spec.rb b/spec/helpers/integrations_helper_spec.rb index 3bedc1d8aec..dccbc110be6 100644 --- a/spec/helpers/integrations_helper_spec.rb +++ b/spec/helpers/integrations_helper_spec.rb @@ -62,6 +62,7 @@ RSpec.describe IntegrationsHelper do :enable_comments, :comment_detail, :learn_more_path, + :about_pricing_url, :trigger_events, :fields, :inherit_from_id, diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index ffe2e71b50f..04fe1fad10e 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -2228,12 +2228,17 @@ RSpec.describe Gitlab::Database::MigrationHelpers do end describe '#ensure_batched_background_migration_is_finished' do + let(:job_class_name) { 'CopyColumnUsingBackgroundMigrationJob' } + let(:table) { :events } + let(:column_name) { :id } + let(:job_arguments) { [["id"], ["id_convert_to_bigint"], nil] } + let(:configuration) do { - job_class_name: 'CopyColumnUsingBackgroundMigrationJob', - table_name: :events, - column_name: :id, - job_arguments: [["id"], ["id_convert_to_bigint"], nil] + job_class_name: job_class_name, + table_name: table, + column_name: column_name, + job_arguments: job_arguments } end @@ -2242,6 +2247,10 @@ RSpec.describe Gitlab::Database::MigrationHelpers do it 'raises an error when migration exists and is not marked as finished' do create(:batched_background_migration, :active, configuration) + allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner| + allow(runner).to receive(:finalize).with(job_class_name, table, column_name, job_arguments).and_return(false) + end + expect { ensure_batched_background_migration_is_finished } .to raise_error "Expected batched background migration for the given configuration to be marked as 'finished', but it is 'active':" \ "\t#{configuration}" \ @@ -2269,6 +2278,28 @@ RSpec.describe Gitlab::Database::MigrationHelpers do expect { ensure_batched_background_migration_is_finished } .not_to raise_error end + + it 'finalizes the migration' do + migration = create(:batched_background_migration, :active, configuration) + + allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner| + expect(runner).to receive(:finalize).with(job_class_name, table, column_name, job_arguments).and_return(migration.finish!) + end + + ensure_batched_background_migration_is_finished + end + + context 'when the flag finalize is false' do + it 'does not finalize the migration' do + create(:batched_background_migration, :active, configuration) + + allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner| + expect(runner).not_to receive(:finalize).with(job_class_name, table, column_name, job_arguments) + end + + expect { model.ensure_batched_background_migration_is_finished(**configuration.merge(finalize: false)) }.to raise_error(RuntimeError) + end + end end describe '#index_exists_by_name?' do diff --git a/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb b/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb index f9347a174c4..70889369af1 100644 --- a/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migrations/batched_background_migration_helpers_spec.rb @@ -163,4 +163,24 @@ RSpec.describe Gitlab::Database::Migrations::BatchedBackgroundMigrationHelpers d end end end + + describe '#finalize_batched_background_migration' do + let!(:batched_migration) { create(:batched_background_migration, job_class_name: 'MyClass', table_name: :projects, column_name: :id, job_arguments: []) } + + it 'finalizes the migration' do + allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner| + expect(runner).to receive(:finalize).with('MyClass', :projects, :id, []) + end + + migration.finalize_batched_background_migration(job_class_name: 'MyClass', table_name: :projects, column_name: :id, job_arguments: []) + end + + context 'when the migration does not exist' do + it 'raises an exception' do + expect do + migration.finalize_batched_background_migration(job_class_name: 'MyJobClass', table_name: :projects, column_name: :id, job_arguments: []) + end.to raise_error(RuntimeError, 'Could not find batched background migration') + end + end + end end diff --git a/spec/migrations/finalize_project_namespaces_backfill_spec.rb b/spec/migrations/finalize_project_namespaces_backfill_spec.rb index f70f1612f9d..56f3b0f6ba5 100644 --- a/spec/migrations/finalize_project_namespaces_backfill_spec.rb +++ b/spec/migrations/finalize_project_namespaces_backfill_spec.rb @@ -9,9 +9,11 @@ RSpec.describe FinalizeProjectNamespacesBackfill, :migration do let_it_be(:migration) { described_class::MIGRATION } describe '#up' do - shared_examples 'raises migration not finished exception' do - it 'raises exception' do - expect { migrate! }.to raise_error(/Expected batched background migration for the given configuration to be marked as 'finished'/) + shared_examples 'finalizes the migration' do + it 'finalizes the migration' do + allow_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |runner| + expect(runner).to receive(:finalize).with('"ProjectNamespaces::BackfillProjectNamespaces"', :projects, :id, [nil, "up"]) + end end end @@ -61,7 +63,7 @@ RSpec.describe FinalizeProjectNamespacesBackfill, :migration do project_namespace_backfill.update!(status: status) end - it_behaves_like 'raises migration not finished exception' + it_behaves_like 'finalizes the migration' end end end diff --git a/spec/models/integrations/jira_spec.rb b/spec/models/integrations/jira_spec.rb index a9cac5a982a..061c770a61a 100644 --- a/spec/models/integrations/jira_spec.rb +++ b/spec/models/integrations/jira_spec.rb @@ -126,6 +126,11 @@ RSpec.describe Integrations::Jira do it 'includes SECTION_TYPE_JIRA_ISSUES' do expect(sections).to include(described_class::SECTION_TYPE_JIRA_ISSUES) end + + it 'section SECTION_TYPE_JIRA_ISSUES has `plan` attribute' do + jira_issues_section = integration.sections.find { |s| s[:type] == described_class::SECTION_TYPE_JIRA_ISSUES } + expect(jira_issues_section[:plan]).to eq('premium') + end end context 'when project_level? is false' do diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 6fafbb90bb4..8328b454122 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -151,6 +151,13 @@ RSpec.describe API::Environments do expect(json_response).to be_an Array expect(json_response.size).to eq(0) end + + it 'returns a 400 status code with invalid states' do + get api("/projects/#{project.id}/environments?states=test", user) + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to include('Requested states are invalid') + end end end diff --git a/spec/views/projects/tags/index.html.haml_spec.rb b/spec/views/projects/tags/index.html.haml_spec.rb index ae59c1aa4b2..664f05428f4 100644 --- a/spec/views/projects/tags/index.html.haml_spec.rb +++ b/spec/views/projects/tags/index.html.haml_spec.rb @@ -27,7 +27,7 @@ RSpec.describe 'projects/tags/index.html.haml' do it 'renders links to the Releases page for tags associated with a release' do render - expect(rendered).to have_link(release.name, href: project_releases_path(project, anchor: release.tag)) + expect(rendered).to have_link(release.name, href: project_release_path(project, release.tag)) end context 'when the most recent build for a tag has artifacts' do diff --git a/yarn.lock b/yarn.lock index 54aa0ac7550..6894334ef66 100644 --- a/yarn.lock +++ b/yarn.lock @@ -963,15 +963,15 @@ stylelint-declaration-strict-value "1.8.0" stylelint-scss "4.1.0" -"@gitlab/svgs@2.12.0": - version "2.12.0" - resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-2.12.0.tgz#f4825c3e9b95d219935b62f71143f9f2fc48b0fe" - integrity sha512-FHd0ImNyj8ZIiH3Ah2esxbxNQlXMSezqkcUe0nm+aWkSFsg6MbMcRYa6WvOKq5CbAgWpH1TbDqokkJ421YUtEg== +"@gitlab/svgs@2.14.0": + version "2.14.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-2.14.0.tgz#92b36bc98ccbed49a4dbca310862146275091cb2" + integrity sha512-U9EYmEIiTMl7R3X5DmCrw6fz7gz8c1kjvQtaF6HfJ15xDtR7trRAyCNbn3z7YGk1QJ8Cv/Ifw2/T5SxXwYd7dw== -"@gitlab/ui@40.0.0": - version "40.0.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-40.0.0.tgz#5a48a2c1ad509317ff0fde07070bd2070c82bd1a" - integrity sha512-LLnhju89i3usX66lSWQod222EFQjkBkiHG6frrDotW2PnCE6wB/xjlXumwpi93zfdyvIHS12cWPZwNa3ayiLfQ== +"@gitlab/ui@40.2.0": + version "40.2.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-40.2.0.tgz#798630112809816afa0d37f58d567e8f1ad53f8e" + integrity sha512-3AbCh0UVB5xEUoPrwr2YkzM9IrNOW3LFmyYCXEuVTp7whHyHG14T+sty3YDQWlOFjjEdMD4fU2iXveq2V3cq0A== dependencies: "@popperjs/core" "^2.11.2" bootstrap-vue "2.20.1"