summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-04-20 00:12:57 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-04-20 00:12:57 +0000
commit8277a5b4ba05fe11174397c689c66cef5c06fc38 (patch)
treec4abbec1509476efbb14c040045ca163042de3bf
parentf46a8dbf1a0999e27dfeddd258096ef97def8d64 (diff)
downloadgitlab-ce-8277a5b4ba05fe11174397c689c66cef5c06fc38.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/release-environments/main.gitlab-ci.yml2
-rw-r--r--.rubocop_todo/layout/line_length.yml1
-rw-r--r--.rubocop_todo/lint/unused_block_argument.yml1
-rw-r--r--.rubocop_todo/style/if_unless_modifier.yml1
-rw-r--r--.rubocop_todo/style/redundant_freeze.yml1
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile6
-rw-r--r--Gemfile.checksum8
-rw-r--r--Gemfile.lock18
-rw-r--r--app/assets/javascripts/issues/constants.js5
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue165
-rw-r--r--app/assets/javascripts/issues/show/components/new_header_actions_popover.vue82
-rw-r--r--app/assets/javascripts/issues/show/constants.js2
-rw-r--r--app/assets/javascripts/issues/show/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue14
-rw-r--r--app/assets/javascripts/right_sidebar.js6
-rw-r--r--app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue27
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue15
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js17
-rw-r--r--app/assets/stylesheets/components/detail_page.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/issuable.scss10
-rw-r--r--app/controllers/projects/incidents_controller.rb1
-rw-r--r--app/controllers/projects/issues_controller.rb1
-rw-r--r--app/helpers/issuables_helper.rb4
-rw-r--r--app/helpers/issues_helper.rb5
-rw-r--r--app/helpers/labels_helper.rb21
-rw-r--r--app/helpers/merge_requests_helper.rb10
-rw-r--r--app/helpers/nav_helper.rb2
-rw-r--r--app/models/concerns/protected_ref_access.rb6
-rw-r--r--app/models/packages/npm/metadatum.rb2
-rw-r--r--app/models/sent_notification.rb2
-rw-r--r--app/services/issues/update_service.rb2
-rw-r--r--app/services/packages/npm/deprecate_package_service.rb78
-rw-r--r--app/services/system_note_service.rb4
-rw-r--r--app/services/system_notes/issuables_service.rb6
-rw-r--r--app/views/admin/labels/new.html.haml1
-rw-r--r--app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml3
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml31
-rw-r--r--app/views/shared/issue_type/_details_header.html.haml2
-rw-r--r--app/views/shared/labels/_form.html.haml4
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml2
-rw-r--r--app/workers/all_queues.yml9
-rw-r--r--app/workers/packages/npm/deprecate_package_worker.rb22
-rw-r--r--config/initializers/doorkeeper_openid_connect_patch.rb23
-rw-r--r--danger/specs/Dangerfile8
-rw-r--r--db/post_migrate/20230419164438_change_code_suggestions_default_false_in_namespace_settings.rb7
-rw-r--r--db/schema_migrations/202304191644381
-rw-r--r--db/structure.sql2
-rw-r--r--doc/development/database/adding_database_indexes.md32
-rw-r--r--doc/development/database_review.md2
-rw-r--r--doc/development/documentation/styleguide/index.md9
-rw-r--r--doc/development/migration_style_guide.md83
-rw-r--r--doc/operations/incident_management/manage_incidents.md20
-rw-r--r--doc/user/group/epics/img/button_close_epic.pngbin0 -> 13850 bytes
-rw-r--r--doc/user/group/epics/manage_epics.md11
-rw-r--r--doc/user/packages/npm_registry/index.md35
-rw-r--r--doc/user/profile/notifications.md2
-rw-r--r--doc/user/project/issues/create_issues.md2
-rw-r--r--doc/user/project/issues/managing_issues.md12
-rw-r--r--doc/user/project/merge_requests/index.md5
-rw-r--r--doc/user/report_abuse.md4
-rw-r--r--lib/api/npm_project_packages.rb26
-rw-r--r--locale/gitlab.pot75
-rw-r--r--spec/features/ide/user_opens_merge_request_spec.rb4
-rw-r--r--spec/features/incidents/incident_details_spec.rb2
-rw-r--r--spec/features/issues/discussion_lock_spec.rb1
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb3
-rw-r--r--spec/features/issues/issue_detail_spec.rb5
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb4
-rw-r--r--spec/features/issues/user_edits_issue_spec.rb3
-rw-r--r--spec/features/issues/user_toggles_subscription_spec.rb4
-rw-r--r--spec/features/merge_request/user_manages_subscription_spec.rb4
-rw-r--r--spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb3
-rw-r--r--spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb2
-rw-r--r--spec/features/projects/issuable_templates_spec.rb2
-rw-r--r--spec/features/reportable_note/issue_spec.rb4
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js317
-rw-r--r--spec/frontend/issues/show/components/new_header_actions_popover_spec.js77
-rw-r--r--spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js15
-rw-r--r--spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js21
-rw-r--r--spec/frontend/vue_shared/issuable/list/mock_data.js6
-rw-r--r--spec/helpers/issues_helper_spec.rb10
-rw-r--r--spec/initializers/doorkeeper_openid_connect_patch_spec.rb74
-rw-r--r--spec/lib/gitlab/jwt_authenticatable_spec.rb8
-rw-r--r--spec/lib/gitlab/middleware/multipart_spec.rb4
-rw-r--r--spec/lib/json_web_token/hmac_token_spec.rb4
-rw-r--r--spec/models/packages/npm/metadatum_spec.rb14
-rw-r--r--spec/requests/api/npm_project_packages_spec.rb54
-rw-r--r--spec/services/issues/update_service_spec.rb2
-rw-r--r--spec/services/packages/npm/deprecate_package_service_spec.rb115
-rw-r--r--spec/services/system_note_service_spec.rb4
-rw-r--r--spec/services/system_notes/issuables_service_spec.rb24
-rw-r--r--spec/tooling/danger/specs/feature_category_suggestion_spec.rb99
-rw-r--r--spec/tooling/danger/specs/match_with_array_suggestion_spec.rb99
-rw-r--r--spec/tooling/danger/specs/project_factory_suggestion_spec.rb104
-rw-r--r--spec/tooling/danger/specs_spec.rb271
-rw-r--r--spec/workers/packages/npm/deprecate_package_worker_spec.rb35
-rw-r--r--tooling/danger/specs.rb88
-rw-r--r--tooling/danger/specs/feature_category_suggestion.rb42
-rw-r--r--tooling/danger/specs/match_with_array_suggestion.rb17
-rw-r--r--tooling/danger/specs/project_factory_suggestion.rb38
-rw-r--r--tooling/danger/suggestion.rb39
103 files changed, 1252 insertions, 1338 deletions
diff --git a/.gitlab/ci/release-environments/main.gitlab-ci.yml b/.gitlab/ci/release-environments/main.gitlab-ci.yml
index 982329646a7..aa6afee57ae 100644
--- a/.gitlab/ci/release-environments/main.gitlab-ci.yml
+++ b/.gitlab/ci/release-environments/main.gitlab-ci.yml
@@ -89,6 +89,6 @@ release-environments-deploy:
VERSIONS: "${VERSIONS}"
ENVIRONMENT: "${ENVIRONMENT}"
trigger:
- project: gl-infra/release-environments
+ project: gitlab-com/gl-infra/release-environments
branch: main
strategy: depend
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index 376671613b9..ad0272376a7 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -5477,7 +5477,6 @@ Layout/LineLength:
- 'spec/tooling/danger/product_intelligence_spec.rb'
- 'spec/tooling/danger/project_helper_spec.rb'
- 'spec/tooling/danger/sidekiq_queues_spec.rb'
- - 'spec/tooling/danger/specs_spec.rb'
- 'spec/tooling/lib/tooling/kubernetes_client_spec.rb'
- 'spec/tooling/lib/tooling/test_map_generator_spec.rb'
- 'spec/tooling/quality/test_level_spec.rb'
diff --git a/.rubocop_todo/lint/unused_block_argument.yml b/.rubocop_todo/lint/unused_block_argument.yml
index a70c3823c1d..c09dc939ef4 100644
--- a/.rubocop_todo/lint/unused_block_argument.yml
+++ b/.rubocop_todo/lint/unused_block_argument.yml
@@ -438,5 +438,4 @@ Lint/UnusedBlockArgument:
- 'spec/tooling/lib/tooling/find_codeowners_spec.rb'
- 'spec/tooling/rspec_flaky/config_spec.rb'
- 'spec/workers/projects/git_garbage_collect_worker_spec.rb'
- - 'tooling/danger/specs.rb'
- 'tooling/lib/tooling/find_codeowners.rb'
diff --git a/.rubocop_todo/style/if_unless_modifier.yml b/.rubocop_todo/style/if_unless_modifier.yml
index 00e7957ed9e..a4bd4a18854 100644
--- a/.rubocop_todo/style/if_unless_modifier.yml
+++ b/.rubocop_todo/style/if_unless_modifier.yml
@@ -385,7 +385,6 @@ Style/IfUnlessModifier:
- 'config/routes.rb'
- 'danger/database/Dangerfile'
- 'danger/pipeline/Dangerfile'
- - 'danger/specs/Dangerfile'
- 'danger/z_metadata/Dangerfile'
- 'db/migrate/20210909184349_add_index_package_id_id_on_package_files.rb'
- 'db/migrate/20220324175325_add_key_data_to_secure_files.rb'
diff --git a/.rubocop_todo/style/redundant_freeze.yml b/.rubocop_todo/style/redundant_freeze.yml
index 07027d7dd3d..3a0f099fd24 100644
--- a/.rubocop_todo/style/redundant_freeze.yml
+++ b/.rubocop_todo/style/redundant_freeze.yml
@@ -235,7 +235,6 @@ Style/RedundantFreeze:
- 'tooling/danger/config_files.rb'
- 'tooling/danger/customer_success.rb'
- 'tooling/danger/datateam.rb'
- - 'tooling/danger/specs.rb'
- 'tooling/danger/stable_branch.rb'
- 'tooling/lib/tooling/kubernetes_client.rb'
- 'tooling/lib/tooling/mappings/view_to_js_mappings.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 3f97b6fb8cb..828e7b38a39 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-f06cc91443358f405e7a5896dd9e9eb80036efba
+3f8234c4ccbd84051184de2d60cc9ef545420665
diff --git a/Gemfile b/Gemfile
index 232205824da..1c10a075978 100644
--- a/Gemfile
+++ b/Gemfile
@@ -45,8 +45,8 @@ gem 'declarative_policy', '~> 1.1.0'
gem 'devise', '~> 4.8.1'
gem 'devise-pbkdf2-encryptable', '~> 0.0.0', path: 'vendor/gems/devise-pbkdf2-encryptable'
gem 'bcrypt', '~> 3.1', '>= 3.1.14'
-gem 'doorkeeper', '~> 5.5'
-gem 'doorkeeper-openid_connect', '~> 1.8'
+gem 'doorkeeper', '~> 5.6', '>= 5.6.6'
+gem 'doorkeeper-openid_connect', '~> 1.8', '>= 1.8.5'
gem 'rexml', '~> 3.2.5'
gem 'ruby-saml', '~> 1.13.0'
gem 'omniauth', '~> 2.1.0'
@@ -71,7 +71,7 @@ gem 'openid_connect', '= 1.3.0'
gem 'omniauth-salesforce', '~> 1.0.5', path: 'vendor/gems/omniauth-salesforce' # See gem README.md
gem 'omniauth-atlassian-oauth2', '~> 0.2.0'
gem 'rack-oauth2', '~> 1.21.3'
-gem 'jwt', '~> 2.1.0'
+gem 'jwt', '~> 2.5'
# Kerberos authentication. EE-only
gem 'gssapi', '~> 1.3.1', group: :kerberos
diff --git a/Gemfile.checksum b/Gemfile.checksum
index ee4978bb8f0..b7112d5b775 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -29,7 +29,7 @@
{"name":"asciidoctor-kroki","version":"0.8.0","platform":"ruby","checksum":"e53b3f349167cebde990b0098863e8fe98fd235e35263a78c88cc4e0268b1a36"},
{"name":"asciidoctor-plantuml","version":"0.0.16","platform":"ruby","checksum":"407e47cd1186ded5ccc75f0c812e5524c26c571d542247c5132abb8f47bd1793"},
{"name":"ast","version":"2.4.2","platform":"ruby","checksum":"1e280232e6a33754cde542bc5ef85520b74db2aac73ec14acef453784447cc12"},
-{"name":"atlassian-jwt","version":"0.2.0","platform":"ruby","checksum":"52e653e9d6062d7a740c3675b0e79fa08367927c6fc17f5476d1b6b3798c6eb2"},
+{"name":"atlassian-jwt","version":"0.2.1","platform":"ruby","checksum":"2fd2d87418773f2e140c038cb22e049069708aff2bd0a423a7e1740574e97823"},
{"name":"attr_required","version":"1.0.1","platform":"ruby","checksum":"024e10393bd30901e1adf6769bd756b873a5ef7da60f86f8f11066116b5742bc"},
{"name":"autoprefixer-rails","version":"10.2.5.1","platform":"ruby","checksum":"3711d67f1112361c7628847ac192d8aa6f3b8abe47527aee8a69dc8985e798ee"},
{"name":"awesome_print","version":"1.9.2","platform":"ruby","checksum":"e99b32b704acff16d768b3468680793ced40bfdc4537eb07e06a4be11133786e"},
@@ -119,8 +119,8 @@
{"name":"discordrb-webhooks","version":"3.4.2","platform":"ruby","checksum":"cfdba8a4b28236b6ab34e37389f881a59c241aeb5be0a4447249efd4e4383c6e"},
{"name":"docile","version":"1.4.0","platform":"ruby","checksum":"5f1734bde23721245c20c3d723e76c104208e1aa01277a69901ce770f0ebb8d3"},
{"name":"domain_name","version":"0.5.20190701","platform":"ruby","checksum":"000a600454cb4a344769b2f10b531765ea7bd3a304fe47ed12e5ca1eab969851"},
-{"name":"doorkeeper","version":"5.5.4","platform":"ruby","checksum":"7fe233a96f93bf0d5496e2284abf431f38ab465fd65d1972b90cbec7c45b1ea1"},
-{"name":"doorkeeper-openid_connect","version":"1.8.3","platform":"ruby","checksum":"0df2e714508f1f43fdb4669e97b38b90d365a072908427416da943a1a8e00b6e"},
+{"name":"doorkeeper","version":"5.6.6","platform":"ruby","checksum":"2344e86c77770526efcda893b5217aa13d1c7eb1b40de840b58b19eb1ff757e0"},
+{"name":"doorkeeper-openid_connect","version":"1.8.5","platform":"ruby","checksum":"d4ee57687945402843c948cee399c758cdddf04468c42b1fb02a8800dd0627f6"},
{"name":"dotenv","version":"2.7.6","platform":"ruby","checksum":"2451ed5e8e43776d7a787e51d6f8903b98e446146c7ad143d5678cc2c409d547"},
{"name":"dry-configurable","version":"0.12.0","platform":"ruby","checksum":"87a9579a04dfbae73e401d694282800d64bbdb8631cb3e987bfb79b673df7c67"},
{"name":"dry-container","version":"0.7.2","platform":"ruby","checksum":"a071824ba3451048b23500210f96a2b9facd6e46ac687f65e49c75d18786f6da"},
@@ -316,7 +316,7 @@
{"name":"json-jwt","version":"1.15.3","platform":"ruby","checksum":"66db4f14e538a774c15502a5b5b26b1f3e7585481bbb96df490aa74b5c2d6110"},
{"name":"json_schemer","version":"0.2.18","platform":"ruby","checksum":"3362c21efbefdd12ce994e541a1e7fdb86fd267a6541dd8715e8a580fe3b6be6"},
{"name":"jsonpath","version":"1.1.2","platform":"ruby","checksum":"6804124c244d04418218acb85b15c7caa79c592d7d6970195300428458946d3a"},
-{"name":"jwt","version":"2.1.0","platform":"ruby","checksum":"7e7e7ffc1a5ebce628ac7da428341c50615a3a10ac47bb74c22c1cba325613f0"},
+{"name":"jwt","version":"2.5.0","platform":"ruby","checksum":"b835fe55287572e1f65128d6c12d3ff7402bb4652c4565bf3ecdcb974db7954d"},
{"name":"kaminari","version":"1.2.2","platform":"ruby","checksum":"c4076ff9adccc6109408333f87b5c4abbda5e39dc464bd4c66d06d9f73442a3e"},
{"name":"kaminari-actionview","version":"1.2.2","platform":"ruby","checksum":"1330f6fc8b59a4a4ef6a549ff8a224797289ebf7a3a503e8c1652535287cc909"},
{"name":"kaminari-activerecord","version":"1.2.2","platform":"ruby","checksum":"0dd3a67bab356a356f36b3b7236bcb81cef313095365befe8e98057dd2472430"},
diff --git a/Gemfile.lock b/Gemfile.lock
index a40830271f7..cbafe9b60d5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -199,8 +199,8 @@ GEM
asciidoctor-plantuml (0.0.16)
asciidoctor (>= 2.0.17, < 3.0.0)
ast (2.4.2)
- atlassian-jwt (0.2.0)
- jwt (~> 2.1.0)
+ atlassian-jwt (0.2.1)
+ jwt (~> 2.1)
attr_required (1.0.1)
autoprefixer-rails (10.2.5.1)
execjs (> 0)
@@ -398,11 +398,11 @@ GEM
docile (1.4.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
- doorkeeper (5.5.4)
+ doorkeeper (5.6.6)
railties (>= 5)
- doorkeeper-openid_connect (1.8.3)
+ doorkeeper-openid_connect (1.8.5)
doorkeeper (>= 5.5, < 5.7)
- json-jwt (>= 1.15.0)
+ jwt (>= 2.5)
dotenv (2.7.6)
dry-configurable (0.12.0)
concurrent-ruby (~> 1.0)
@@ -857,7 +857,7 @@ GEM
uri_template (~> 0.7)
jsonpath (1.1.2)
multi_json
- jwt (2.1.0)
+ jwt (2.5.0)
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
@@ -1716,8 +1716,8 @@ DEPENDENCIES
diff_match_patch (~> 0.1.0)
diffy (~> 3.4)
discordrb-webhooks (~> 3.4)
- doorkeeper (~> 5.5)
- doorkeeper-openid_connect (~> 1.8)
+ doorkeeper (~> 5.6, >= 5.6.6)
+ doorkeeper-openid_connect (~> 1.8, >= 1.8.5)
duo_api (~> 1.3)
ed25519 (~> 1.3.0)
elasticsearch-api (= 7.13.3)
@@ -1803,7 +1803,7 @@ DEPENDENCIES
js_regex (~> 3.8)
json (~> 2.6.3)
json_schemer (~> 0.2.18)
- jwt (~> 2.1.0)
+ jwt (~> 2.5)
kaminari (~> 1.2.2)
kas-grpc (~> 0.0.2)
knapsack (~> 1.21.1)
diff --git a/app/assets/javascripts/issues/constants.js b/app/assets/javascripts/issues/constants.js
index 371db4eacc3..d35355a8f26 100644
--- a/app/assets/javascripts/issues/constants.js
+++ b/app/assets/javascripts/issues/constants.js
@@ -26,8 +26,3 @@ export const IssuableStatusText = {
[STATUS_MERGED]: __('Merged'),
[STATUS_LOCKED]: __('Open'),
};
-
-export const IssuableTypeText = {
- [TYPE_ISSUE]: __('issue'),
- [TYPE_MERGE_REQUEST]: __('merge request'),
-};
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index b929c4dbae0..84def374d13 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -2,36 +2,23 @@
import {
GlButton,
GlDropdown,
- GlDropdownDivider,
GlDropdownItem,
GlLink,
GlModal,
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { mapActions, mapGetters, mapState } from 'vuex';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
-import { STATUS_CLOSED, TYPE_INCIDENT, TYPE_ISSUE, IssuableTypeText } from '~/issues/constants';
-import {
- ISSUE_STATE_EVENT_CLOSE,
- ISSUE_STATE_EVENT_REOPEN,
- NEW_ACTIONS_POPOVER_KEY,
-} from '~/issues/show/constants';
+import { STATUS_CLOSED, TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
+import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show/constants';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
-import { getCookie, parseBoolean, setCookie } from '~/lib/utils/common_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { s__, __, sprintf } from '~/locale';
import eventHub from '~/notes/event_hub';
import Tracking from '~/tracking';
-import toast from '~/vue_shared/plugins/global_toast';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
-import NewHeaderActionsPopover from '~/issues/show/components/new_header_actions_popover.vue';
-import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
-import IssuableLockForm from '~/sidebar/components/lock/issuable_lock_form.vue';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
import issuesEventHub from '../event_hub';
import promoteToEpicMutation from '../queries/promote_to_epic.mutation.graphql';
import updateIssueMutation from '../queries/update_issue.mutation.graphql';
@@ -57,27 +44,21 @@ export default {
'The issue was successfully promoted to an epic. Redirecting to epic...',
),
reportAbuse: __('Report abuse to administrator'),
- referenceFetchError: __('An error occurred while fetching reference'),
- copyReferenceText: __('Copy reference'),
},
components: {
DeleteIssueModal,
GlButton,
GlDropdown,
- GlDropdownDivider,
GlDropdownItem,
GlLink,
GlModal,
AbuseCategorySelector,
- NewHeaderActionsPopover,
- SidebarSubscriptionsWidget,
- IssuableLockForm,
},
directives: {
GlModal: GlModalDirective,
GlTooltip: GlTooltipDirective,
},
- mixins: [trackingMixin, glFeatureFlagMixin()],
+ mixins: [trackingMixin],
inject: {
canCreateIssue: {
default: false,
@@ -124,46 +105,15 @@ export default {
reportedFromUrl: {
default: '',
},
- issuableEmailAddress: {
- default: '',
- },
- fullPath: {
- default: '',
- },
},
data() {
return {
isReportAbuseDrawerOpen: false,
};
},
- apollo: {
- issuableReference: {
- query: issueReferenceQuery,
- variables() {
- return {
- fullPath: this.fullPath,
- iid: this.iid,
- };
- },
- update(data) {
- return data.workspace?.issuable?.reference || '';
- },
- skip() {
- return !this.isMrSidebarMoved;
- },
- error(error) {
- createAlert({ message: this.$options.i18n.referenceFetchError });
- Sentry.captureException(error);
- },
- },
- },
computed: {
...mapState(['isToggleStateButtonLoading']),
...mapGetters(['openState', 'getBlockedByIssues']),
- ...mapGetters(['getNoteableData']),
- isLocked() {
- return this.getNoteableData.discussion_locked;
- },
isClosed() {
return this.openState === STATUS_CLOSED;
},
@@ -207,17 +157,6 @@ export default {
hasMobileDropdown() {
return this.hasDesktopDropdown || this.showToggleIssueStateButton;
},
- copyMailAddressText() {
- return sprintf(__('Copy %{issueType} email address'), {
- issueType: IssuableTypeText[this.issueType],
- });
- },
- isMrSidebarMoved() {
- return this.glFeatures.movedMrSidebar;
- },
- showLockIssueOption() {
- return this.isMrSidebarMoved && this.issueType === TYPE_ISSUE;
- },
},
created() {
eventHub.$on('toggle.issuable.state', this.toggleIssueState);
@@ -227,7 +166,6 @@ export default {
},
methods: {
...mapActions(['toggleStateButtonLoading']),
- ...mapActions(['updateLockedAttribute']),
toggleIssueState() {
if (!this.isClosed && this.getBlockedByIssues?.length) {
this.$refs.blockedByIssuesModal.show();
@@ -306,19 +244,7 @@ export default {
edit() {
issuesEventHub.$emit('open.form');
},
- dismissPopover() {
- if (this.isMrSidebarMoved && !parseBoolean(getCookie(`${NEW_ACTIONS_POPOVER_KEY}`))) {
- setCookie(NEW_ACTIONS_POPOVER_KEY, true);
- }
- },
- copyReference() {
- toast(__('Reference copied'));
- },
- copyEmailAddress() {
- toast(__('Email address copied'));
- },
},
- TYPE_ISSUE,
};
</script>
@@ -333,21 +259,6 @@ export default {
data-testid="mobile-dropdown"
:loading="isToggleStateButtonLoading"
>
- <template v-if="isMrSidebarMoved">
- <sidebar-subscriptions-widget
- :iid="String(iid)"
- :full-path="fullPath"
- :issuable-type="$options.TYPE_ISSUE"
- data-testid="notification-toggle"
- />
-
- <gl-dropdown-divider />
- </template>
-
- <template v-if="showLockIssueOption">
- <issuable-lock-form :is-editable="false" data-testid="lock-issue-toggle" />
- </template>
-
<gl-dropdown-item v-if="canUpdateIssue" @click="edit">
{{ $options.i18n.edit }}
</gl-dropdown-item>
@@ -364,21 +275,9 @@ export default {
<gl-dropdown-item v-if="canPromoteToEpic" @click="promoteToEpic">
{{ __('Promote to epic') }}
</gl-dropdown-item>
- <template v-if="isMrSidebarMoved">
- <gl-dropdown-item
- :data-clipboard-text="issuableReference"
- data-testid="copy-reference"
- @click="copyReference"
- >{{ $options.i18n.copyReferenceText }}</gl-dropdown-item
- >
- <gl-dropdown-item
- v-if="issuableEmailAddress"
- :data-clipboard-text="issuableEmailAddress"
- data-testid="copy-email"
- @click="copyEmailAddress"
- >{{ copyMailAddressText }}</gl-dropdown-item
- >
- </template>
+ <gl-dropdown-item v-if="!isIssueAuthor" @click="toggleReportAbuseDrawer(true)">
+ {{ $options.i18n.reportAbuse }}
+ </gl-dropdown-item>
<gl-dropdown-item
v-if="canReportSpam"
:href="submitAsSpamPath"
@@ -388,7 +287,6 @@ export default {
{{ __('Submit as spam') }}
</gl-dropdown-item>
<template v-if="canDestroyIssue">
- <gl-dropdown-divider />
<gl-dropdown-item
v-gl-modal="$options.deleteModalId"
variant="danger"
@@ -397,13 +295,6 @@ export default {
{{ deleteButtonText }}
</gl-dropdown-item>
</template>
- <gl-dropdown-item
- v-if="!isIssueAuthor"
- data-testid="report-abuse-item"
- @click="toggleReportAbuseDrawer(true)"
- >
- {{ $options.i18n.reportAbuse }}
- </gl-dropdown-item>
</gl-dropdown>
<gl-button
@@ -431,7 +322,6 @@ export default {
<gl-dropdown
v-if="hasDesktopDropdown"
- id="new-actions-header-dropdown"
v-gl-tooltip.hover
class="gl-display-none gl-sm-display-inline-flex! gl-sm-ml-3"
icon="ellipsis_v"
@@ -444,19 +334,7 @@ export default {
data-testid="desktop-dropdown"
no-caret
right
- @shown="dismissPopover"
>
- <template v-if="isMrSidebarMoved">
- <sidebar-subscriptions-widget
- :iid="String(iid)"
- :full-path="fullPath"
- :issuable-type="$options.TYPE_ISSUE"
- data-testid="notification-toggle"
- />
-
- <gl-dropdown-divider />
- </template>
-
<gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath">
{{ newIssueTypeText }}
</gl-dropdown-item>
@@ -468,24 +346,9 @@ export default {
>
{{ __('Promote to epic') }}
</gl-dropdown-item>
- <template v-if="showLockIssueOption">
- <issuable-lock-form :is-editable="false" data-testid="lock-issue-toggle" />
- </template>
- <template v-if="isMrSidebarMoved">
- <gl-dropdown-item
- :data-clipboard-text="issuableReference"
- data-testid="copy-reference"
- @click="copyReference"
- >{{ $options.i18n.copyReferenceText }}</gl-dropdown-item
- >
- <gl-dropdown-item
- v-if="issuableEmailAddress"
- :data-clipboard-text="issuableEmailAddress"
- data-testid="copy-email"
- @click="copyEmailAddress"
- >{{ copyMailAddressText }}</gl-dropdown-item
- >
- </template>
+ <gl-dropdown-item v-if="!isIssueAuthor" @click="toggleReportAbuseDrawer(true)">
+ {{ $options.i18n.reportAbuse }}
+ </gl-dropdown-item>
<gl-dropdown-item
v-if="canReportSpam"
:href="submitAsSpamPath"
@@ -494,8 +357,8 @@ export default {
>
{{ __('Submit as spam') }}
</gl-dropdown-item>
+
<template v-if="canDestroyIssue">
- <gl-dropdown-divider />
<gl-dropdown-item
v-gl-modal="$options.deleteModalId"
variant="danger"
@@ -505,16 +368,8 @@ export default {
{{ deleteButtonText }}
</gl-dropdown-item>
</template>
- <gl-dropdown-item
- v-if="!isIssueAuthor"
- data-testid="report-abuse-item"
- @click="toggleReportAbuseDrawer(true)"
- >
- {{ $options.i18n.reportAbuse }}
- </gl-dropdown-item>
</gl-dropdown>
- <new-header-actions-popover v-if="isMrSidebarMoved" :issue-type="issueType" />
<gl-modal
ref="blockedByIssuesModal"
modal-id="blocked-by-issues-modal"
diff --git a/app/assets/javascripts/issues/show/components/new_header_actions_popover.vue b/app/assets/javascripts/issues/show/components/new_header_actions_popover.vue
deleted file mode 100644
index 8262b3ac0ff..00000000000
--- a/app/assets/javascripts/issues/show/components/new_header_actions_popover.vue
+++ /dev/null
@@ -1,82 +0,0 @@
-<script>
-import { GlPopover, GlButton } from '@gitlab/ui';
-import { s__, sprintf } from '~/locale';
-import { getCookie, parseBoolean, setCookie } from '~/lib/utils/common_utils';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { NEW_ACTIONS_POPOVER_KEY } from '~/issues/show/constants';
-import { IssuableTypeText } from '~/issues/constants';
-
-export default {
- name: 'NewHeaderActionsPopover',
- i18n: {
- popoverText: s__(
- 'HeaderAction|Notifications and other %{issueType} actions have moved to this menu.',
- ),
- confirmButtonText: s__('HeaderAction|Okay!'),
- },
- components: {
- GlPopover,
- GlButton,
- },
- mixins: [glFeatureFlagMixin()],
- props: {
- issueType: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- dismissKey: NEW_ACTIONS_POPOVER_KEY,
- popoverDismissed: parseBoolean(getCookie(`${NEW_ACTIONS_POPOVER_KEY}`)),
- };
- },
- computed: {
- popoverText() {
- return sprintf(this.$options.i18n.popoverText, {
- issueType: IssuableTypeText[this.issueType],
- });
- },
- showPopover() {
- return !this.popoverDismissed && this.isMrSidebarMoved;
- },
- isMrSidebarMoved() {
- return this.glFeatures.movedMrSidebar;
- },
- },
- methods: {
- dismissPopover() {
- this.popoverDismissed = true;
- setCookie(this.dismissKey, this.popoverDismissed);
- },
- },
-};
-</script>
-
-<template>
- <div>
- <gl-popover
- v-if="showPopover"
- target="new-actions-header-dropdown"
- container="viewport"
- placement="left"
- :show="showPopover"
- triggers="manual"
- content="text"
- :css-classes="['gl-p-2 new-header-popover']"
- >
- <template #title>
- <div class="gl-font-base gl-font-weight-normal">
- {{ popoverText }}
- </div>
- </template>
- <gl-button
- data-testid="confirm-button"
- variant="confirm"
- type="submit"
- @click="dismissPopover"
- >{{ $options.i18n.confirmButtonText }}</gl-button
- >
- </gl-popover>
- </div>
-</template>
diff --git a/app/assets/javascripts/issues/show/constants.js b/app/assets/javascripts/issues/show/constants.js
index 6320e4ef266..4d8c11f9669 100644
--- a/app/assets/javascripts/issues/show/constants.js
+++ b/app/assets/javascripts/issues/show/constants.js
@@ -17,5 +17,3 @@ export const issueState = {
issueType: undefined,
isDirty: false,
};
-
-export const NEW_ACTIONS_POPOVER_KEY = 'new-actions-popover-viewed';
diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js
index 100abcbe1e5..e677328cd2e 100644
--- a/app/assets/javascripts/issues/show/index.js
+++ b/app/assets/javascripts/issues/show/index.js
@@ -174,8 +174,6 @@ export function initHeaderActions(store, type = '') {
reportedUserId: parseInt(el.dataset.reportedUserId, 10),
reportedFromUrl: el.dataset.reportedFromUrl,
submitAsSpamPath: el.dataset.submitAsSpamPath,
- issuableEmailAddress: el.dataset.issuableEmailAddress,
- fullPath: el.dataset.projectPath,
},
render: (createElement) => createElement(HeaderActions),
});
diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue b/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue
index 96a935643f6..b89e311ff1d 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue
+++ b/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue
@@ -1,4 +1,6 @@
<script>
+import { GlAlert } from '@gitlab/ui';
+import { __ } from '~/locale';
import { queryToObject } from '~/lib/utils/url_utility';
import { validateQueryString } from '~/jobs/components/filtered_search/utils';
import JobsTable from '~/jobs/components/table/jobs_table.vue';
@@ -8,8 +10,12 @@ import { DEFAULT_FIELDS_ADMIN } from '../constants';
import GetAllJobs from './graphql/queries/get_all_jobs.query.graphql';
export default {
+ i18n: {
+ jobsFetchErrorMsg: __('There was an error fetching the jobs.'),
+ },
components: {
JobsTableEmptyState,
+ GlAlert,
JobsTable,
JobsTableTabs,
},
@@ -39,7 +45,7 @@ export default {
};
},
error() {
- this.hasError = true;
+ this.error = this.$options.i18n.jobsFetchErrorMsg;
},
},
},
@@ -48,7 +54,7 @@ export default {
jobs: {
list: [],
},
- hasError: false,
+ error: '',
count: 0,
scope: null,
infiniteScrollingTriggered: false,
@@ -94,6 +100,10 @@ export default {
<template>
<div>
+ <gl-alert v-if="error" class="gl-mt-2" variant="danger" dismissible @dismiss="error = ''">
+ {{ error }}
+ </gl-alert>
+
<jobs-table-tabs :all-jobs-count="count" :loading="loading" />
<jobs-table-empty-state v-if="showEmptyState" />
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 58e4553d00d..297b8ae1fc2 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -56,10 +56,8 @@ Sidebar.prototype.addEventListeners = function () {
const layoutPage = document.querySelector('.layout-page');
const rightSidebar = document.querySelector('.js-right-sidebar');
- if (rightSidebar.classList.contains('right-sidebar-merge-requests')) {
- updateSidebarClasses(layoutPage, rightSidebar);
- window.addEventListener('resize', () => updateSidebarClasses(layoutPage, rightSidebar));
- }
+ updateSidebarClasses(layoutPage, rightSidebar);
+ window.addEventListener('resize', () => updateSidebarClasses(layoutPage, rightSidebar));
}
};
diff --git a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
index 06876546fa4..1eff4db3970 100644
--- a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
@@ -1,9 +1,8 @@
<script>
import { GlIcon, GlTooltipDirective, GlOutsideDirective as Outside } from '@gitlab/ui';
import { mapGetters, mapActions } from 'vuex';
-import { TYPE_ISSUE } from '~/issues/constants';
+import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
import { __, sprintf } from '~/locale';
-import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { createAlert } from '~/alert';
import toast from '~/vue_shared/plugins/global_toast';
@@ -46,8 +45,10 @@ export default {
},
computed: {
...mapGetters(['getNoteableData']),
- isMovedMrSidebar() {
- return this.glFeatures.movedMrSidebar;
+ isMergeRequest() {
+ return (
+ this.getNoteableData.targetType === TYPE_MERGE_REQUEST && this.glFeatures.movedMrSidebar
+ );
},
issuableDisplayName() {
const isInIssuePage = this.getNoteableData.targetType === TYPE_ISSUE;
@@ -59,6 +60,7 @@ export default {
lockStatus() {
return this.isLocked ? this.$options.locked : this.$options.unlocked;
},
+
tooltipLabel() {
return this.isLocked ? __('Locked') : __('Unlocked');
},
@@ -87,13 +89,8 @@ export default {
fullPath: this.fullPath,
})
.then(() => {
- if (this.isMovedMrSidebar) {
- toast(
- sprintf(__('%{issuableDisplayName} %{lockStatus}.'), {
- issuableDisplayName: capitalizeFirstCharacter(this.issuableDisplayName),
- lockStatus: this.isLocked ? __('locked') : __('unlocked'),
- }),
- );
+ if (this.isMergeRequest) {
+ toast(this.isLocked ? __('Merge request locked.') : __('Merge request unlocked.'));
}
})
.catch(() => {
@@ -116,14 +113,14 @@ export default {
</script>
<template>
- <li v-if="isMovedMrSidebar" class="gl-dropdown-item">
- <button type="button" class="dropdown-item" data-testid="issuable-lock" @click="toggleLocked">
+ <li v-if="isMergeRequest" class="gl-dropdown-item">
+ <button type="button" class="dropdown-item" @click="toggleLocked">
<span class="gl-dropdown-item-text-wrapper">
<template v-if="isLocked">
- {{ sprintf(__('Unlock %{issuableType}'), { issuableType: issuableDisplayName }) }}
+ {{ __('Unlock merge request') }}
</template>
<template v-else>
- {{ sprintf(__('Lock %{issuableType}'), { issuableType: issuableDisplayName }) }}
+ {{ __('Lock merge request') }}
</template>
</span>
</button>
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
index f2b960ed02c..344fa880131 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
@@ -1,7 +1,12 @@
<script>
import { GlDropdownForm, GlIcon, GlLoadingIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui';
import { createAlert } from '~/alert';
-import { TYPE_EPIC, WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
+import {
+ TYPE_EPIC,
+ TYPE_MERGE_REQUEST,
+ WORKSPACE_GROUP,
+ WORKSPACE_PROJECT,
+} from '~/issues/constants';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __, sprintf } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -86,8 +91,8 @@ export default {
},
},
computed: {
- isMovedMrSidebar() {
- return this.glFeatures.movedMrSidebar;
+ isMergeRequest() {
+ return this.issuableType === TYPE_MERGE_REQUEST && this.glFeatures.movedMrSidebar;
},
isLoading() {
return this.$apollo.queries?.subscribed?.loading || this.loading;
@@ -143,7 +148,7 @@ export default {
});
}
- if (this.isMovedMrSidebar) {
+ if (this.isMergeRequest) {
toast(subscribed ? __('Notifications turned on.') : __('Notifications turned off.'));
}
},
@@ -182,7 +187,7 @@ export default {
</script>
<template>
- <gl-dropdown-form v-if="isMovedMrSidebar" class="gl-dropdown-item">
+ <gl-dropdown-form v-if="isMergeRequest" class="gl-dropdown-item">
<div class="gl-px-5 gl-pb-2 gl-pt-1">
<gl-toggle
:value="subscribed"
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 0bf4105fdd6..2828b9fbf1a 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -17,7 +17,6 @@ import { __ } from '~/locale';
import { apolloProvider } from '~/graphql_shared/issuable_client';
import Translate from '~/vue_shared/translate';
import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
-import NewHeaderActionsPopover from '~/issues/show/components/new_header_actions_popover.vue';
import CollapsedAssigneeList from './components/assignees/collapsed_assignee_list.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import SidebarAssigneesWidget from './components/assignees/sidebar_assignees_widget.vue';
@@ -788,21 +787,6 @@ export function mountAssigneesDropdown() {
});
}
-function mountNewIssuePopover() {
- const el = document.querySelector('.js-sidebar-header-popover');
-
- if (!el) {
- return null;
- }
-
- return new Vue({
- el,
- name: 'NewHeaderActionsPopover',
- render: (createElement) =>
- createElement(NewHeaderActionsPopover, { props: { issueType: TYPE_MERGE_REQUEST } }),
- });
-}
-
const isAssigneesWidgetShown =
(isInIssuePage() || isInDesignPage() || isInMRPage()) && gon.features.issueAssigneesWidget;
@@ -830,7 +814,6 @@ export function mountSidebar(mediator, store) {
mountSidebarSeverityWidget();
mountSidebarEscalationStatus();
mountMoveIssueButton();
- mountNewIssuePopover();
}
export { getSidebarOptions };
diff --git a/app/assets/stylesheets/components/detail_page.scss b/app/assets/stylesheets/components/detail_page.scss
index 74f61faa9ae..de8142924f9 100644
--- a/app/assets/stylesheets/components/detail_page.scss
+++ b/app/assets/stylesheets/components/detail_page.scss
@@ -74,7 +74,3 @@
color: $gl-text-color;
}
}
-
-.new-header-popover {
- z-index: 999;
-}
diff --git a/app/assets/stylesheets/page_bundles/issuable.scss b/app/assets/stylesheets/page_bundles/issuable.scss
index 1b98fd4df07..e0fb95a1359 100644
--- a/app/assets/stylesheets/page_bundles/issuable.scss
+++ b/app/assets/stylesheets/page_bundles/issuable.scss
@@ -165,13 +165,3 @@
border: 0;
}
}
-
-.merge-request-notification-toggle {
- .gl-toggle {
- @include gl-ml-auto;
- }
-
- .gl-toggle-label {
- @include gl-font-weight-normal;
- }
-}
diff --git a/app/controllers/projects/incidents_controller.rb b/app/controllers/projects/incidents_controller.rb
index 7121096bd77..3842a88d15b 100644
--- a/app/controllers/projects/incidents_controller.rb
+++ b/app/controllers/projects/incidents_controller.rb
@@ -10,7 +10,6 @@ class Projects::IncidentsController < Projects::ApplicationController
push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_mvc, @project&.work_items_mvc_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_mvc_2, @project&.work_items_mvc_2_feature_flag_enabled?)
- push_frontend_feature_flag(:moved_mr_sidebar, project)
end
feature_category :incident_management
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index d50f681beec..efe88d17cab 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -67,7 +67,6 @@ class Projects::IssuesController < Projects::ApplicationController
push_force_frontend_feature_flag(:work_items_mvc, project&.work_items_mvc_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_mvc_2, project&.work_items_mvc_2_feature_flag_enabled?)
push_frontend_feature_flag(:epic_widget_edit_confirmation, project)
- push_frontend_feature_flag(:moved_mr_sidebar, project)
end
around_action :allow_gitaly_ref_name_caching, only: [:discussions]
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index fc2c927a2b1..179ce01ae44 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -12,8 +12,8 @@ module IssuablesHelper
end
end
- def sidebar_gutter_collapsed_class(is_merge_request_with_flag)
- return "right-sidebar-expanded" if is_merge_request_with_flag
+ def sidebar_gutter_collapsed_class
+ return "right-sidebar-expanded" if moved_mr_sidebar_enabled?
"right-sidebar-#{sidebar_gutter_collapsed? ? 'collapsed' : 'expanded'}"
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 98c378db7d3..e82f09a0a97 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -153,7 +153,7 @@ module IssuesHelper
issue.moved_from.project.service_desk_enabled? && !issue.project.service_desk_enabled?
end
- def issue_header_actions_data(project, issuable, current_user, issuable_sidebar)
+ def issue_header_actions_data(project, issuable, current_user)
new_issuable_params = { issue: {}, add_related_issue: issuable.iid }
if issuable.incident?
new_issuable_params[:issuable_template] = 'incident'
@@ -176,8 +176,7 @@ module IssuesHelper
report_abuse_path: add_category_abuse_reports_path,
reported_user_id: issuable.author.id,
reported_from_url: issue_url(issuable),
- submit_as_spam_path: mark_as_spam_project_issue_path(project, issuable),
- issuable_email_address: issuable_sidebar.nil? ? '' : issuable_sidebar[:create_note_email]
+ submit_as_spam_path: mark_as_spam_project_issue_path(project, issuable)
}
end
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 8c069bc828b..c4967a42a45 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -80,27 +80,20 @@ module LabelsHelper
def suggested_colors
{
+ '#cc338b' => s_('SuggestedColors|Magenta-pink'),
+ '#dc143c' => s_('SuggestedColors|Crimson'),
+ '#c21e56' => s_('SuggestedColors|Rose red'),
+ '#cd5b45' => s_('SuggestedColors|Dark coral'),
+ '#ed9121' => s_('SuggestedColors|Carrot orange'),
+ '#eee600' => s_('SuggestedColors|Titanium yellow'),
'#009966' => s_('SuggestedColors|Green-cyan'),
'#8fbc8f' => s_('SuggestedColors|Dark sea green'),
- '#3cb371' => s_('SuggestedColors|Medium sea green'),
- '#00b140' => s_('SuggestedColors|Green screen'),
- '#013220' => s_('SuggestedColors|Dark green'),
'#6699cc' => s_('SuggestedColors|Blue-gray'),
- '#0000ff' => s_('SuggestedColors|Blue'),
'#e6e6fa' => s_('SuggestedColors|Lavender'),
'#9400d3' => s_('SuggestedColors|Dark violet'),
'#330066' => s_('SuggestedColors|Deep violet'),
- '#808080' => s_('SuggestedColors|Gray'),
'#36454f' => s_('SuggestedColors|Charcoal grey'),
- '#f7e7ce' => s_('SuggestedColors|Champagne'),
- '#c21e56' => s_('SuggestedColors|Rose red'),
- '#cc338b' => s_('SuggestedColors|Magenta-pink'),
- '#dc143c' => s_('SuggestedColors|Crimson'),
- '#ff0000' => s_('SuggestedColors|Red'),
- '#cd5b45' => s_('SuggestedColors|Dark coral'),
- '#eee600' => s_('SuggestedColors|Titanium yellow'),
- '#ed9121' => s_('SuggestedColors|Carrot orange'),
- '#c39953' => s_('SuggestedColors|Aztec Gold')
+ '#808080' => s_('SuggestedColors|Gray')
}
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index a897484853d..833f2874a90 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -179,10 +179,6 @@ module MergeRequestsHelper
end
end
- def moved_mr_sidebar_enabled?
- Feature.enabled?(:moved_mr_sidebar, @project)
- end
-
def diffs_tab_pane_data(project, merge_request, params)
{
"is-locked": merge_request.discussion_locked?,
@@ -190,7 +186,7 @@ module MergeRequestsHelper
endpoint_metadata: @endpoint_metadata_url,
endpoint_batch: diffs_batch_project_json_merge_request_path(project, merge_request, 'json', params),
endpoint_coverage: @coverage_path,
- endpoint_diff_for_path: diff_for_path_namespace_project_merge_request_path(format: 'json', id: merge_request.iid, namespace_id: project.namespace.path, project_id: project.path),
+ endpoint_diff_for_path: diff_for_path_namespace_project_merge_request_path(format: 'json', id: merge_request.iid, namespace_id: project.namespace.to_param, project_id: project.path),
help_page_path: help_page_path('user/project/merge_requests/reviews/suggestions.md'),
current_user_data: @current_user_data,
update_current_user_path: @update_current_user_path,
@@ -276,6 +272,10 @@ module MergeRequestsHelper
_('%{author} requested to merge %{source_branch} %{copy_button} into %{target_branch} %{created_at}').html_safe % { author: link_to_author.html_safe, source_branch: merge_request_source_branch(merge_request).html_safe, copy_button: copy_button.html_safe, target_branch: target_branch.html_safe, created_at: time_ago_with_tooltip(merge_request.created_at, html_class: 'gl-display-inline-block').html_safe }
end
+ def moved_mr_sidebar_enabled?
+ Feature.enabled?(:moved_mr_sidebar, @project) && defined?(@merge_request)
+ end
+
def single_file_file_by_file?
Feature.enabled?(:single_file_file_by_file, @project)
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 1da84bf2660..b101f184ca6 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -38,7 +38,7 @@ module NavHelper
end
def page_gutter_class
- moved_sidebar_enabled = @is_merge_request_with_flag
+ moved_sidebar_enabled = current_controller?('merge_requests') && moved_mr_sidebar_enabled?
if (page_has_markdown? || current_path?('projects/merge_requests#diffs')) && !current_controller?('conflicts')
if cookies[:collapsed_gutter] == 'true'
diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb
index facf0808e7a..6ed2cfb6f78 100644
--- a/app/models/concerns/protected_ref_access.rb
+++ b/app/models/concerns/protected_ref_access.rb
@@ -27,13 +27,11 @@ module ProtectedRefAccess
scope :for_user, -> { where.not(user_id: nil) }
scope :for_group, -> { where.not(group_id: nil) }
- validates :access_level, presence: true, if: :role?, inclusion: {
- in: self.allowed_access_levels
- }
+ validates :access_level, presence: true, if: :role?, inclusion: { in: allowed_access_levels }
end
def humanize
- HUMAN_ACCESS_LEVELS[self.access_level]
+ HUMAN_ACCESS_LEVELS[access_level]
end
def type
diff --git a/app/models/packages/npm/metadatum.rb b/app/models/packages/npm/metadatum.rb
index 7388c4bdbd2..a856cd7225f 100644
--- a/app/models/packages/npm/metadatum.rb
+++ b/app/models/packages/npm/metadatum.rb
@@ -9,6 +9,8 @@ class Packages::Npm::Metadatum < ApplicationRecord
validate :ensure_npm_package_type
validate :ensure_package_json_size
+ scope :package_id_in, ->(package_ids) { where(package_id: package_ids) }
+
private
def ensure_npm_package_type
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index 1a0a65df6a3..8a3449e8f7c 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -16,8 +16,6 @@ class SentNotification < ApplicationRecord
validates :in_reply_to_discussion_id, format: { with: /\A\h{40}\z/, allow_nil: true }
validate :note_valid
- ignore_column :id_convert_to_bigint, remove_with: '16.0', remove_after: '2023-05-22'
-
after_save :keep_around_commit, if: :for_commit?
class << self
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 4dd4e2978c0..2cf3f36eef1 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -183,7 +183,7 @@ module Issues
end
def do_handle_issue_type_change(issue)
- SystemNoteService.change_issue_type(issue, current_user)
+ SystemNoteService.change_issue_type(issue, current_user, issue.issue_type_before_last_save)
::IncidentManagement::IssuableEscalationStatuses::CreateService.new(issue).execute if issue.supports_escalation?
end
diff --git a/app/services/packages/npm/deprecate_package_service.rb b/app/services/packages/npm/deprecate_package_service.rb
new file mode 100644
index 00000000000..2633e9f877c
--- /dev/null
+++ b/app/services/packages/npm/deprecate_package_service.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+module Packages
+ module Npm
+ class DeprecatePackageService < BaseService
+ Deprecated = Struct.new(:package_id, :message)
+ BATCH_SIZE = 50
+
+ def initialize(project, params)
+ super(project, nil, params)
+ end
+
+ def execute(async: false)
+ return ::Packages::Npm::DeprecatePackageWorker.perform_async(project.id, filtered_params) if async
+
+ packages.select(:id, :version).each_batch(of: BATCH_SIZE) do |relation|
+ deprecated_metadatum = handle_batch(relation)
+ update_metadatum(deprecated_metadatum)
+ end
+ end
+
+ private
+
+ # To avoid passing the whole metadata to the worker
+ def filtered_params
+ {
+ package_name: params[:package_name],
+ versions: params[:versions].transform_values { |version| version.slice(:deprecated) }
+ }
+ end
+
+ def packages
+ ::Packages::Npm::PackageFinder
+ .new(params['package_name'], project: project, last_of_each_version: false)
+ .execute
+ end
+
+ def handle_batch(relation)
+ relation
+ .preload_npm_metadatum
+ .filter_map { |package| deprecate(package) }
+ end
+
+ def deprecate(package)
+ deprecation_message = params.dig('versions', package.version, 'deprecated')
+ return if deprecation_message.nil?
+
+ npm_metadatum = package.npm_metadatum
+ return if identical?(npm_metadatum.package_json['deprecated'], deprecation_message)
+
+ Deprecated.new(npm_metadatum.package_id, deprecation_message)
+ end
+
+ def identical?(package_json_deprecated, deprecation_message)
+ package_json_deprecated == deprecation_message ||
+ (package_json_deprecated.nil? && deprecation_message.empty?)
+ end
+
+ def update_metadatum(deprecated_metadatum)
+ return if deprecated_metadatum.empty?
+
+ deprecation_message = deprecated_metadatum.first.message
+
+ ::Packages::Npm::Metadatum
+ .package_id_in(deprecated_metadatum.map(&:package_id))
+ .update_all(update_clause(deprecation_message))
+ end
+
+ def update_clause(deprecation_message)
+ if deprecation_message.empty?
+ "package_json = package_json - 'deprecated'"
+ else
+ ["package_json = jsonb_set(package_json, '{deprecated}', ?)", deprecation_message.to_json]
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 9de73a00eac..5f71b7ac9e9 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -388,8 +388,8 @@ module SystemNoteService
::SystemNotes::AlertManagementService.new(noteable: alert, project: alert.project).log_resolving_alert(monitoring_tool)
end
- def change_issue_type(issue, author)
- ::SystemNotes::IssuablesService.new(noteable: issue, project: issue.project, author: author).change_issue_type
+ def change_issue_type(issue, author, previous_type)
+ ::SystemNotes::IssuablesService.new(noteable: issue, project: issue.project, author: author).change_issue_type(previous_type)
end
def add_timeline_event(timeline_event)
diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb
index ad9f0dd0368..61a4316e8ae 100644
--- a/app/services/system_notes/issuables_service.rb
+++ b/app/services/system_notes/issuables_service.rb
@@ -456,8 +456,10 @@ module SystemNotes
create_resource_state_event(status: 'closed', close_auto_resolve_prometheus_alert: true)
end
- def change_issue_type
- body = "changed issue type to #{noteable.issue_type.humanize(capitalize: false)}"
+ def change_issue_type(previous_type)
+ previous = previous_type.humanize(capitalize: false)
+ new = noteable.issue_type.humanize(capitalize: false)
+ body = "changed type from #{previous} to #{new}"
create_note(NoteSummary.new(noteable, project, author, body, action: 'issue_type'))
end
diff --git a/app/views/admin/labels/new.html.haml b/app/views/admin/labels/new.html.haml
index 76f9eee717e..c8c3fe7b9af 100644
--- a/app/views/admin/labels/new.html.haml
+++ b/app/views/admin/labels/new.html.haml
@@ -1,5 +1,4 @@
- page_title _("New Label")
%h1.page-title.gl-font-size-h-display
= _('New Label')
-%hr
= render 'shared/labels/form', url: admin_labels_path, back_path: admin_labels_path
diff --git a/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml b/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
index 9bfa0e7a309..b8ee62055f0 100644
--- a/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
+++ b/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
@@ -1,8 +1,7 @@
- display_issuable_type = issuable_display_type(@merge_request)
.btn-group.gl-md-ml-3.gl-display-flex.dropdown.gl-dropdown.gl-md-w-auto.gl-w-full
- %span.js-sidebar-header-popover
- = button_tag type: 'button', id: "new-actions-header-dropdown", class: "btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret gl-display-none! gl-md-display-inline-flex!", title: _('Merge request actions'), 'aria-label': _('Merge request actions'), data: { toggle: 'dropdown', testid: 'merge-request-actions' } do
+ = button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret gl-display-none! gl-md-display-inline-flex!", title: _('Merge request actions'), 'aria-label': _('Merge request actions'), data: { toggle: 'dropdown', testid: 'merge-request-actions' } do
= sprite_icon "ellipsis_v", size: 16, css_class: "dropdown-icon gl-icon"
= button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md btn-block gl-button gl-dropdown-toggle gl-md-display-none!", data: { 'toggle' => 'dropdown' } do
%span.gl-dropdown-button-text= _('Merge request actions')
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 82e95a6a8e8..f54354674e2 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -9,15 +9,14 @@
- reviewers = local_assigns.fetch(:reviewers, nil)
- in_group_context_with_iterations = @project.group.present? && issuable_sidebar[:supports_iterations]
- is_merge_request = issuable_type === 'merge_request'
-- moved_sidebar_enabled = moved_mr_sidebar_enabled?
-- is_merge_request_with_flag = is_merge_request && moved_sidebar_enabled
+- moved_sidebar_enabled = moved_mr_sidebar_enabled? && is_merge_request
-%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in }, issuable_type: issuable_type }, class: "#{sidebar_gutter_collapsed_class(is_merge_request_with_flag)} #{'right-sidebar-merge-requests' if is_merge_request_with_flag}", 'aria-live' => 'polite', 'aria-label': issuable_type }
- .issuable-sidebar{ class: "#{'is-merge-request' if is_merge_request_with_flag}" }
- .issuable-sidebar-header{ class: "#{'gl-pb-2! gl-md-display-flex gl-justify-content-end gl-lg-display-none!' if is_merge_request_with_flag}" }
+%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in }, issuable_type: issuable_type }, class: "#{sidebar_gutter_collapsed_class} #{'right-sidebar-merge-requests' if moved_sidebar_enabled}", 'aria-live' => 'polite', 'aria-label': issuable_type }
+ .issuable-sidebar{ class: "#{'is-merge-request' if moved_sidebar_enabled}" }
+ .issuable-sidebar-header{ class: "#{'gl-pb-2! gl-md-display-flex gl-justify-content-end gl-lg-display-none!' if moved_sidebar_enabled}" }
%button.btn.gl-button.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ type: "reset", class: "gl-shadow-none! #{'gl-display-block' if moved_sidebar_enabled}", "aria-label" => _('Toggle sidebar'), title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
= sidebar_gutter_toggle_icon
- - if signed_in && !is_merge_request_with_flag
+ - if signed_in && !moved_sidebar_enabled
.js-sidebar-todo-widget-root{ data: { project_path: issuable_sidebar[:project_full_path], iid: issuable_sidebar[:iid], id: issuable_sidebar[:id] } }
= form_for issuable_type, url: issuable_sidebar[:issuable_json_path], remote: true, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f|
@@ -82,17 +81,17 @@
.js-sidebar-participants-widget-root
- - if !moved_sidebar_enabled
- .block.with-sub-blocks
+ .block.with-sub-blocks
+ - if !moved_sidebar_enabled
.js-sidebar-reference-widget-root
- - if is_merge_request && !moved_sidebar_enabled
- .sub-block.js-sidebar-source-branch
- .sidebar-collapsed-icon.js-dont-change-state
- = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport', class: 'btn-clipboard gl-button btn-default-tertiary btn-icon btn-sm js-source-branch-copy')
- .gl-display-flex.gl-align-items-center.gl-justify-content-space-between.gl-mb-2.hide-collapsed
- %span.gl-overflow-hidden.gl-text-overflow-ellipsis.gl-white-space-nowrap
- = _('Source branch: %{source_branch_open}%{source_branch}%{source_branch_close}').html_safe % { source_branch_open: "<span class='gl-font-monospace' data-testid='ref-name' title='#{html_escape(source_branch)}'>".html_safe, source_branch_close: "</span>".html_safe, source_branch: html_escape(source_branch) }
- = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport', class: 'btn-clipboard gl-button btn-default-tertiary btn-icon btn-sm js-source-branch-copy')
+ - if issuable_type == 'merge_request' && !moved_sidebar_enabled
+ .sub-block.js-sidebar-source-branch
+ .sidebar-collapsed-icon.js-dont-change-state
+ = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport', class: 'btn-clipboard gl-button btn-default-tertiary btn-icon btn-sm js-source-branch-copy')
+ .gl-display-flex.gl-align-items-center.gl-justify-content-space-between.gl-mb-2.hide-collapsed
+ %span.gl-overflow-hidden.gl-text-overflow-ellipsis.gl-white-space-nowrap
+ = _('Source branch: %{source_branch_open}%{source_branch}%{source_branch_close}').html_safe % { source_branch_open: "<span class='gl-font-monospace' data-testid='ref-name' title='#{html_escape(source_branch)}'>".html_safe, source_branch_close: "</span>".html_safe, source_branch: html_escape(source_branch) }
+ = clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport', class: 'btn-clipboard gl-button btn-default-tertiary btn-icon btn-sm js-source-branch-copy')
- if show_forwarding_email
.block
diff --git a/app/views/shared/issue_type/_details_header.html.haml b/app/views/shared/issue_type/_details_header.html.haml
index b6c0b73a83d..9f7ed6b17c3 100644
--- a/app/views/shared/issue_type/_details_header.html.haml
+++ b/app/views/shared/issue_type/_details_header.html.haml
@@ -19,4 +19,4 @@
%a.btn.gl-button.btn-default.btn-icon.float-right.gl-display-block.d-sm-none.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
= sprite_icon('chevron-double-lg-left')
- .js-issue-header-actions{ data: issue_header_actions_data(@project, issuable, current_user, @issuable_sidebar) }
+ .js-issue-header-actions{ data: issue_header_actions_data(@project, issuable, current_user) }
diff --git a/app/views/shared/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml
index 5d749b16eee..9148cb615d4 100644
--- a/app/views/shared/labels/_form.html.haml
+++ b/app/views/shared/labels/_form.html.haml
@@ -19,9 +19,7 @@
%input.label-color-preview.gl-w-7.gl-h-full.gl-border-1.gl-border-solid.gl-border-gray-500.gl-border-r-0.gl-rounded-top-right-none.gl-rounded-bottom-right-none{ type: "color", placeholder: _('Select color') }
= f.text_field :color, class: "gl-form-input form-control", data: { qa_selector: 'label_color_field' }
.form-text.text-muted
- = _('Choose any color.')
- %br
- = _("Or you can choose one of the suggested colors below")
+ = _('Select a color from the color picker or from the presets below.')
= render_suggested_colors
.gl-display-flex.gl-justify-content-space-between
%div
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index 5477b9395ea..cc1965945ac 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -1,7 +1,7 @@
- affix_offset = local_assigns.fetch(:affix_offset, "50")
- project = local_assigns[:project]
-%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class(false), 'aria-live' => 'polite', 'aria-label': _('Milestone') }
+%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite', 'aria-label': _('Milestone') }
.issuable-sidebar.milestone-sidebar
.block.milestone-progress.issuable-sidebar-header
%a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", href: "#", "aria-label" => s_('MilestoneSidebar|Toggle sidebar'), title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 0418bef3c1c..f47d5da95f0 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -1758,6 +1758,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: package_repositories:packages_npm_deprecate_package
+ :worker_name: Packages::Npm::DeprecatePackageWorker
+ :feature_category: :package_registry
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: package_repositories:packages_nuget_extraction
:worker_name: Packages::Nuget::ExtractionWorker
:feature_category: :package_registry
diff --git a/app/workers/packages/npm/deprecate_package_worker.rb b/app/workers/packages/npm/deprecate_package_worker.rb
new file mode 100644
index 00000000000..1fd324b89c3
--- /dev/null
+++ b/app/workers/packages/npm/deprecate_package_worker.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Packages
+ module Npm
+ class DeprecatePackageWorker
+ include ApplicationWorker
+
+ data_consistency :sticky
+ queue_namespace :package_repositories
+ feature_category :package_registry
+ deduplicate :until_executed
+ urgency :low
+ idempotent!
+
+ def perform(project_id, params)
+ project = Project.find(project_id)
+
+ ::Packages::Npm::DeprecatePackageService.new(project, params).execute
+ end
+ end
+ end
+end
diff --git a/config/initializers/doorkeeper_openid_connect_patch.rb b/config/initializers/doorkeeper_openid_connect_patch.rb
new file mode 100644
index 00000000000..d61b70eaa31
--- /dev/null
+++ b/config/initializers/doorkeeper_openid_connect_patch.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+# This pulls in
+# https://github.com/doorkeeper-gem/doorkeeper-openid_connect/pull/194
+# to ensure generated `kid` values are RFC 7638-compliant.
+require 'doorkeeper/openid_connect'
+
+raise 'This patch is only needed for doorkeeper_openid_connect v1.8.5' if Doorkeeper::OpenidConnect::VERSION != '1.8.5'
+
+module Doorkeeper
+ module OpenidConnect
+ def self.signing_key
+ key =
+ if %i[HS256 HS384 HS512].include?(signing_algorithm)
+ configuration.signing_key
+ else
+ OpenSSL::PKey.read(configuration.signing_key)
+ end
+
+ ::JWT::JWK.new(key, { kid_generator: JWT::JWK::Thumbprint })
+ end
+ end
+end
diff --git a/danger/specs/Dangerfile b/danger/specs/Dangerfile
index d17c17bc545..ff766cf688e 100644
--- a/danger/specs/Dangerfile
+++ b/danger/specs/Dangerfile
@@ -50,12 +50,8 @@ if has_ee_app_changes && has_spec_changes && !(has_app_changes || has_ee_spec_ch
end
# Forbidding a new file addition under `/spec/controllers` or `/ee/spec/controllers`
-if helper.changes.added.files.grep(%r{^(ee/)?spec/controllers/}).any?
- warn CONTROLLER_SPEC_DEPRECATION_MESSAGE
-end
+warn CONTROLLER_SPEC_DEPRECATION_MESSAGE if helper.changes.added.files.grep(%r{^(ee/)?spec/controllers/}).any?
specs.changed_specs_files.each do |filename|
- specs.add_suggestions_for_match_with_array(filename)
- specs.add_suggestions_for_project_factory_usage(filename)
- specs.add_suggestions_for_feature_category(filename)
+ specs.add_suggestions_for(filename)
end
diff --git a/db/post_migrate/20230419164438_change_code_suggestions_default_false_in_namespace_settings.rb b/db/post_migrate/20230419164438_change_code_suggestions_default_false_in_namespace_settings.rb
new file mode 100644
index 00000000000..51042995af0
--- /dev/null
+++ b/db/post_migrate/20230419164438_change_code_suggestions_default_false_in_namespace_settings.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class ChangeCodeSuggestionsDefaultFalseInNamespaceSettings < Gitlab::Database::Migration[2.1]
+ def change
+ change_column_default :namespace_settings, :code_suggestions, from: true, to: false
+ end
+end
diff --git a/db/schema_migrations/20230419164438 b/db/schema_migrations/20230419164438
new file mode 100644
index 00000000000..56881cd904a
--- /dev/null
+++ b/db/schema_migrations/20230419164438
@@ -0,0 +1 @@
+859bc13b517efd3020d6192486e94fd9430387872fb01df77e43551c2a691fe6 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 2803d03b01a..1a7b84d8e0f 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -18763,7 +18763,7 @@ CREATE TABLE namespace_settings (
allow_runner_registration_token boolean DEFAULT true NOT NULL,
unique_project_download_limit_alertlist integer[] DEFAULT '{}'::integer[] NOT NULL,
emails_enabled boolean DEFAULT true NOT NULL,
- code_suggestions boolean DEFAULT true NOT NULL,
+ code_suggestions boolean DEFAULT false NOT NULL,
CONSTRAINT check_0ba93c78c7 CHECK ((char_length(default_branch_name) <= 255)),
CONSTRAINT namespace_settings_unique_project_download_limit_alertlist_size CHECK ((cardinality(unique_project_download_limit_alertlist) <= 100)),
CONSTRAINT namespace_settings_unique_project_download_limit_allowlist_size CHECK ((cardinality(unique_project_download_limit_allowlist) <= 100))
diff --git a/doc/development/database/adding_database_indexes.md b/doc/development/database/adding_database_indexes.md
index 0bb175598a6..7b29b1b14de 100644
--- a/doc/development/database/adding_database_indexes.md
+++ b/doc/development/database/adding_database_indexes.md
@@ -38,6 +38,15 @@ when adding a new index:
1. Is the overhead of maintaining the index worth the reduction in query
timings?
+In some situations, an index might not be required:
+
+- The table is small (less than `1,000` records) and it's not expected to exponentially grow in size.
+- Any existing indexes filter out enough rows.
+- The reduction in query timings after the index is added is not significant.
+
+Additionally, wide indexes are not required to match all filter criteria of queries. We just need
+to cover enough columns so that the index lookup has a small enough selectivity.
+
## Re-using Queries
The first step is to make sure your query re-uses as many existing indexes as
@@ -183,6 +192,29 @@ for `index_exists?`, causing a required index to not be created
properly. By always requiring a name for certain types of indexes, the
chance of error is greatly reduced.
+## Testing for existence of indexes
+
+The easiest way to test for existence of an index by name is to use the `index_name_exists?` method, but the `index_exists?` method can also be used with a name option. For example:
+
+```ruby
+class MyMigration < Gitlab::Database::Migration[2.1]
+ INDEX_NAME = 'index_name'
+
+ def up
+ # an index must be conditionally created due to schema inconsistency
+ unless index_exists?(:table_name, :column_name, name: INDEX_NAME)
+ add_index :table_name, :column_name, name: INDEX_NAME
+ end
+ end
+
+ def down
+ # no op
+ end
+end
+```
+
+Keep in mind that concurrent index helpers like `add_concurrent_index`, `remove_concurrent_index`, and `remove_concurrent_index_by_name` already perform existence checks internally.
+
## Temporary indexes
There may be times when an index is only needed temporarily.
diff --git a/doc/development/database_review.md b/doc/development/database_review.md
index 1f653f8af94..feac0b3098d 100644
--- a/doc/development/database_review.md
+++ b/doc/development/database_review.md
@@ -125,7 +125,7 @@ the following preparations into account.
test its execution using `CREATE INDEX CONCURRENTLY` in [Database Lab](database/database_lab.md) and add the execution time to the MR description:
- Execution time largely varies between Database Lab and GitLab.com, but an elevated execution time from Database Lab
can give a hint that the execution on GitLab.com is also considerably high.
- - If the execution from Database Lab is longer than `1h`, the index should be moved to a [post-migration](database/post_deployment_migrations.md).
+ - If the execution from Database Lab is longer than `10 minutes`, the [index](database/adding_database_indexes.md) should be moved to a [post-migration](database/post_deployment_migrations.md).
Keep in mind that in this case you may need to split the migration and the application changes in separate releases to ensure the index
is in place when the code that needs it is deployed.
- Manually trigger the [database testing](database/database_migration_pipeline.md) job (`db:gitlabcom-database-testing`) in the `test` stage.
diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md
index e49df4b8ec6..2925a0f0371 100644
--- a/doc/development/documentation/styleguide/index.md
+++ b/doc/development/documentation/styleguide/index.md
@@ -1555,15 +1555,6 @@ If the document resides outside of the `doc/` directory, use the full path
instead of the relative link:
`https://docs.gitlab.com/ee/administration/restart_gitlab.html`.
-### Installation guide
-
-In [step 2 of the installation guide](../../../install/installation.md#2-ruby),
-we install Ruby from source. To update the guide for a new Ruby version:
-
-- Change the version throughout the code block.
-- Replace the sha256sum. It's available on the
- [downloads page](https://www.ruby-lang.org/en/downloads/) of the Ruby website.
-
### How to document different installation methods
GitLab supports five official installation methods. If you're referring to
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index ee5fc877d4c..47580f91af6 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -644,89 +644,14 @@ for more details.
## Adding indexes
-Before adding an index, consider if this one is necessary. There are situations in which an index
-might not be required, like:
-
-- The table is small (less than `1,000` records) and it's not expected to exponentially grow in size.
-- Any existing indexes filter out enough rows.
-- The reduction in query timings after the index is added is not significant.
-
-Additionally, wide indexes are not required to match all filter criteria of queries, we just need
-to cover enough columns so that the index lookup has a small enough selectivity. Please review our
-[Adding Database indexes](database/adding_database_indexes.md) guide for more details.
-
-When adding an index to a non-empty table make sure to use the method
-`add_concurrent_index` instead of the regular `add_index` method.
-The `add_concurrent_index` method automatically creates concurrent indexes
-when using PostgreSQL, removing the need for downtime.
-
-To use this method, you must disable single-transactions mode
-by calling the method `disable_ddl_transaction!` in the body of your migration
-class like so:
-
-```ruby
-class MyMigration < Gitlab::Database::Migration[2.1]
- disable_ddl_transaction!
-
- INDEX_NAME = 'index_name'
-
- def up
- add_concurrent_index :table, :column, name: INDEX_NAME
- end
-
- def down
- remove_concurrent_index :table, :column, name: INDEX_NAME
- end
-end
-```
-
-You must explicitly name indexes that are created with more complex
-definitions beyond table name, column names, and uniqueness constraint.
-Consult the [Adding Database Indexes](database/adding_database_indexes.md#requirements-for-naming-indexes)
-guide for more details.
-
-If you need to add a unique index, please keep in mind there is the possibility
-of existing duplicates being present in the database. This means that should
-always _first_ add a migration that removes any duplicates, before adding the
-unique index.
-
-For a small table (such as an empty one or one with less than `1,000` records),
-it is recommended to use `add_index` in a single-transaction migration, combining it with other
-operations that don't require `disable_ddl_transaction!`.
+Before adding an index, consider if one is necessary. The [Adding Database indexes](database/adding_database_indexes.md) guide contains more details to help you decide if an index is necessary and provides best practices for adding indexes.
## Testing for existence of indexes
-If a migration requires conditional logic based on the absence or
-presence of an index, you must test for existence of that index using
-its name. This helps avoids problems with how Rails compares index definitions,
-which can lead to unexpected results. For more details, review the
-[Adding Database Indexes](database/adding_database_indexes.md#why-explicit-names-are-required)
-guide.
-
-The easiest way to test for existence of an index by name is to use the
-`index_name_exists?` method, but the `index_exists?` method can also
-be used with a name option. For example:
-
-```ruby
-class MyMigration < Gitlab::Database::Migration[2.1]
- INDEX_NAME = 'index_name'
-
- def up
- # an index must be conditionally created due to schema inconsistency
- unless index_exists?(:table_name, :column_name, name: INDEX_NAME)
- add_index :table_name, :column_name, name: INDEX_NAME
- end
- end
-
- def down
- # no op
- end
-end
-```
+If a migration requires conditional logic based on the absence or presence of an index, you must test for existence of that index using its name. This helps avoids problems with how Rails compares index definitions, which can lead to unexpected results.
-Keep in mind that concurrent index helpers like `add_concurrent_index`,
-`remove_concurrent_index`, and `remove_concurrent_index_by_name` already
-perform existence checks internally.
+For more details, review the [Adding Database Indexes](database/adding_database_indexes.md#testing-for-existence-of-indexes)
+guide.
## Adding foreign-key constraints
diff --git a/doc/operations/incident_management/manage_incidents.md b/doc/operations/incident_management/manage_incidents.md
index 9d0c8075ff9..338dacda166 100644
--- a/doc/operations/incident_management/manage_incidents.md
+++ b/doc/operations/incident_management/manage_incidents.md
@@ -226,10 +226,6 @@ When you close an incident that is linked to an [alert](alerts.md),
the linked alert's status changes to **Resolved**.
You are then credited with the alert's status change.
-<!-- Delete when the `moved_mr_sidebar` feature flag is removed -->
-If you don't see this action at the top of an incident, your project or instance might have
-enabled a feature flag for [moved actions](../../user/project/merge_requests/index.md#move-sidebar-actions)
-
### Automatically close incidents via recovery alerts
> [Introduced for HTTP integrations](https://gitlab.com/gitlab-org/gitlab/-/issues/13402) in GitLab 13.4.
@@ -253,22 +249,6 @@ When GitLab receives a recovery alert, it closes the associated incident.
This action is recorded as a system note on the incident indicating that it
was closed automatically by the GitLab Alert bot.
-## Delete an incident
-
-Prerequisites:
-
-- You must have the Owner role for a project.
-
-To delete an incident:
-
-1. In an incident, select **Incident actions** (**{ellipsis_v}**).
-1. Select **Delete incident**.
-
-Alternatively:
-
-1. In an incident, select **Edit title and description** (**{pencil}**).
-1. Select **Delete incident**.
-
## Other actions
Because incidents in GitLab are built on top of [issues](../../user/project/issues/index.md),
diff --git a/doc/user/group/epics/img/button_close_epic.png b/doc/user/group/epics/img/button_close_epic.png
new file mode 100644
index 00000000000..aa1a889ea23
--- /dev/null
+++ b/doc/user/group/epics/img/button_close_epic.png
Binary files differ
diff --git a/doc/user/group/epics/manage_epics.md b/doc/user/group/epics/manage_epics.md
index 1349d28c6a1..0dc87b7e4e4 100644
--- a/doc/user/group/epics/manage_epics.md
+++ b/doc/user/group/epics/manage_epics.md
@@ -159,13 +159,14 @@ Prerequisites:
- You must have at least the Reporter role for the epic's group.
-To close an epic, at the top of an epic, select **Close epic**.
+Whenever you decide that there is no longer need for that epic,
+close the epic by:
-<!-- Delete when the `moved_mr_sidebar` feature flag is removed -->
-If you don't see this action at the top of an epic, your project or instance might have
-enabled a feature flag for [moved actions](../../project/merge_requests/index.md#move-sidebar-actions)
+- Selecting **Close epic**.
-You can also use the `/close` [quick action](../../project/quick_actions.md).
+ ![close epic - button](img/button_close_epic.png)
+
+- Using the `/close` [quick action](../../project/quick_actions.md).
## Reopen a closed epic
diff --git a/doc/user/packages/npm_registry/index.md b/doc/user/packages/npm_registry/index.md
index 02233e096a2..52fdda0d523 100644
--- a/doc/user/packages/npm_registry/index.md
+++ b/doc/user/packages/npm_registry/index.md
@@ -205,6 +205,39 @@ To install a package from the instance level, the package must have been publish
npm install @scope/my-package
```
+## Deprecate a package
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/396763) in GitLab 16.0.
+
+You can deprecate a package so that a deprecation warning displays when the package is fetched.
+
+Pre-requisites:
+
+- The same [permissions](../../permissions.md) as deleting a package.
+- [Authenticated to the package registry](#authentication-to-the-package-registry).
+
+From the command line, run:
+
+```shell
+npm deprecate @scope/package "Deprecation message"
+```
+
+The CLI also accepts version ranges for `@scope/package`. For example:
+
+```shell
+npm deprecate @scope/package "All package versions are deprecated"
+npm deprecate @scope/package@1.0.1 "Only version 1.0.1 is deprecated"
+npm deprecate @scope/package@"< 1.0.5" "All package versions less than 1.0.5 are deprecated"
+```
+
+### Remove deprecation warning
+
+To remove a package's deprecation warning, specify `""` (an empty string) for the message. For example:
+
+```shell
+npm deprecate @scope/package ""
+```
+
## Helpful hints
### Package forwarding to npmjs.com
@@ -293,6 +326,8 @@ The GitLab npm repository supports the following commands for the npm CLI (`npm`
- `npm dist-tag rm`: Delete a dist-tag.
- `npm ci`: Install npm packages directly from your `package-lock.json` file.
- `npm view`: Show package metadata.
+- `npm pack`: Create a tarball from a package.
+- `npm deprecate`: Deprecate a version of a package.
## Troubleshooting
diff --git a/doc/user/profile/notifications.md b/doc/user/profile/notifications.md
index 89366c73b16..c0c967a3f18 100644
--- a/doc/user/profile/notifications.md
+++ b/doc/user/profile/notifications.md
@@ -239,7 +239,7 @@ Turning this toggle off only unsubscribes you from updates related to this issue
Learn how to [opt out of all emails from GitLab](#opt-out-of-all-gitlab-emails).
<!-- Delete when the `moved_mr_sidebar` feature flag is removed -->
-If you don't see this action on the right sidebar, your project or instance might have
+If you don't see this action on the right sidebar, your project or instance may have
enabled a feature flag for [moved sidebar actions](../project/merge_requests/index.md#move-sidebar-actions).
### Notification events on issues, merge requests, and epics
diff --git a/doc/user/project/issues/create_issues.md b/doc/user/project/issues/create_issues.md
index 4511c89b0ff..b6931149ede 100644
--- a/doc/user/project/issues/create_issues.md
+++ b/doc/user/project/issues/create_issues.md
@@ -78,7 +78,7 @@ Prerequisites:
To create an issue from another issue:
-1. In an existing issue, select **Issue actions** (**{ellipsis_v}**).
+1. In an existing issue, select the vertical ellipsis (**{ellipsis_v}**).
1. Select **New related issue**.
1. Complete the [fields](#fields-in-the-new-issue-form).
The new issue form has a **Relate to issue #123** checkbox, where `123` is the ID of the
diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md
index b532fd0c5b8..069bc4582c6 100644
--- a/doc/user/project/issues/managing_issues.md
+++ b/doc/user/project/issues/managing_issues.md
@@ -209,10 +209,6 @@ To close an issue, you can do the following:
- At the top of the issue, select **Close issue**.
- In an [issue board](../issue_board.md), drag an issue card from its list into the **Closed** list.
-<!-- Delete when the `moved_mr_sidebar` feature flag is removed -->
-If you don't see this action at the top of an issue, your project or instance might have
-enabled a feature flag for [moved actions](../merge_requests/index.md#move-sidebar-actions).
-
### Reopen a closed issue
Prerequisites:
@@ -348,7 +344,7 @@ Prerequisites:
To delete an issue:
-1. In an issue, select **Issue actions** (**{ellipsis_v}**).
+1. In an issue, select the vertical ellipsis (**{ellipsis_v}**).
1. Select **Delete issue**.
Alternatively:
@@ -366,7 +362,7 @@ You can promote an issue to an [epic](../../group/epics/index.md) in the immedia
To promote an issue to an epic:
-1. In an issue, select **Issue actions** (**{ellipsis_v}**).
+1. In an issue, select the vertical ellipsis (**{ellipsis_v}**).
1. Select **Promote to epic**.
Alternatively, you can use the `/promote` [quick action](../quick_actions.md#issues-merge-requests-and-epics).
@@ -476,10 +472,6 @@ You can now paste the reference into another description or comment.
Read more about issue references in [GitLab-Flavored Markdown](../../markdown.md#gitlab-specific-references).
-<!-- Delete when the `moved_mr_sidebar` feature flag is removed -->
-If you don't see this action on the right sidebar, your project or instance might have
-enabled a feature flag for [moved actions](../merge_requests/index.md#move-sidebar-actions).
-
## Copy issue email address
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/18816) in GitLab 13.8.
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 6bcf3f15685..ee7f4e5dfed 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -251,8 +251,7 @@ after merging does not retarget open merge requests. This improvement is
<!-- When the `moved_mr_sidebar` feature flag is removed, delete this topic and update the steps for these actions
like in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87727/diffs?diff_id=522279685#5d9afba799c4af9920dab533571d7abb8b9e9163 -->
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85584) in GitLab 14.10 [with a flag](../../../administration/feature_flags.md) named `moved_mr_sidebar`. Disabled by default.
-> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/373757) to also move actions on issues, incidents, and epics in GitLab 15.10.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85584) in GitLab 14.10 [with a flag](../../../administration/feature_flags.md) named `moved_mr_sidebar`. Disabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available per project or for your entire instance, ask an administrator to [enable the feature flag](../../../administration/feature_flags.md) named `moved_mr_sidebar`.
@@ -267,8 +266,6 @@ When this feature flag is enabled, in the upper-right corner,
- [Lock discussion](../../discussions/index.md#prevent-comments-by-locking-the-discussion)
- Copy reference
-In GitLab 15.10 and later, similar action menus are available on issues, incidents, and epics.
-
When this feature flag is disabled, these actions are in the right sidebar.
## Merge request workflows
diff --git a/doc/user/report_abuse.md b/doc/user/report_abuse.md
index c4b9af28220..de2b82c28d3 100644
--- a/doc/user/report_abuse.md
+++ b/doc/user/report_abuse.md
@@ -50,7 +50,7 @@ A URL to the reported user's comment is pre-filled in the abuse report's
## Report abuse from an issue
-1. On the issue, in the upper-right corner, select **Issue actions** (**{ellipsis_v}**).
+1. On the issue, in the upper-right corner, select the vertical ellipsis (**{ellipsis_v}**).
1. Select **Report abuse to administrator**.
1. Select a reason for reporting the user.
1. Complete an abuse report.
@@ -58,7 +58,7 @@ A URL to the reported user's comment is pre-filled in the abuse report's
## Report abuse from a merge request
-1. On the merge request, in the upper-right corner, select **Merge request actions** (**{ellipsis_v}**).
+1. On the merge request, in the upper-right corner, select the vertical ellipsis (**{ellipsis_v}**).
1. Select **Report abuse to administrator**.
1. Select a reason for reporting this user.
1. Complete an abuse report.
diff --git a/lib/api/npm_project_packages.rb b/lib/api/npm_project_packages.rb
index 07cbb3bf582..171a061bf97 100644
--- a/lib/api/npm_project_packages.rb
+++ b/lib/api/npm_project_packages.rb
@@ -44,8 +44,8 @@ module API
present_package_file!(package_file)
end
- desc 'Create NPM package' do
- detail 'This feature was introduced in GitLab 11.8'
+ desc 'Create or deprecate NPM package' do
+ detail 'Create was introduced in GitLab 11.8 & deprecate suppport was added in 16.0'
success code: 200
failure [
{ code: 400, message: 'Bad Request' },
@@ -61,16 +61,22 @@ module API
end
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
put ':package_name', requirements: ::API::Helpers::Packages::Npm::NPM_ENDPOINT_REQUIREMENTS do
- authorize_create_package!(project)
-
- created_package = ::Packages::Npm::CreatePackageService
- .new(project, current_user, params.merge(build: current_authenticated_job)).execute
+ if headers['Npm-Command'] == 'deprecate'
+ authorize_destroy_package!(project)
- if created_package[:status] == :error
- render_api_error!(created_package[:message], created_package[:http_status])
+ ::Packages::Npm::DeprecatePackageService.new(project, declared(params)).execute(async: true)
else
- track_package_event('push_package', :npm, category: 'API::NpmPackages', project: project, namespace: project.namespace)
- created_package
+ authorize_create_package!(project)
+
+ created_package = ::Packages::Npm::CreatePackageService
+ .new(project, current_user, params.merge(build: current_authenticated_job)).execute
+
+ if created_package[:status] == :error
+ render_api_error!(created_package[:message], created_package[:http_status])
+ else
+ track_package_event('push_package', :npm, category: 'API::NpmPackages', project: project, namespace: project.namespace)
+ created_package
+ end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b67cc5655da..8ea17cbb211 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -754,9 +754,6 @@ msgstr ""
msgid "%{integrations_link_start}Integrations%{link_end} enable you to make third-party applications part of your GitLab workflow. If the available integrations don't meet your needs, consider using a %{webhooks_link_start}webhook%{link_end}."
msgstr ""
-msgid "%{issuableDisplayName} %{lockStatus}."
-msgstr ""
-
msgid "%{issuableType} will be removed! Are you sure?"
msgstr ""
@@ -9137,9 +9134,6 @@ msgstr ""
msgid "Choose a type..."
msgstr ""
-msgid "Choose any color."
-msgstr ""
-
msgid "Choose file…"
msgstr ""
@@ -11915,9 +11909,6 @@ msgstr ""
msgid "Copy %{http_label} clone URL"
msgstr ""
-msgid "Copy %{issueType} email address"
-msgstr ""
-
msgid "Copy %{name}"
msgstr ""
@@ -16030,9 +16021,6 @@ msgstr ""
msgid "Email a new %{name} to this project"
msgstr ""
-msgid "Email address copied"
-msgstr ""
-
msgid "Email address suffix"
msgstr ""
@@ -21469,12 +21457,6 @@ msgstr ""
msgid "Header message"
msgstr ""
-msgid "HeaderAction|Notifications and other %{issueType} actions have moved to this menu."
-msgstr ""
-
-msgid "HeaderAction|Okay!"
-msgstr ""
-
msgid "HeaderAction|incident"
msgstr ""
@@ -26409,9 +26391,6 @@ msgstr ""
msgid "Lock %{issuableDisplayName}"
msgstr ""
-msgid "Lock %{issuableType}"
-msgstr ""
-
msgid "Lock File?"
msgstr ""
@@ -26421,6 +26400,9 @@ msgstr ""
msgid "Lock memberships to SAML Group Links synchronization"
msgstr ""
+msgid "Lock merge request"
+msgstr ""
+
msgid "Lock not found"
msgstr ""
@@ -27403,12 +27385,18 @@ msgstr ""
msgid "Merge request events"
msgstr ""
+msgid "Merge request locked."
+msgstr ""
+
msgid "Merge request not merged"
msgstr ""
msgid "Merge request reports"
msgstr ""
+msgid "Merge request unlocked."
+msgstr ""
+
msgid "Merge request was scheduled to merge after pipeline succeeds"
msgstr ""
@@ -30932,9 +30920,6 @@ msgstr ""
msgid "Options"
msgstr ""
-msgid "Or you can choose one of the suggested colors below"
-msgstr ""
-
msgid "Ordered list"
msgstr ""
@@ -36495,9 +36480,6 @@ msgstr ""
msgid "Reference"
msgstr ""
-msgid "Reference copied"
-msgstr ""
-
msgid "References"
msgstr ""
@@ -40566,6 +40548,9 @@ msgstr ""
msgid "Select a color"
msgstr ""
+msgid "Select a color from the color picker or from the presets below."
+msgstr ""
+
msgid "Select a compliance framework to apply to this project. %{linkStart}How are these added?%{linkEnd}"
msgstr ""
@@ -43108,9 +43093,6 @@ msgstr ""
msgid "Suggested change"
msgstr ""
-msgid "SuggestedColors|Aztec Gold"
-msgstr ""
-
msgid "SuggestedColors|Blue"
msgstr ""
@@ -43120,9 +43102,6 @@ msgstr ""
msgid "SuggestedColors|Carrot orange"
msgstr ""
-msgid "SuggestedColors|Champagne"
-msgstr ""
-
msgid "SuggestedColors|Charcoal grey"
msgstr ""
@@ -43138,9 +43117,6 @@ msgstr ""
msgid "SuggestedColors|Dark coral"
msgstr ""
-msgid "SuggestedColors|Dark green"
-msgstr ""
-
msgid "SuggestedColors|Dark sea green"
msgstr ""
@@ -43162,9 +43138,6 @@ msgstr ""
msgid "SuggestedColors|Green"
msgstr ""
-msgid "SuggestedColors|Green screen"
-msgstr ""
-
msgid "SuggestedColors|Green-cyan"
msgstr ""
@@ -43174,9 +43147,6 @@ msgstr ""
msgid "SuggestedColors|Magenta-pink"
msgstr ""
-msgid "SuggestedColors|Medium sea green"
-msgstr ""
-
msgid "SuggestedColors|Orange"
msgstr ""
@@ -44975,6 +44945,9 @@ msgstr ""
msgid "There was an error fetching the jobs for your project."
msgstr ""
+msgid "There was an error fetching the jobs."
+msgstr ""
+
msgid "There was an error fetching the number of jobs for your project."
msgstr ""
@@ -47217,10 +47190,10 @@ msgstr ""
msgid "Unlock"
msgstr ""
-msgid "Unlock %{issuableType}"
+msgid "Unlock account"
msgstr ""
-msgid "Unlock account"
+msgid "Unlock merge request"
msgstr ""
msgid "Unlock more features with GitLab Ultimate"
@@ -49301,7 +49274,10 @@ msgstr ""
msgid "Vulnerability|There was an unexpected error. %{linkStart}Please try again%{linkEnd}."
msgstr ""
-msgid "Vulnerability|This is an experimental feature that uses AI to explain the vulnerability and provide recommendations. Use this feature with caution as we continue to iterate. Please provide your feedback and ideas in this %{linkStart}issue%{linkEnd}."
+msgid "Vulnerability|This is an experimental feature that uses AI to explain the vulnerability and provide recommendations. Use this feature with caution as we continue to iterate. Please provide your feedback and ideas in %{linkStart}this issue%{linkEnd}."
+msgstr ""
+
+msgid "Vulnerability|This response is generated by AI."
msgstr ""
msgid "Vulnerability|Tool"
@@ -49316,6 +49292,9 @@ msgstr ""
msgid "Vulnerability|Training not available for this vulnerability."
msgstr ""
+msgid "Vulnerability|Try it out"
+msgstr ""
+
msgid "Vulnerability|Unmodified Response"
msgstr ""
@@ -52828,9 +52807,6 @@ msgstr ""
msgid "loading"
msgstr ""
-msgid "locked"
-msgstr ""
-
msgid "locked by %{path_lock_user_name} %{created_at}"
msgstr ""
@@ -53738,9 +53714,6 @@ msgstr ""
msgid "unicode domains should use IDNA encoding"
msgstr ""
-msgid "unlocked"
-msgstr ""
-
msgid "updated"
msgstr ""
diff --git a/spec/features/ide/user_opens_merge_request_spec.rb b/spec/features/ide/user_opens_merge_request_spec.rb
index dc280133a20..0074b4b1eb0 100644
--- a/spec/features/ide/user_opens_merge_request_spec.rb
+++ b/spec/features/ide/user_opens_merge_request_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe 'IDE merge request', :js, feature_category: :web_ide do
- include CookieHelper
-
let(:merge_request) { create(:merge_request, :simple, source_project: project) }
let(:project) { create(:project, :public, :repository) }
let(:user) { project.first_owner }
@@ -14,8 +12,6 @@ RSpec.describe 'IDE merge request', :js, feature_category: :web_ide do
sign_in(user)
- set_cookie('new-actions-popover-viewed', 'true')
-
visit(merge_request_path(merge_request))
end
diff --git a/spec/features/incidents/incident_details_spec.rb b/spec/features/incidents/incident_details_spec.rb
index a166ff46177..709919d0196 100644
--- a/spec/features/incidents/incident_details_spec.rb
+++ b/spec/features/incidents/incident_details_spec.rb
@@ -94,7 +94,6 @@ RSpec.describe 'Incident details', :js, feature_category: :incident_management d
end
it 'routes the user to the incident details page when the `issue_type` is set to incident' do
- set_cookie('new-actions-popover-viewed', 'true')
visit project_issue_path(project, issue)
wait_for_requests
@@ -114,7 +113,6 @@ RSpec.describe 'Incident details', :js, feature_category: :incident_management d
end
it 'routes the user to the issue details page when the `issue_type` is set to issue' do
- set_cookie('new-actions-popover-viewed', 'true')
visit incident_project_issues_path(project, incident)
wait_for_requests
diff --git a/spec/features/issues/discussion_lock_spec.rb b/spec/features/issues/discussion_lock_spec.rb
index fb9addff1a2..47865d2b6ba 100644
--- a/spec/features/issues/discussion_lock_spec.rb
+++ b/spec/features/issues/discussion_lock_spec.rb
@@ -9,7 +9,6 @@ RSpec.describe 'Discussion Lock', :js, feature_category: :team_planning do
before do
sign_in(user)
- stub_feature_flags(moved_mr_sidebar: false)
end
context 'when a user is a team member' do
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 665c7307231..2bd5373b715 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do
- include CookieHelper
-
let_it_be(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
let_it_be(:user2) { create(:user, name: 'Marge Simpson', username: 'msimpson') }
@@ -47,7 +45,6 @@ RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do
before do
sign_in(user)
- set_cookie('new-actions-popover-viewed', 'true')
visit project_issue_path(project, issue_to_edit)
wait_for_requests
diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb
index 29a61d584ee..d5f90bb9260 100644
--- a/spec/features/issues/issue_detail_spec.rb
+++ b/spec/features/issues/issue_detail_spec.rb
@@ -98,7 +98,6 @@ RSpec.describe 'Issue Detail', :js, feature_category: :team_planning do
project.add_developer(user_to_be_deleted)
sign_in(user_to_be_deleted)
- stub_feature_flags(moved_mr_sidebar: false)
visit project_issue_path(project, issue)
wait_for_requests
@@ -130,7 +129,7 @@ RSpec.describe 'Issue Detail', :js, feature_category: :team_planning do
describe 'when an issue `issue_type` is edited' do
before do
sign_in(user)
- set_cookie('new-actions-popover-viewed', 'true')
+
visit project_issue_path(project, issue)
wait_for_requests
end
@@ -164,7 +163,7 @@ RSpec.describe 'Issue Detail', :js, feature_category: :team_planning do
describe 'when an incident `issue_type` is edited' do
before do
sign_in(user)
- set_cookie('new-actions-popover-viewed', 'true')
+
visit project_issue_path(project, incident)
wait_for_requests
end
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index ee71181fba2..2ae347d4f9e 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -5,7 +5,6 @@ require 'spec_helper'
RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
include MobileHelpers
include Features::InviteMembersModalHelpers
- include CookieHelper
let_it_be(:group) { create(:group, :nested) }
let_it_be(:project) { create(:project, :public, namespace: group) }
@@ -21,7 +20,6 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
context 'when signed in' do
before do
sign_in(user)
- set_cookie('new-actions-popover-viewed', 'true')
end
context 'when concerning the assignee', :js do
@@ -207,7 +205,6 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
context 'as an allowed user' do
before do
- stub_feature_flags(moved_mr_sidebar: false)
project.add_developer(user)
visit_issue(project, issue)
end
@@ -296,7 +293,6 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do
context 'as a guest' do
before do
- stub_feature_flags(moved_mr_sidebar: false)
project.add_guest(user)
visit_issue(project, issue)
end
diff --git a/spec/features/issues/user_edits_issue_spec.rb b/spec/features/issues/user_edits_issue_spec.rb
index 4ef58918a2b..c6cedbc83cd 100644
--- a/spec/features/issues/user_edits_issue_spec.rb
+++ b/spec/features/issues/user_edits_issue_spec.rb
@@ -3,8 +3,6 @@
require "spec_helper"
RSpec.describe "Issues > User edits issue", :js, feature_category: :team_planning do
- include CookieHelper
-
let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:project_with_milestones) { create(:project_empty_repo, :public) }
let_it_be(:user) { create(:user) }
@@ -20,7 +18,6 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin
project.add_developer(user)
project_with_milestones.add_developer(user)
sign_in(user)
- set_cookie('new-actions-popover-viewed', 'true')
end
context "from edit page" do
diff --git a/spec/features/issues/user_toggles_subscription_spec.rb b/spec/features/issues/user_toggles_subscription_spec.rb
index 00b04c10d33..904fafdf56a 100644
--- a/spec/features/issues/user_toggles_subscription_spec.rb
+++ b/spec/features/issues/user_toggles_subscription_spec.rb
@@ -10,7 +10,6 @@ RSpec.describe "User toggles subscription", :js, feature_category: :team_plannin
context 'user is not logged in' do
before do
- stub_feature_flags(moved_mr_sidebar: false)
visit(project_issue_path(project, issue))
end
@@ -21,9 +20,9 @@ RSpec.describe "User toggles subscription", :js, feature_category: :team_plannin
context 'user is logged in' do
before do
- stub_feature_flags(moved_mr_sidebar: false)
project.add_developer(user)
sign_in(user)
+
visit(project_issue_path(project, issue))
end
@@ -53,7 +52,6 @@ RSpec.describe "User toggles subscription", :js, feature_category: :team_plannin
context 'user is logged in without edit permission' do
before do
- stub_feature_flags(moved_mr_sidebar: false)
sign_in(user2)
visit(project_issue_path(project, issue))
diff --git a/spec/features/merge_request/user_manages_subscription_spec.rb b/spec/features/merge_request/user_manages_subscription_spec.rb
index 3bcc8255ab7..d4ccc4a93b5 100644
--- a/spec/features/merge_request/user_manages_subscription_spec.rb
+++ b/spec/features/merge_request/user_manages_subscription_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe 'User manages subscription', :js, feature_category: :code_review_workflow do
- include CookieHelper
-
let(:project) { create(:project, :public, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) }
@@ -12,7 +10,7 @@ RSpec.describe 'User manages subscription', :js, feature_category: :code_review_
before do
stub_feature_flags(moved_mr_sidebar: moved_mr_sidebar_enabled)
- set_cookie('new-actions-popover-viewed', 'true')
+
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb b/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb
index 601310cbacf..7cb1c95f6dc 100644
--- a/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb
+++ b/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe 'Merge request > User opens checkout branch modal', :js, feature_category: :code_review_workflow do
include ProjectForksHelper
- include CookieHelper
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
@@ -12,7 +11,6 @@ RSpec.describe 'Merge request > User opens checkout branch modal', :js, feature_
before do
project.add_maintainer(user)
sign_in(user)
- set_cookie('new-actions-popover-viewed', 'true')
end
describe 'for fork' do
diff --git a/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb b/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb
index 21c62b0d0d8..ad2ceeb23e2 100644
--- a/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb
+++ b/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe 'Merge request > User sees check out branch modal', :js, feature_category: :code_review_workflow do
- include CookieHelper
-
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
let(:merge_request) { create(:merge_request, source_project: project) }
@@ -12,7 +10,6 @@ RSpec.describe 'Merge request > User sees check out branch modal', :js, feature_
before do
sign_in(user)
- set_cookie('new-actions-popover-viewed', 'true')
visit project_merge_request_path(project, merge_request)
wait_for_requests
diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
index dae28cbb05c..0de59ea21c5 100644
--- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
+++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_category: :code_review_workflow do
include ListboxHelpers
- include CookieHelper
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
@@ -18,7 +17,6 @@ RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_
before do
project.add_maintainer(user)
sign_in(user)
- set_cookie('new-actions-popover-viewed', 'true')
end
it 'selects the source branch sha when a tag with the same name exists' do
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index 77f88994bfb..adf410ce6e8 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe 'issuable templates', :js, feature_category: :projects do
include ProjectForksHelper
- include CookieHelper
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
@@ -13,7 +12,6 @@ RSpec.describe 'issuable templates', :js, feature_category: :projects do
before do
project.add_maintainer(user)
sign_in user
- set_cookie('new-actions-popover-viewed', 'true')
end
context 'user creates an issue using templates' do
diff --git a/spec/features/reportable_note/issue_spec.rb b/spec/features/reportable_note/issue_spec.rb
index a18cdf27294..55e7f5897bc 100644
--- a/spec/features/reportable_note/issue_spec.rb
+++ b/spec/features/reportable_note/issue_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe 'Reportable note on issue', :js, feature_category: :team_planning do
- include CookieHelper
-
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project) }
@@ -13,7 +11,7 @@ RSpec.describe 'Reportable note on issue', :js, feature_category: :team_planning
before do
project.add_maintainer(user)
sign_in(user)
- set_cookie('new-actions-popover-viewed', 'true')
+
visit project_issue_path(project, issue)
end
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index a5ba512434c..db3435855f6 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -2,8 +2,6 @@ import Vue, { nextTick } from 'vue';
import { GlDropdownItem, GlLink, GlModal, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
-import VueApollo from 'vue-apollo';
-import waitForPromises from 'helpers/wait_for_promises';
import { mockTracking } from 'helpers/tracking_helper';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { STATUS_CLOSED, STATUS_OPEN, TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
@@ -16,22 +14,17 @@ import promoteToEpicMutation from '~/issues/show/queries/promote_to_epic.mutatio
import * as urlUtility from '~/lib/utils/url_utility';
import eventHub from '~/notes/event_hub';
import createStore from '~/notes/stores';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
-import updateIssueMutation from '~/issues/show/queries/update_issue.mutation.graphql';
-import toast from '~/vue_shared/plugins/global_toast';
jest.mock('~/alert');
jest.mock('~/issues/show/event_hub', () => ({ $emit: jest.fn() }));
-jest.mock('~/vue_shared/plugins/global_toast');
describe('HeaderActions component', () => {
let dispatchEventSpy;
+ let mutateMock;
let wrapper;
let visitUrlSpy;
Vue.use(Vuex);
- Vue.use(VueApollo);
const store = createStore();
@@ -52,28 +45,15 @@ describe('HeaderActions component', () => {
reportedUserId: 1,
reportedFromUrl: 'http://localhost:/gitlab-org/-/issues/32',
submitAsSpamPath: 'gitlab-org/gitlab-test/-/issues/32/submit_as_spam',
- issuableEmailAddress: null,
- fullPath: 'full-path',
};
- const updateIssueMutationResponse = {
- data: {
- updateIssue: {
- errors: [],
- issuable: {
- id: 'gid://gitlab/Issue/511',
- state: STATUS_OPEN,
- },
- },
- },
- };
+ const updateIssueMutationResponse = { data: { updateIssue: { errors: [] } } };
const promoteToEpicMutationResponse = {
data: {
promoteToEpic: {
errors: [],
epic: {
- id: 'gid://gitlab/Epic/1',
webPath: '/groups/gitlab-org/-/epics/1',
},
},
@@ -89,20 +69,6 @@ describe('HeaderActions component', () => {
},
};
- const mockIssueReferenceData = {
- data: {
- workspace: {
- id: 'gid://gitlab/Project/7',
- issuable: {
- id: 'gid://gitlab/Issue/511',
- reference: 'flightjs/Flight#33',
- __typename: 'Issue',
- },
- __typename: 'Project',
- },
- },
- };
-
const findToggleIssueStateButton = () => wrapper.find(`[data-testid="toggle-button"]`);
const findEditButton = () => wrapper.find(`[data-testid="edit-button"]`);
@@ -111,54 +77,33 @@ describe('HeaderActions component', () => {
const findDesktopDropdown = () => findDropdownBy('desktop-dropdown');
const findMobileDropdownItems = () => findMobileDropdown().findAllComponents(GlDropdownItem);
const findDesktopDropdownItems = () => findDesktopDropdown().findAllComponents(GlDropdownItem);
- const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
- const findReportAbuseSelectorItem = () => wrapper.find(`[data-testid="report-abuse-item"]`);
- const findNotificationWidget = () => wrapper.find(`[data-testid="notification-toggle"]`);
- const findLockIssueWidget = () => wrapper.find(`[data-testid="lock-issue-toggle"]`);
- const findCopyRefenceDropdownItem = () => wrapper.find(`[data-testid="copy-reference"]`);
- const findCopyEmailItem = () => wrapper.find(`[data-testid="copy-email"]`);
const findModal = () => wrapper.findComponent(GlModal);
const findModalLinkAt = (index) => findModal().findAllComponents(GlLink).at(index);
- const issueReferenceSuccessHandler = jest.fn().mockResolvedValue(mockIssueReferenceData);
- const updateIssueMutationResponseHandler = jest
- .fn()
- .mockResolvedValue(updateIssueMutationResponse);
- const promoteToEpicMutationSuccessResponseHandler = jest
- .fn()
- .mockResolvedValue(promoteToEpicMutationResponse);
- const promoteToEpicMutationErrorHandler = jest
- .fn()
- .mockResolvedValue(promoteToEpicMutationErrorResponse);
-
const mountComponent = ({
props = {},
issueState = STATUS_OPEN,
blockedByIssues = [],
- movedMrSidebarEnabled = false,
- promoteToEpicHandler = promoteToEpicMutationSuccessResponseHandler,
+ mutateResponse = {},
} = {}) => {
+ mutateMock = jest.fn().mockResolvedValue(mutateResponse);
+
store.dispatch('setNoteableData', {
blocked_by_issues: blockedByIssues,
state: issueState,
});
- const handlers = [
- [issueReferenceQuery, issueReferenceSuccessHandler],
- [updateIssueMutation, updateIssueMutationResponseHandler],
- [promoteToEpicMutation, promoteToEpicHandler],
- ];
-
return shallowMount(HeaderActions, {
- apolloProvider: createMockApollo(handlers),
store,
provide: {
...defaultProps,
...props,
- glFeatures: {
- movedMrSidebar: movedMrSidebarEnabled,
+ },
+ mocks: {
+ $apollo: {
+ mutate: mutateMock,
},
},
stubs: {
@@ -193,6 +138,7 @@ describe('HeaderActions component', () => {
wrapper = mountComponent({
props: { issueType },
issueState,
+ mutateResponse: updateIssueMutationResponse,
});
});
@@ -203,19 +149,23 @@ describe('HeaderActions component', () => {
it('calls apollo mutation', () => {
findToggleIssueStateButton().vm.$emit('click');
- expect(updateIssueMutationResponseHandler).toHaveBeenCalledWith({
- input: {
- iid: defaultProps.iid,
- projectPath: defaultProps.projectPath,
- stateEvent: newIssueState,
- },
- });
+ expect(mutateMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ variables: {
+ input: {
+ iid: defaultProps.iid,
+ projectPath: defaultProps.projectPath,
+ stateEvent: newIssueState,
+ },
+ },
+ }),
+ );
});
it('dispatches a custom event to update the issue page', async () => {
findToggleIssueStateButton().vm.$emit('click');
- await waitForPromises();
+ await nextTick();
expect(dispatchEventSpy).toHaveBeenCalledTimes(1);
});
@@ -340,25 +290,28 @@ describe('HeaderActions component', () => {
describe('when "Promote to epic" button is clicked', () => {
describe('when response is successful', () => {
- beforeEach(async () => {
+ beforeEach(() => {
visitUrlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({});
wrapper = mountComponent({
- promoteToEpicHandler: promoteToEpicMutationSuccessResponseHandler,
+ mutateResponse: promoteToEpicMutationResponse,
});
wrapper.find('[data-testid="promote-button"]').vm.$emit('click');
-
- await waitForPromises();
});
it('invokes GraphQL mutation when clicked', () => {
- expect(promoteToEpicMutationSuccessResponseHandler).toHaveBeenCalledWith({
- input: {
- iid: defaultProps.iid,
- projectPath: defaultProps.projectPath,
- },
- });
+ expect(mutateMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ mutation: promoteToEpicMutation,
+ variables: {
+ input: {
+ iid: defaultProps.iid,
+ projectPath: defaultProps.projectPath,
+ },
+ },
+ }),
+ );
});
it('shows a success message and tells the user they are being redirected', () => {
@@ -376,16 +329,14 @@ describe('HeaderActions component', () => {
});
describe('when response contains errors', () => {
- beforeEach(async () => {
+ beforeEach(() => {
visitUrlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({});
wrapper = mountComponent({
- promoteToEpicHandler: promoteToEpicMutationErrorHandler,
+ mutateResponse: promoteToEpicMutationErrorResponse,
});
wrapper.find('[data-testid="promote-button"]').vm.$emit('click');
-
- await waitForPromises();
});
it('shows an error message', () => {
@@ -398,17 +349,21 @@ describe('HeaderActions component', () => {
describe('when `toggle.issuable.state` event is emitted', () => {
it('invokes a method to toggle the issue state', () => {
- wrapper = mountComponent();
+ wrapper = mountComponent({ mutateResponse: updateIssueMutationResponse });
eventHub.$emit('toggle.issuable.state');
- expect(updateIssueMutationResponseHandler).toHaveBeenCalledWith({
- input: {
- iid: defaultProps.iid,
- projectPath: defaultProps.projectPath,
- stateEvent: ISSUE_STATE_EVENT_CLOSE,
- },
- });
+ expect(mutateMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ variables: {
+ input: {
+ iid: defaultProps.iid,
+ projectPath: defaultProps.projectPath,
+ stateEvent: ISSUE_STATE_EVENT_CLOSE,
+ },
+ },
+ }),
+ );
});
});
@@ -437,13 +392,17 @@ describe('HeaderActions component', () => {
it('calls apollo mutation when primary button is clicked', () => {
findModal().vm.$emit('primary');
- expect(updateIssueMutationResponseHandler).toHaveBeenCalledWith({
- input: {
- iid: defaultProps.iid.toString(),
- projectPath: defaultProps.projectPath,
- stateEvent: ISSUE_STATE_EVENT_CLOSE,
- },
- });
+ expect(mutateMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ variables: {
+ input: {
+ iid: defaultProps.iid.toString(),
+ projectPath: defaultProps.projectPath,
+ stateEvent: ISSUE_STATE_EVENT_CLOSE,
+ },
+ },
+ }),
+ );
});
describe.each`
@@ -475,6 +434,8 @@ describe('HeaderActions component', () => {
});
describe('abuse category selector', () => {
+ const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
+
beforeEach(() => {
wrapper = mountComponent({ props: { isIssueAuthor: false } });
});
@@ -484,7 +445,7 @@ describe('HeaderActions component', () => {
});
it('opens the drawer', async () => {
- findReportAbuseSelectorItem().vm.$emit('click');
+ findDesktopDropdownItems().at(2).vm.$emit('click');
await nextTick();
@@ -492,160 +453,10 @@ describe('HeaderActions component', () => {
});
it('closes the drawer', async () => {
- await findReportAbuseSelectorItem().vm.$emit('click');
+ await findDesktopDropdownItems().at(2).vm.$emit('click');
await findAbuseCategorySelector().vm.$emit('close-drawer');
expect(findAbuseCategorySelector().exists()).toEqual(false);
});
});
-
- describe('notification toggle', () => {
- describe('visibility', () => {
- describe.each`
- movedMrSidebarEnabled | issueType | visible
- ${true} | ${TYPE_ISSUE} | ${true}
- ${true} | ${TYPE_INCIDENT} | ${true}
- ${false} | ${TYPE_ISSUE} | ${false}
- ${false} | ${TYPE_INCIDENT} | ${false}
- `(
- `when movedMrSidebarEnabled flag is "$movedMrSidebarEnabled" with issue type "$issueType"`,
- ({ movedMrSidebarEnabled, issueType, visible }) => {
- beforeEach(() => {
- wrapper = mountComponent({
- props: {
- issueType,
- },
- movedMrSidebarEnabled,
- });
- });
-
- it(`${visible ? 'shows' : 'hides'} Notification toggle`, () => {
- expect(findNotificationWidget().exists()).toBe(visible);
- });
- },
- );
- });
- });
-
- describe('lock issue option', () => {
- describe('visibility', () => {
- describe.each`
- movedMrSidebarEnabled | issueType | visible
- ${true} | ${TYPE_ISSUE} | ${true}
- ${true} | ${TYPE_INCIDENT} | ${false}
- ${false} | ${TYPE_ISSUE} | ${false}
- ${false} | ${TYPE_INCIDENT} | ${false}
- `(
- `when movedMrSidebarEnabled flag is "$movedMrSidebarEnabled" with issue type "$issueType"`,
- ({ movedMrSidebarEnabled, issueType, visible }) => {
- beforeEach(() => {
- wrapper = mountComponent({
- props: {
- issueType,
- },
- movedMrSidebarEnabled,
- });
- });
-
- it(`${visible ? 'shows' : 'hides'} Lock issue option`, () => {
- expect(findLockIssueWidget().exists()).toBe(visible);
- });
- },
- );
- });
- });
-
- describe('copy reference option', () => {
- describe('visibility', () => {
- describe.each`
- movedMrSidebarEnabled | issueType | visible
- ${true} | ${TYPE_ISSUE} | ${true}
- ${true} | ${TYPE_INCIDENT} | ${true}
- ${false} | ${TYPE_ISSUE} | ${false}
- ${false} | ${TYPE_INCIDENT} | ${false}
- `(
- 'when movedMrSidebarFlagEnabled is "$movedMrSidebarEnabled" with issue type "$issueType"',
- ({ movedMrSidebarEnabled, issueType, visible }) => {
- beforeEach(() => {
- wrapper = mountComponent({
- props: {
- issueType,
- },
- movedMrSidebarEnabled,
- });
- });
-
- it(`${visible ? 'shows' : 'hides'} Copy reference option`, () => {
- expect(findCopyRefenceDropdownItem().exists()).toBe(visible);
- });
- },
- );
- });
-
- describe('clicking when visible', () => {
- beforeEach(() => {
- wrapper = mountComponent({
- props: {
- issueType: TYPE_ISSUE,
- },
- movedMrSidebarEnabled: true,
- });
- });
-
- it('shows toast message', () => {
- findCopyRefenceDropdownItem().vm.$emit('click');
-
- expect(toast).toHaveBeenCalledWith('Reference copied');
- });
- });
- });
-
- describe('copy email option', () => {
- describe('visibility', () => {
- describe.each`
- movedMrSidebarEnabled | issueType | issuableEmailAddress | visible
- ${true} | ${TYPE_ISSUE} | ${'mock-email-address'} | ${true}
- ${true} | ${TYPE_ISSUE} | ${''} | ${false}
- ${true} | ${TYPE_INCIDENT} | ${'mock-email-address'} | ${true}
- ${true} | ${TYPE_INCIDENT} | ${''} | ${false}
- ${false} | ${TYPE_ISSUE} | ${'mock-email-address'} | ${false}
- ${false} | ${TYPE_INCIDENT} | ${'mock-email-address'} | ${false}
- `(
- 'when movedMrSidebarEnabled flag is "$movedMrSidebarEnabled" issue type is "$issueType" and issuableEmailAddress="$issuableEmailAddress"',
- ({ movedMrSidebarEnabled, issueType, issuableEmailAddress, visible }) => {
- beforeEach(() => {
- wrapper = mountComponent({
- props: {
- issueType,
- issuableEmailAddress,
- },
- movedMrSidebarEnabled,
- });
- });
-
- it(`${visible ? 'shows' : 'hides'} Copy email option`, () => {
- expect(findCopyEmailItem().exists()).toBe(visible);
- });
- },
- );
- });
-
- describe('clicking when visible', () => {
- beforeEach(() => {
- wrapper = mountComponent({
- props: {
- issueType: TYPE_ISSUE,
- issuableEmailAddress: 'mock-email-address',
- },
- movedMrSidebarEnabled: true,
- });
- });
-
- it('shows toast message', () => {
- findCopyEmailItem().vm.$emit('click');
-
- expect(toast).toHaveBeenCalledWith('Email address copied');
- });
- });
- });
});
diff --git a/spec/frontend/issues/show/components/new_header_actions_popover_spec.js b/spec/frontend/issues/show/components/new_header_actions_popover_spec.js
deleted file mode 100644
index bf3e81c7d3a..00000000000
--- a/spec/frontend/issues/show/components/new_header_actions_popover_spec.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import { GlPopover } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import NewHeaderActionsPopover from '~/issues/show/components/new_header_actions_popover.vue';
-import { NEW_ACTIONS_POPOVER_KEY } from '~/issues/show/constants';
-import { TYPE_ISSUE } from '~/issues/constants';
-import * as utils from '~/lib/utils/common_utils';
-
-describe('NewHeaderActionsPopover', () => {
- let wrapper;
-
- const createComponent = ({ issueType = TYPE_ISSUE, movedMrSidebarEnabled = true }) => {
- wrapper = shallowMountExtended(NewHeaderActionsPopover, {
- propsData: {
- issueType,
- },
- stubs: {
- GlPopover,
- },
- provide: {
- glFeatures: {
- movedMrSidebar: movedMrSidebarEnabled,
- },
- },
- });
- };
-
- const findPopover = () => wrapper.findComponent(GlPopover);
- const findConfirmButton = () => wrapper.findByTestId('confirm-button');
-
- it('should not be visible when the feature flag :moved_mr_sidebar is disabled', () => {
- createComponent({ movedMrSidebarEnabled: false });
- expect(findPopover().exists()).toBe(false);
- });
-
- describe('without the popover cookie', () => {
- beforeEach(() => {
- utils.setCookie = jest.fn();
-
- createComponent({});
- });
-
- it('renders the popover with correct text', () => {
- expect(findPopover().exists()).toBe(true);
- expect(findPopover().text()).toContain('issue actions');
- });
-
- it('does not call setCookie', () => {
- expect(utils.setCookie).not.toHaveBeenCalled();
- });
-
- describe('when the confirm button is clicked', () => {
- beforeEach(() => {
- findConfirmButton().vm.$emit('click');
- });
-
- it('sets the popover cookie', () => {
- expect(utils.setCookie).toHaveBeenCalledWith(NEW_ACTIONS_POPOVER_KEY, true);
- });
-
- it('hides the popover', () => {
- expect(findPopover().exists()).toBe(false);
- });
- });
- });
-
- describe('with the popover cookie', () => {
- beforeEach(() => {
- jest.spyOn(utils, 'getCookie').mockReturnValue('true');
-
- createComponent({});
- });
-
- it('does not render the popover', () => {
- expect(findPopover().exists()).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js
index 46d27528443..8dabc076980 100644
--- a/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js
+++ b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js
@@ -1,4 +1,4 @@
-import { GlSkeletonLoader, GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
+import { GlSkeletonLoader, GlLoadingIcon, GlEmptyState, GlAlert } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
@@ -21,11 +21,13 @@ describe('Job table app', () => {
const successHandler = jest.fn().mockResolvedValue(mockAllJobsResponsePaginated);
const emptyHandler = jest.fn().mockResolvedValue(mockJobsResponseEmpty);
+ const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findLoadingSpinner = () => wrapper.findComponent(GlLoadingIcon);
const findTable = () => wrapper.findComponent(JobsTable);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findAlert = () => wrapper.findComponent(GlAlert);
const createMockApolloProvider = (handler) => {
const requestHandlers = [[getJobsQuery, handler]];
@@ -84,4 +86,15 @@ describe('Job table app', () => {
expect(findTable().exists()).toBe(true);
});
});
+
+ describe('error state', () => {
+ it('should show an alert if there is an error fetching the jobs data', async () => {
+ createComponent({ handler: failedHandler });
+
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe('There was an error fetching the jobs.');
+ expect(findTable().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
index 5e766e9a41c..d26ef7298ce 100644
--- a/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
+++ b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
@@ -29,7 +29,6 @@ describe('IssuableLockForm', () => {
const findEditForm = () => wrapper.findComponent(EditForm);
const findSidebarLockStatusTooltip = () =>
getBinding(findSidebarCollapseIcon().element, 'gl-tooltip');
- const findIssuableLockClickable = () => wrapper.find('[data-testid="issuable-lock"]');
const initStore = (isLocked) => {
if (issuableType === ISSUABLE_TYPE_ISSUE) {
@@ -49,7 +48,7 @@ describe('IssuableLockForm', () => {
store.getters.getNoteableData.discussion_locked = isLocked;
};
- const createComponent = ({ props = {}, movedMrSidebar = false }) => {
+ const createComponent = ({ props = {} }, movedMrSidebar = false) => {
wrapper = shallowMount(IssuableLockForm, {
store,
provide: {
@@ -170,27 +169,11 @@ describe('IssuableLockForm', () => {
`('displays $message when merge request is $locked', async ({ locked, message }) => {
initStore(locked);
- createComponent({ movedMrSidebar: true });
+ createComponent({}, true);
await wrapper.find('.dropdown-item').trigger('click');
expect(toast).toHaveBeenCalledWith(message);
});
});
-
- describe('moved_mr_sidebar flag', () => {
- describe('when the flag is off', () => {
- it('does not show the non editable lock status', () => {
- createComponent({ movedMrSidebar: false });
- expect(findIssuableLockClickable().exists()).toBe(false);
- });
- });
-
- describe('when the flag is on', () => {
- it('does not show the non editable lock status', () => {
- createComponent({ movedMrSidebar: true });
- expect(findIssuableLockClickable().exists()).toBe(true);
- });
- });
- });
});
diff --git a/spec/frontend/vue_shared/issuable/list/mock_data.js b/spec/frontend/vue_shared/issuable/list/mock_data.js
index b67bd0f42fe..964b48f4275 100644
--- a/spec/frontend/vue_shared/issuable/list/mock_data.js
+++ b/spec/frontend/vue_shared/issuable/list/mock_data.js
@@ -60,6 +60,12 @@ export const mockIssuable = {
type: 'issue',
};
+export const mockIssuableItems = (n) =>
+ [...Array(n).keys()].map((i) => ({
+ id: i,
+ ...mockIssuable,
+ }));
+
export const mockIssuables = [
mockIssuable,
{
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 38cbb5a1d66..d940c696fb3 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe IssuesHelper do
- include Features::MergeRequestHelpers
-
let_it_be(:project) { create(:project) }
let_it_be_with_reload(:issue) { create(:issue, project: project) }
@@ -237,13 +235,10 @@ RSpec.describe IssuesHelper do
describe '#issue_header_actions_data' do
let(:current_user) { create(:user) }
- let(:merge_request) { create(:merge_request, :opened, source_project: project, author: current_user) }
- let(:issuable_sidebar_issue) { serialize_issuable_sidebar(current_user, project, merge_request) }
before do
allow(helper).to receive(:current_user).and_return(current_user)
allow(helper).to receive(:can?).and_return(true)
- allow(helper).to receive(:issuable_sidebar).and_return(issuable_sidebar_issue)
end
it 'returns expected result' do
@@ -262,11 +257,10 @@ RSpec.describe IssuesHelper do
report_abuse_path: add_category_abuse_reports_path,
reported_user_id: issue.author.id,
reported_from_url: issue_url(issue),
- submit_as_spam_path: mark_as_spam_project_issue_path(project, issue),
- issuable_email_address: issuable_sidebar_issue[:create_note_email]
+ submit_as_spam_path: mark_as_spam_project_issue_path(project, issue)
}
- expect(helper.issue_header_actions_data(project, issue, current_user, issuable_sidebar_issue)).to include(expected)
+ expect(helper.issue_header_actions_data(project, issue, current_user)).to include(expected)
end
end
diff --git a/spec/initializers/doorkeeper_openid_connect_patch_spec.rb b/spec/initializers/doorkeeper_openid_connect_patch_spec.rb
new file mode 100644
index 00000000000..c04d7d95de6
--- /dev/null
+++ b/spec/initializers/doorkeeper_openid_connect_patch_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_relative '../../config/initializers/doorkeeper_openid_connect_patch'
+
+RSpec.describe 'doorkeeper_openid_connect_patch', feature_category: :integrations do
+ describe '.signing_key' do
+ let(:config) { Doorkeeper::OpenidConnect::Config.new }
+
+ before do
+ allow(config).to receive(:signing_key).and_return(key)
+ allow(config).to receive(:signing_algorithm).and_return(algorithm)
+ allow(Doorkeeper::OpenidConnect).to receive(:configuration).and_return(config)
+ end
+
+ context 'with RS256 algorithm' do
+ let(:algorithm) { :RS256 }
+ # Taken from https://github.com/doorkeeper-gem/doorkeeper-openid_connect/blob/01903c81a2b6237a3bf576ed45864f69ef20184e/spec/dummy/config/initializers/doorkeeper_openid_connect.rb#L6-L34
+ let(:key) do
+ <<~KEY
+ -----BEGIN RSA PRIVATE KEY-----
+ MIIEpgIBAAKCAQEAsjdnSA6UWUQQHf6BLIkIEUhMRNBJC1NN/pFt1EJmEiI88GS0
+ ceROO5B5Ooo9Y3QOWJ/n+u1uwTHBz0HCTN4wgArWd1TcqB5GQzQRP4eYnWyPfi4C
+ feqAHzQp+v4VwbcK0LW4FqtW5D0dtrFtI281FDxLhARzkhU2y7fuYhL8fVw5rUhE
+ 8uwvHRZ5CEZyxf7BSHxIvOZAAymhuzNLATt2DGkDInU1BmF75tEtBJAVLzWG/j4L
+ PZh1EpSdfezqaXQlcy9PJi916UzTl0P7Yy+ulOdUsMlB6yo8qKTY1+AbZ5jzneHb
+ GDU/O8QjYvii1WDmJ60t0jXicmOkGrOhruOptwIDAQABAoIBAQChYNwMeu9IugJi
+ NsEf4+JDTBWMRpOuRrwcpfIvQAUPrKNEB90COPvCoju0j9OxCDmpdPtq1K/zD6xx
+ khlw485FVAsKufSp4+g6GJ75yT6gZtq1JtKo1L06BFFzb7uh069eeP7+wB6JxPHw
+ KlAqwxvsfADhxeolQUKCTMb3Vjv/Aw2cO/nn6RAOeftw2aDmFy8Xl+oTUtSxyib0
+ YCdU9cK8MxsxDdmowwHp04xRTm/wfG5hLEn7HMz1PP86iP9BiFsCqTId9dxEUTS1
+ K+VAt9FbxRAq5JlBocxUMHNxLigb94Ca2FOMR7F6l/tronLfHD801YoObF0fN9qW
+ Cgw4aTO5AoGBAOR79hiZVM7/l1cBid7hKSeMWKUZ/nrwJsVfNpu1H9xt9uDu+79U
+ mcGfM7pm7L2qCNGg7eeWBHq2CVg/XQacRNtcTlomFrw4tDXUkFN1hE56t1iaTs9m
+ dN9IDr6jFgf6UaoOxxoPT9Q1ZtO46l043Nzrkoz8cBEBaBY20bUDwCYjAoGBAMet
+ tt1ImGF1cx153KbOfjl8v54VYUVkmRNZTa1E821nL/EMpoONSqJmRVsX7grLyPL1
+ QyZe245NOvn63YM0ng0rn2osoKsMVJwYBEYjHL61iF6dPtW5p8FIs7auRnC3NrG0
+ XxHATZ4xhHD0iIn14iXh0XIhUVk+nGktHU1gbmVdAoGBANniwKdqqS6RHKBTDkgm
+ Dhnxw6MGa+CO3VpA1xGboxuRHeoY3KfzpIC5MhojBsZDvQ8zWUwMio7+w2CNZEfm
+ g99wYiOjyPCLXocrAssj+Rzh97AdzuQHf5Jh4/W2Dk9jTbdPSl02ltj2Z+2lnJFz
+ pWNjnqimHrSI09rDQi5NulJjAoGBAImquujVpDmNQFCSNA7NTzlTSMk09FtjgCZW
+ 67cKUsqa2fLXRfZs84gD+s1TMks/NMxNTH6n57e0h3TSAOb04AM0kDQjkKJdXfhA
+ lrHEg4z4m4yf3TJ9Tat09HJ+tRIBPzRFp0YVz23Btg4qifiUDdcQWdbWIb/l6vCY
+ qhsu4O4BAoGBANbceYSDYRdT7a5QjJGibkC90Z3vFe4rDTBgZWg7xG0cpSU4JNg7
+ SFR3PjWQyCg7aGGXiooCM38YQruACTj0IFub24MFRA4ZTXvrACvpsVokJlQiG0Z4
+ tuQKYki41JvYqPobcq/rLE/AM7PKJftW35nqFuj0MrsUwPacaVwKBf5J
+ -----END RSA PRIVATE KEY-----
+ KEY
+ end
+
+ it 'returns the private key as JWK instance' do
+ expect(Doorkeeper::OpenidConnect.signing_key).to be_a ::JWT::JWK::KeyBase
+ expect(Doorkeeper::OpenidConnect.signing_key.kid).to eq 'IqYwZo2cE6hsyhs48cU8QHH4GanKIx0S4Dc99kgTIMA'
+ end
+
+ it 'matches json-jwt implementation' do
+ json_jwt_key = OpenSSL::PKey::RSA.new(key).public_key.to_jwk.slice(:kty, :kid, :e, :n)
+ expect(Doorkeeper::OpenidConnect.signing_key.export.sort.to_json).to eq(json_jwt_key.sort.to_json)
+ end
+ end
+
+ context 'with HS512 algorithm' do
+ let(:algorithm) { :HS512 }
+ let(:key) { 'the_greatest_secret_key' }
+
+ it 'returns the HMAC public key parameters' do
+ expect(Doorkeeper::OpenidConnect.signing_key_normalized).to eq(
+ kty: 'oct',
+ kid: 'lyAW7LdxryFWQtLdgxZpOrI87APHrzJKgWLT0BkWVog'
+ )
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/jwt_authenticatable_spec.rb b/spec/lib/gitlab/jwt_authenticatable_spec.rb
index 92d5feceb75..9a06f9b91df 100644
--- a/spec/lib/gitlab/jwt_authenticatable_spec.rb
+++ b/spec/lib/gitlab/jwt_authenticatable_spec.rb
@@ -172,11 +172,17 @@ RSpec.describe Gitlab::JwtAuthenticatable do
end
it 'raises an error if iat is invalid' do
- encoded_message = JWT.encode(payload.merge(iat: 'wrong'), test_class.secret, 'HS256')
+ encoded_message = JWT.encode(payload.merge(iat: Time.current.to_i + 1), test_class.secret, 'HS256')
expect { test_class.decode_jwt(encoded_message, iat_after: true) }.to raise_error(JWT::DecodeError)
end
+ it 'raises InvalidPayload exception if iat is a string' do
+ expect do
+ JWT.encode(payload.merge(iat: 'wrong'), test_class.secret, 'HS256')
+ end.to raise_error(JWT::InvalidPayload)
+ end
+
it 'raises an error if iat is absent' do
encoded_message = JWT.encode(payload, test_class.secret, 'HS256')
diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb
index 294a5ee82ed..509a4bb921b 100644
--- a/spec/lib/gitlab/middleware/multipart_spec.rb
+++ b/spec/lib/gitlab/middleware/multipart_spec.rb
@@ -175,7 +175,7 @@ RSpec.describe Gitlab::Middleware::Multipart do
end
it 'raises an error' do
- expect { subject }.to raise_error(JWT::VerificationError, 'Signature verification raised')
+ expect { subject }.to raise_error(JWT::VerificationError, 'Signature verification failed')
end
end
@@ -191,7 +191,7 @@ RSpec.describe Gitlab::Middleware::Multipart do
end
it 'raises an error' do
- expect { subject }.to raise_error(JWT::VerificationError, 'Signature verification raised')
+ expect { subject }.to raise_error(JWT::VerificationError, 'Signature verification failed')
end
end
end
diff --git a/spec/lib/json_web_token/hmac_token_spec.rb b/spec/lib/json_web_token/hmac_token_spec.rb
index 016084eaf69..7c486b2fe1b 100644
--- a/spec/lib/json_web_token/hmac_token_spec.rb
+++ b/spec/lib/json_web_token/hmac_token_spec.rb
@@ -50,8 +50,8 @@ RSpec.describe JSONWebToken::HMACToken do
context 'that was generated using a different secret' do
let(:encoded_token) { described_class.new('some other secret').encoded }
- it "raises exception saying 'Signature verification raised" do
- expect { decoded_token }.to raise_error(JWT::VerificationError, 'Signature verification raised')
+ it "raises exception saying 'Signature verification failed" do
+ expect { decoded_token }.to raise_error(JWT::VerificationError, 'Signature verification failed')
end
end
diff --git a/spec/models/packages/npm/metadatum_spec.rb b/spec/models/packages/npm/metadatum_spec.rb
index ff8cce5310e..92daddded7e 100644
--- a/spec/models/packages/npm/metadatum_spec.rb
+++ b/spec/models/packages/npm/metadatum_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Packages::Npm::Metadatum, type: :model do
+RSpec.describe Packages::Npm::Metadatum, type: :model, feature_category: :package_registry do
describe 'relationships' do
it { is_expected.to belong_to(:package).inverse_of(:npm_metadatum) }
end
@@ -47,4 +47,16 @@ RSpec.describe Packages::Npm::Metadatum, type: :model do
end
end
end
+
+ describe 'scopes' do
+ describe '.package_id_in' do
+ let_it_be(:package) { create(:npm_package) }
+ let_it_be(:metadatum_1) { create(:npm_metadatum, package: package) }
+ let_it_be(:metadatum_2) { create(:npm_metadatum) }
+
+ it 'returns metadatums with the given package ids' do
+ expect(described_class.package_id_in([package.id])).to contain_exactly(metadatum_1)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb
index 633006735a5..f621af5d968 100644
--- a/spec/requests/api/npm_project_packages_spec.rb
+++ b/spec/requests/api/npm_project_packages_spec.rb
@@ -415,6 +415,60 @@ RSpec.describe API::NpmProjectPackages, feature_category: :package_registry do
end
end
end
+
+ context 'when the Npm-Command in headers is deprecate' do
+ let(:package_name) { "@#{group.path}/my_package_name" }
+ let(:headers) { build_token_auth_header(token.plaintext_token).merge('Npm-Command' => 'deprecate') }
+ let(:params) do
+ {
+ 'id' => project.id.to_s,
+ 'package_name' => package_name,
+ 'versions' => {
+ '1.0.1' => {
+ 'name' => package_name,
+ 'deprecated' => 'This version is deprecated'
+ },
+ '1.0.2' => {
+ 'name' => package_name
+ }
+ }
+ }
+ end
+
+ subject(:request) { put api("/projects/#{project.id}/packages/npm/#{package_name.sub('/', '%2f')}"), params: params, headers: headers }
+
+ context 'when the user is not authorized to destroy the package' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'does not call DeprecatePackageService' do
+ expect(::Packages::Npm::DeprecatePackageService).not_to receive(:new)
+
+ request
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when the user is authorized to destroy the package' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'calls DeprecatePackageService with the correct arguments' do
+ expect(::Packages::Npm::DeprecatePackageService).to receive(:new).with(project, params) do
+ double.tap do |service|
+ expect(service).to receive(:execute).with(async: true)
+ end
+ end
+
+ request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
end
def upload_package(package_name, params = {})
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 14b0a8b1cc1..f96fbf54f08 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -259,7 +259,7 @@ RSpec.describe Issues::UpdateService, :mailer, feature_category: :team_planning
it 'creates system note about issue type' do
update_issue(issue_type: 'incident')
- note = find_note('changed issue type to incident')
+ note = find_note('changed type from issue to incident')
expect(note).not_to eq(nil)
end
diff --git a/spec/services/packages/npm/deprecate_package_service_spec.rb b/spec/services/packages/npm/deprecate_package_service_spec.rb
new file mode 100644
index 00000000000..a3686e3a8b5
--- /dev/null
+++ b/spec/services/packages/npm/deprecate_package_service_spec.rb
@@ -0,0 +1,115 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Npm::DeprecatePackageService, feature_category: :package_registry do
+ let_it_be(:namespace) { create(:namespace) }
+ let_it_be(:project) { create(:project, namespace: namespace) }
+
+ let_it_be(:package_name) { "@#{namespace.path}/my-app" }
+ let_it_be_with_reload(:package_1) do
+ create(:npm_package, project: project, name: package_name, version: '1.0.1').tap do |package|
+ create(:npm_metadatum, package: package)
+ end
+ end
+
+ let_it_be(:package_2) do
+ create(:npm_package, project: project, name: package_name, version: '1.0.2').tap do |package|
+ create(:npm_metadatum, package: package)
+ end
+ end
+
+ let(:service) { described_class.new(project, params) }
+
+ subject(:execute) { service.execute }
+
+ describe '#execute' do
+ context 'when passing deprecatation message' do
+ let(:params) do
+ {
+ 'package_name' => package_name,
+ 'versions' => {
+ '1.0.1' => {
+ 'name' => package_name,
+ 'deprecated' => 'This version is deprecated'
+ },
+ '1.0.2' => {
+ 'name' => package_name,
+ 'deprecated' => 'This version is deprecated'
+ }
+ }
+ }
+ end
+
+ before do
+ package_json = package_2.npm_metadatum.package_json
+ package_2.npm_metadatum.update!(package_json: package_json.merge('deprecated' => 'old deprecation message'))
+ end
+
+ it 'adds or updates the deprecated field' do
+ expect { execute }
+ .to change { package_1.reload.npm_metadatum.package_json['deprecated'] }.to('This version is deprecated')
+ .and change { package_2.reload.npm_metadatum.package_json['deprecated'] }
+ .from('old deprecation message').to('This version is deprecated')
+ end
+
+ it 'executes 5 queries' do
+ queries = ActiveRecord::QueryRecorder.new do
+ execute
+ end
+
+ # 1. each_batch lower bound
+ # 2. each_batch upper bound
+ # 3. SELECT packages_packages.id, packages_packages.version FROM packages_packages
+ # 4. SELECT packages_npm_metadata.* FROM packages_npm_metadata
+ # 5. UPDATE packages_npm_metadata SET package_json =
+ expect(queries.count).to eq(5)
+ end
+ end
+
+ context 'when passing deprecated as empty string' do
+ let(:params) do
+ {
+ 'package_name' => package_name,
+ 'versions' => {
+ '1.0.1' => {
+ 'name' => package_name,
+ 'deprecated' => ''
+ }
+ }
+ }
+ end
+
+ before do
+ package_json = package_1.npm_metadatum.package_json
+ package_1.npm_metadatum.update!(package_json: package_json.merge('deprecated' => 'This version is deprecated'))
+ end
+
+ it 'removes the deprecation warning' do
+ expect { execute }
+ .to change { package_1.reload.npm_metadatum.package_json['deprecated'] }
+ .from('This version is deprecated').to(nil)
+ end
+ end
+
+ context 'when passing async: true to execute' do
+ let(:params) do
+ {
+ package_name: package_name,
+ versions: {
+ '1.0.1': {
+ deprecated: 'This version is deprecated'
+ }
+ }
+ }
+ end
+
+ it 'calls the worker and return' do
+ expect(::Packages::Npm::DeprecatePackageWorker).to receive(:perform_async).with(project.id, params)
+ expect(service).not_to receive(:packages)
+
+ service.execute(async: true)
+ end
+ end
+ end
+end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 38b6943b12a..1eb11c80264 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -692,10 +692,10 @@ RSpec.describe SystemNoteService, feature_category: :shared do
it 'calls IssuableService' do
expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
- expect(service).to receive(:change_issue_type)
+ expect(service).to receive(:change_issue_type).with('issue')
end
- described_class.change_issue_type(incident, author)
+ described_class.change_issue_type(incident, author, 'issue')
end
end
diff --git a/spec/services/system_notes/issuables_service_spec.rb b/spec/services/system_notes/issuables_service_spec.rb
index 08a91234174..af660a9b72e 100644
--- a/spec/services/system_notes/issuables_service_spec.rb
+++ b/spec/services/system_notes/issuables_service_spec.rb
@@ -861,15 +861,29 @@ RSpec.describe ::SystemNotes::IssuablesService, feature_category: :team_planning
end
describe '#change_issue_type' do
- let(:noteable) { create(:incident, project: project) }
+ context 'with issue' do
+ let_it_be_with_reload(:noteable) { create(:issue, project: project) }
- subject { service.change_issue_type }
+ subject { service.change_issue_type('incident') }
- it_behaves_like 'a system note' do
- let(:action) { 'issue_type' }
+ it_behaves_like 'a system note' do
+ let(:action) { 'issue_type' }
+ end
+
+ it { expect(subject.note).to eq "changed type from incident to issue" }
end
- it { expect(subject.note).to eq "changed issue type to incident" }
+ context 'with work item' do
+ let_it_be_with_reload(:noteable) { create(:work_item, project: project) }
+
+ subject { service.change_issue_type('task') }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'issue_type' }
+ end
+
+ it { expect(subject.note).to eq "changed type from task to issue" }
+ end
end
describe '#hierarchy_changed' do
diff --git a/spec/tooling/danger/specs/feature_category_suggestion_spec.rb b/spec/tooling/danger/specs/feature_category_suggestion_spec.rb
new file mode 100644
index 00000000000..3956553f488
--- /dev/null
+++ b/spec/tooling/danger/specs/feature_category_suggestion_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require 'gitlab/dangerfiles/spec_helper'
+
+require_relative '../../../../tooling/danger/specs'
+require_relative '../../../../tooling/danger/project_helper'
+
+RSpec.describe Tooling::Danger::Specs::FeatureCategorySuggestion, feature_category: :tooling do
+ include_context "with dangerfile"
+
+ let(:fake_danger) { DangerSpecHelper.fake_danger.include(Tooling::Danger::Specs) }
+ let(:fake_project_helper) { instance_double('Tooling::Danger::ProjectHelper') }
+ let(:filename) { 'spec/foo_spec.rb' }
+
+ let(:template) do
+ <<~SUGGESTION_MARKDOWN.chomp
+ ```suggestion
+ %<suggested_line>s
+ ```
+
+ Consider adding `feature_category: <feature_category_name>` for this example if it is not set already.
+ See [testing best practices](https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#feature-category-metadata).
+ SUGGESTION_MARKDOWN
+ end
+
+ let(:file_lines) do
+ [
+ " require 'spec_helper'",
+ " \n",
+ " RSpec.describe Projects::SummaryController, feature_category: :planning_analytics do",
+ " end",
+ "RSpec.describe Projects::SummaryController do",
+ " let_it_be(:user) { create(:user) }",
+ " end",
+ " describe 'GET \"time_summary\"' do",
+ " end",
+ " RSpec.describe Projects::SummaryController do",
+ " let_it_be(:user) { create(:user) }",
+ " end",
+ " describe 'GET \"time_summary\"' do",
+ " end",
+ " \n",
+ "RSpec.describe Projects :aggregate_failures,",
+ " feature_category: planning_analytics do",
+ " \n",
+ "RSpec.describe Epics :aggregate_failures,",
+ " ee: true do",
+ "\n",
+ "RSpec.describe Issues :aggregate_failures,",
+ " feature_category: :team_planning do",
+ "\n",
+ "RSpec.describe MergeRequest :aggregate_failures,",
+ " :js,",
+ " feature_category: :team_planning do"
+ ]
+ end
+
+ let(:changed_lines) do
+ [
+ "+ RSpec.describe Projects::SummaryController, feature_category: :planning_analytics do",
+ "+RSpec.describe Projects::SummaryController do",
+ "+ let_it_be(:user) { create(:user) }",
+ "- end",
+ "+ describe 'GET \"time_summary\"' do",
+ "+ RSpec.describe Projects::SummaryController do",
+ "+RSpec.describe Projects :aggregate_failures,",
+ "+ feature_category: planning_analytics do",
+ "+RSpec.describe Epics :aggregate_failures,",
+ "+ ee: true do",
+ "+RSpec.describe Issues :aggregate_failures,",
+ "+RSpec.describe MergeRequest :aggregate_failures,",
+ "+ :js,",
+ "+ feature_category: :team_planning do",
+ "+RSpec.describe 'line in commit diff but no longer in working copy' do"
+ ]
+ end
+
+ subject(:specs) { fake_danger.new(helper: fake_helper) }
+
+ before do
+ allow(specs).to receive(:project_helper).and_return(fake_project_helper)
+ allow(specs.helper).to receive(:changed_lines).with(filename).and_return(changed_lines)
+ allow(specs.project_helper).to receive(:file_lines).and_return(file_lines)
+ end
+
+ it 'adds suggestions at the correct lines', :aggregate_failures do
+ [
+ { suggested_line: "RSpec.describe Projects::SummaryController do", number: 5 },
+ { suggested_line: " RSpec.describe Projects::SummaryController do", number: 10 },
+ { suggested_line: "RSpec.describe Epics :aggregate_failures,", number: 19 }
+
+ ].each do |test_case|
+ comment = format(template, suggested_line: test_case[:suggested_line])
+ expect(specs).to receive(:markdown).with(comment, file: filename, line: test_case[:number])
+ end
+
+ specs.add_suggestions_for(filename)
+ end
+end
diff --git a/spec/tooling/danger/specs/match_with_array_suggestion_spec.rb b/spec/tooling/danger/specs/match_with_array_suggestion_spec.rb
new file mode 100644
index 00000000000..b065772a09b
--- /dev/null
+++ b/spec/tooling/danger/specs/match_with_array_suggestion_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require 'gitlab/dangerfiles/spec_helper'
+
+require_relative '../../../../tooling/danger/specs'
+require_relative '../../../../tooling/danger/project_helper'
+
+RSpec.describe Tooling::Danger::Specs::MatchWithArraySuggestion, feature_category: :tooling do
+ include_context "with dangerfile"
+
+ let(:fake_danger) { DangerSpecHelper.fake_danger.include(Tooling::Danger::Specs) }
+ let(:fake_project_helper) { instance_double('Tooling::Danger::ProjectHelper') }
+ let(:filename) { 'spec/foo_spec.rb' }
+
+ let(:file_lines) do
+ [
+ " describe 'foo' do",
+ " expect(foo).to match(['bar', 'baz'])",
+ " end",
+ " expect(foo).to match(['bar', 'baz'])", # same line as line 1 above, we expect two different suggestions
+ " ",
+ " expect(foo).to match ['bar', 'baz']",
+ " expect(foo).to eq(['bar', 'baz'])",
+ " expect(foo).to eq ['bar', 'baz']",
+ " expect(foo).to(match(['bar', 'baz']))",
+ " expect(foo).to(eq(['bar', 'baz']))",
+ " expect(foo).to(eq([bar, baz]))",
+ " expect(foo).to(eq(['bar']))",
+ " foo.eq(['bar'])"
+ ]
+ end
+
+ let(:matching_lines) do
+ [
+ "+ expect(foo).to match(['should not error'])",
+ "+ expect(foo).to match(['bar', 'baz'])",
+ "+ expect(foo).to match(['bar', 'baz'])",
+ "+ expect(foo).to match ['bar', 'baz']",
+ "+ expect(foo).to eq(['bar', 'baz'])",
+ "+ expect(foo).to eq ['bar', 'baz']",
+ "+ expect(foo).to(match(['bar', 'baz']))",
+ "+ expect(foo).to(eq(['bar', 'baz']))",
+ "+ expect(foo).to(eq([bar, baz]))"
+ ]
+ end
+
+ let(:changed_lines) do
+ [
+ " expect(foo).to match(['bar', 'baz'])",
+ " expect(foo).to match(['bar', 'baz'])",
+ " expect(foo).to match ['bar', 'baz']",
+ " expect(foo).to eq(['bar', 'baz'])",
+ " expect(foo).to eq ['bar', 'baz']",
+ "- expect(foo).to match(['bar', 'baz'])",
+ "- expect(foo).to match(['bar', 'baz'])",
+ "- expect(foo).to match ['bar', 'baz']",
+ "- expect(foo).to eq(['bar', 'baz'])",
+ "- expect(foo).to eq ['bar', 'baz']",
+ "- expect(foo).to eq [bar, foo]",
+ "+ expect(foo).to eq([])"
+ ] + matching_lines
+ end
+
+ let(:template) do
+ <<~MARKDOWN.chomp
+ ```suggestion
+ %<suggested_line>s
+ ```
+
+ If order of the result is not important, please consider using `match_array` to avoid flakiness.
+ MARKDOWN
+ end
+
+ subject(:specs) { fake_danger.new(helper: fake_helper) }
+
+ before do
+ allow(specs).to receive(:project_helper).and_return(fake_project_helper)
+ allow(specs.helper).to receive(:changed_lines).with(filename).and_return(changed_lines)
+ allow(specs.project_helper).to receive(:file_lines).and_return(file_lines)
+ end
+
+ it 'adds suggestions at the correct lines' do
+ [
+ { suggested_line: " expect(foo).to match_array(['bar', 'baz'])", number: 2 },
+ { suggested_line: " expect(foo).to match_array(['bar', 'baz'])", number: 4 },
+ { suggested_line: " expect(foo).to match_array ['bar', 'baz']", number: 6 },
+ { suggested_line: " expect(foo).to match_array(['bar', 'baz'])", number: 7 },
+ { suggested_line: " expect(foo).to match_array ['bar', 'baz']", number: 8 },
+ { suggested_line: " expect(foo).to(match_array(['bar', 'baz']))", number: 9 },
+ { suggested_line: " expect(foo).to(match_array(['bar', 'baz']))", number: 10 },
+ { suggested_line: " expect(foo).to(match_array([bar, baz]))", number: 11 }
+ ].each do |test_case|
+ comment = format(template, suggested_line: test_case[:suggested_line])
+ expect(specs).to receive(:markdown).with(comment, file: filename, line: test_case[:number])
+ end
+
+ specs.add_suggestions_for(filename)
+ end
+end
diff --git a/spec/tooling/danger/specs/project_factory_suggestion_spec.rb b/spec/tooling/danger/specs/project_factory_suggestion_spec.rb
new file mode 100644
index 00000000000..95ffcfb1460
--- /dev/null
+++ b/spec/tooling/danger/specs/project_factory_suggestion_spec.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require 'gitlab/dangerfiles/spec_helper'
+
+require_relative '../../../../tooling/danger/specs'
+require_relative '../../../../tooling/danger/project_helper'
+
+RSpec.describe Tooling::Danger::Specs::ProjectFactorySuggestion, feature_category: :tooling do
+ include_context "with dangerfile"
+
+ let(:fake_danger) { DangerSpecHelper.fake_danger.include(Tooling::Danger::Specs) }
+ let(:fake_project_helper) { instance_double('Tooling::Danger::ProjectHelper') }
+ let(:filename) { 'spec/foo_spec.rb' }
+
+ let(:template) do
+ <<~MARKDOWN.chomp
+ ```suggestion
+ %<suggested_line>s
+ ```
+
+ Project creations are very slow. Use `let_it_be`, `build` or `build_stubbed` if possible.
+ See [testing best practices](https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#optimize-factory-usage)
+ for background information and alternative options.
+ MARKDOWN
+ end
+
+ let(:file_lines) do
+ [
+ " let(:project) { create(:project) }",
+ " let_it_be(:project) { create(:project, :repository)",
+ " let!(:project) { create(:project) }",
+ " let(:var) { create(:project) }",
+ " let(:merge_request) { create(:merge_request, project: project)",
+ " context 'when merge request exists' do",
+ " it { is_expected.to be_success }",
+ " end",
+ " let!(:var) { create(:project) }",
+ " let(:project) { create(:thing) }",
+ " let(:project) { build(:project) }",
+ " let(:project) do",
+ " create(:project)",
+ " end",
+ " let(:project) { create(:project, :repository) }",
+ " str = 'let(:project) { create(:project) }'",
+ " let(:project) { create(:project_empty_repo) }",
+ " let(:project) { create(:forked_project_with_submodules) }",
+ " let(:project) { create(:project_with_design) }",
+ " let(:authorization) { create(:project_authorization) }"
+ ]
+ end
+
+ let(:matching_lines) do
+ [
+ "+ let(:should_not_error) { create(:project) }",
+ "+ let(:project) { create(:project) }",
+ "+ let!(:project) { create(:project) }",
+ "+ let(:var) { create(:project) }",
+ "+ let!(:var) { create(:project) }",
+ "+ let(:project) { create(:project, :repository) }",
+ "+ let(:project) { create(:project_empty_repo) }",
+ "+ let(:project) { create(:forked_project_with_submodules) }",
+ "+ let(:project) { create(:project_with_design) }"
+ ]
+ end
+
+ let(:changed_lines) do
+ [
+ "+ line which doesn't exist in the file and should not cause an error",
+ "+ let_it_be(:project) { create(:project, :repository)",
+ "+ let(:project) { create(:thing) }",
+ "+ let(:project) do",
+ "+ create(:project)",
+ "+ end",
+ "+ str = 'let(:project) { create(:project) }'",
+ "+ let(:authorization) { create(:project_authorization) }"
+ ] + matching_lines
+ end
+
+ subject(:specs) { fake_danger.new(helper: fake_helper) }
+
+ before do
+ allow(specs).to receive(:project_helper).and_return(fake_project_helper)
+ allow(specs.helper).to receive(:changed_lines).with(filename).and_return(changed_lines)
+ allow(specs.project_helper).to receive(:file_lines).and_return(file_lines)
+ end
+
+ it 'adds suggestions at the correct lines', :aggregate_failures do
+ [
+ { suggested_line: " let_it_be(:project) { create(:project) }", number: 1 },
+ { suggested_line: " let_it_be(:project) { create(:project) }", number: 3 },
+ { suggested_line: " let_it_be(:var) { create(:project) }", number: 4 },
+ { suggested_line: " let_it_be(:var) { create(:project) }", number: 9 },
+ { suggested_line: " let_it_be(:project) { create(:project, :repository) }", number: 15 },
+ { suggested_line: " let_it_be(:project) { create(:project_empty_repo) }", number: 17 },
+ { suggested_line: " let_it_be(:project) { create(:forked_project_with_submodules) }", number: 18 },
+ { suggested_line: " let_it_be(:project) { create(:project_with_design) }", number: 19 }
+ ].each do |test_case|
+ comment = format(template, suggested_line: test_case[:suggested_line])
+ expect(specs).to receive(:markdown).with(comment, file: filename, line: test_case[:number])
+ end
+
+ specs.add_suggestions_for(filename)
+ end
+end
diff --git a/spec/tooling/danger/specs_spec.rb b/spec/tooling/danger/specs_spec.rb
index 09550f037d6..b4953858ef7 100644
--- a/spec/tooling/danger/specs_spec.rb
+++ b/spec/tooling/danger/specs_spec.rb
@@ -1,80 +1,24 @@
# frozen_string_literal: true
-require 'rspec-parameterized'
-require 'gitlab-dangerfiles'
-require 'danger'
-require 'danger/plugins/internal/helper'
require 'gitlab/dangerfiles/spec_helper'
require_relative '../../../tooling/danger/specs'
-require_relative '../../../tooling/danger/project_helper'
RSpec.describe Tooling::Danger::Specs, feature_category: :tooling do
include_context "with dangerfile"
let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
- let(:fake_project_helper) { instance_double('Tooling::Danger::ProjectHelper') }
let(:filename) { 'spec/foo_spec.rb' }
- let(:file_lines) do
- [
- " describe 'foo' do",
- " expect(foo).to match(['bar', 'baz'])",
- " end",
- " expect(foo).to match(['bar', 'baz'])", # same line as line 1 above, we expect two different suggestions
- " ",
- " expect(foo).to match ['bar', 'baz']",
- " expect(foo).to eq(['bar', 'baz'])",
- " expect(foo).to eq ['bar', 'baz']",
- " expect(foo).to(match(['bar', 'baz']))",
- " expect(foo).to(eq(['bar', 'baz']))",
- " expect(foo).to(eq([bar, baz]))",
- " expect(foo).to(eq(['bar']))",
- " foo.eq(['bar'])"
- ]
- end
-
- let(:matching_lines) do
- [
- "+ expect(foo).to match(['should not error'])",
- "+ expect(foo).to match(['bar', 'baz'])",
- "+ expect(foo).to match(['bar', 'baz'])",
- "+ expect(foo).to match ['bar', 'baz']",
- "+ expect(foo).to eq(['bar', 'baz'])",
- "+ expect(foo).to eq ['bar', 'baz']",
- "+ expect(foo).to(match(['bar', 'baz']))",
- "+ expect(foo).to(eq(['bar', 'baz']))",
- "+ expect(foo).to(eq([bar, baz]))"
- ]
- end
-
- let(:changed_lines) do
- [
- " expect(foo).to match(['bar', 'baz'])",
- " expect(foo).to match(['bar', 'baz'])",
- " expect(foo).to match ['bar', 'baz']",
- " expect(foo).to eq(['bar', 'baz'])",
- " expect(foo).to eq ['bar', 'baz']",
- "- expect(foo).to match(['bar', 'baz'])",
- "- expect(foo).to match(['bar', 'baz'])",
- "- expect(foo).to match ['bar', 'baz']",
- "- expect(foo).to eq(['bar', 'baz'])",
- "- expect(foo).to eq ['bar', 'baz']",
- "- expect(foo).to eq [bar, foo]",
- "+ expect(foo).to eq([])"
- ] + matching_lines
- end
-
subject(:specs) { fake_danger.new(helper: fake_helper) }
- before do
- allow(specs).to receive(:project_helper).and_return(fake_project_helper)
- allow(specs.helper).to receive(:changed_lines).with(filename).and_return(matching_lines)
- allow(specs.project_helper).to receive(:file_lines).and_return(file_lines)
- end
-
describe '#changed_specs_files' do
- let(:base_expected_files) { %w[spec/foo_spec.rb ee/spec/foo_spec.rb spec/bar_spec.rb ee/spec/bar_spec.rb spec/zab_spec.rb ee/spec/zab_spec.rb] }
+ let(:base_expected_files) do
+ %w[
+ spec/foo_spec.rb ee/spec/foo_spec.rb spec/bar_spec.rb
+ ee/spec/bar_spec.rb spec/zab_spec.rb ee/spec/zab_spec.rb
+ ]
+ end
before do
all_changed_files = %w[
@@ -98,211 +42,16 @@ RSpec.describe Tooling::Danger::Specs, feature_category: :tooling do
context 'with include_ee: :exclude' do
it 'returns spec files without EE-specific files' do
- expect(specs.changed_specs_files(ee: :exclude)).not_to include(%w[ee/spec/foo_spec.rb ee/spec/bar_spec.rb ee/spec/zab_spec.rb])
+ expect(specs.changed_specs_files(ee: :exclude))
+ .not_to include(%w[ee/spec/foo_spec.rb ee/spec/bar_spec.rb ee/spec/zab_spec.rb])
end
end
context 'with include_ee: :only' do
it 'returns EE-specific spec files only' do
- expect(specs.changed_specs_files(ee: :only)).to match_array(%w[ee/spec/foo_spec.rb ee/spec/bar_spec.rb ee/spec/zab_spec.rb])
- end
- end
- end
-
- describe '#add_suggestions_for_match_with_array' do
- let(:template) do
- <<~MARKDOWN.chomp
- ```suggestion
- %<suggested_line>s
- ```
-
- If order of the result is not important, please consider using `match_array` to avoid flakiness.
- MARKDOWN
- end
-
- it 'adds suggestions at the correct lines' do
- [
- { suggested_line: " expect(foo).to match_array(['bar', 'baz'])", number: 2 },
- { suggested_line: " expect(foo).to match_array(['bar', 'baz'])", number: 4 },
- { suggested_line: " expect(foo).to match_array ['bar', 'baz']", number: 6 },
- { suggested_line: " expect(foo).to match_array(['bar', 'baz'])", number: 7 },
- { suggested_line: " expect(foo).to match_array ['bar', 'baz']", number: 8 },
- { suggested_line: " expect(foo).to(match_array(['bar', 'baz']))", number: 9 },
- { suggested_line: " expect(foo).to(match_array(['bar', 'baz']))", number: 10 },
- { suggested_line: " expect(foo).to(match_array([bar, baz]))", number: 11 }
- ].each do |test_case|
- comment = format(template, suggested_line: test_case[:suggested_line])
- expect(specs).to receive(:markdown).with(comment, file: filename, line: test_case[:number])
- end
-
- specs.add_suggestions_for_match_with_array(filename)
- end
- end
-
- describe '#add_suggestions_for_project_factory_usage' do
- let(:template) do
- <<~MARKDOWN.chomp
- ```suggestion
- %<suggested_line>s
- ```
-
- Project creations are very slow. Use `let_it_be`, `build` or `build_stubbed` if possible.
- See [testing best practices](https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#optimize-factory-usage)
- for background information and alternative options.
- MARKDOWN
- end
-
- let(:file_lines) do
- [
- " let(:project) { create(:project) }",
- " let_it_be(:project) { create(:project, :repository)",
- " let!(:project) { create(:project) }",
- " let(:var) { create(:project) }",
- " let(:merge_request) { create(:merge_request, project: project)",
- " context 'when merge request exists' do",
- " it { is_expected.to be_success }",
- " end",
- " let!(:var) { create(:project) }",
- " let(:project) { create(:thing) }",
- " let(:project) { build(:project) }",
- " let(:project) do",
- " create(:project)",
- " end",
- " let(:project) { create(:project, :repository) }",
- " str = 'let(:project) { create(:project) }'",
- " let(:project) { create(:project_empty_repo) }",
- " let(:project) { create(:forked_project_with_submodules) }",
- " let(:project) { create(:project_with_design) }",
- " let(:authorization) { create(:project_authorization) }"
- ]
- end
-
- let(:matching_lines) do
- [
- "+ let(:should_not_error) { create(:project) }",
- "+ let(:project) { create(:project) }",
- "+ let!(:project) { create(:project) }",
- "+ let(:var) { create(:project) }",
- "+ let!(:var) { create(:project) }",
- "+ let(:project) { create(:project, :repository) }",
- "+ let(:project) { create(:project_empty_repo) }",
- "+ let(:project) { create(:forked_project_with_submodules) }",
- "+ let(:project) { create(:project_with_design) }"
- ]
- end
-
- let(:changed_lines) do
- [
- "+ line which doesn't exist in the file and should not cause an error",
- "+ let_it_be(:project) { create(:project, :repository)",
- "+ let(:project) { create(:thing) }",
- "+ let(:project) do",
- "+ create(:project)",
- "+ end",
- "+ str = 'let(:project) { create(:project) }'",
- "+ let(:authorization) { create(:project_authorization) }"
- ] + matching_lines
- end
-
- it 'adds suggestions at the correct lines', :aggregate_failures do
- [
- { suggested_line: " let_it_be(:project) { create(:project) }", number: 1 },
- { suggested_line: " let_it_be(:project) { create(:project) }", number: 3 },
- { suggested_line: " let_it_be(:var) { create(:project) }", number: 4 },
- { suggested_line: " let_it_be(:var) { create(:project) }", number: 9 },
- { suggested_line: " let_it_be(:project) { create(:project, :repository) }", number: 15 },
- { suggested_line: " let_it_be(:project) { create(:project_empty_repo) }", number: 17 },
- { suggested_line: " let_it_be(:project) { create(:forked_project_with_submodules) }", number: 18 },
- { suggested_line: " let_it_be(:project) { create(:project_with_design) }", number: 19 }
- ].each do |test_case|
- comment = format(template, suggested_line: test_case[:suggested_line])
- expect(specs).to receive(:markdown).with(comment, file: filename, line: test_case[:number])
- end
-
- specs.add_suggestions_for_project_factory_usage(filename)
- end
- end
-
- describe '#add_suggestions_for_feature_category' do
- let(:template) do
- <<~SUGGESTION_MARKDOWN.chomp
- ```suggestion
- %<suggested_line>s
- ```
-
- Consider adding `feature_category: <feature_category_name>` for this example if it is not set already.
- See [testing best practices](https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#feature-category-metadata).
- SUGGESTION_MARKDOWN
- end
-
- let(:file_lines) do
- [
- " require 'spec_helper'",
- " \n",
- " RSpec.describe Projects::Analytics::CycleAnalytics::SummaryController, feature_category: :planning_analytics do",
- " end",
- "RSpec.describe Projects::Analytics::CycleAnalytics::SummaryController do",
- " let_it_be(:user) { create(:user) }",
- " end",
- " describe 'GET \"time_summary\"' do",
- " end",
- " RSpec.describe Projects::Analytics::CycleAnalytics::SummaryController do",
- " let_it_be(:user) { create(:user) }",
- " end",
- " describe 'GET \"time_summary\"' do",
- " end",
- " \n",
- "RSpec.describe Projects :aggregate_failures,",
- " feature_category: planning_analytics do",
- " \n",
- "RSpec.describe Epics :aggregate_failures,",
- " ee: true do",
- "\n",
- "RSpec.describe Issues :aggregate_failures,",
- " feature_category: :team_planning do",
- "\n",
- "RSpec.describe MergeRequest :aggregate_failures,",
- " :js,",
- " feature_category: :team_planning do"
- ]
- end
-
- let(:changed_lines) do
- [
- "+ RSpec.describe Projects::Analytics::CycleAnalytics::SummaryController, feature_category: :planning_analytics do",
- "+RSpec.describe Projects::Analytics::CycleAnalytics::SummaryController do",
- "+ let_it_be(:user) { create(:user) }",
- "- end",
- "+ describe 'GET \"time_summary\"' do",
- "+ RSpec.describe Projects::Analytics::CycleAnalytics::SummaryController do",
- "+RSpec.describe Projects :aggregate_failures,",
- "+ feature_category: planning_analytics do",
- "+RSpec.describe Epics :aggregate_failures,",
- "+ ee: true do",
- "+RSpec.describe Issues :aggregate_failures,",
- "+RSpec.describe MergeRequest :aggregate_failures,",
- "+ :js,",
- "+ feature_category: :team_planning do",
- "+RSpec.describe 'line in commit diff but no longer in working copy' do"
- ]
- end
-
- before do
- allow(specs.helper).to receive(:changed_lines).with(filename).and_return(changed_lines)
- end
-
- it 'adds suggestions at the correct lines', :aggregate_failures do
- [
- { suggested_line: "RSpec.describe Projects::Analytics::CycleAnalytics::SummaryController do", number: 5 },
- { suggested_line: " RSpec.describe Projects::Analytics::CycleAnalytics::SummaryController do", number: 10 },
- { suggested_line: "RSpec.describe Epics :aggregate_failures,", number: 19 }
-
- ].each do |test_case|
- comment = format(template, suggested_line: test_case[:suggested_line])
- expect(specs).to receive(:markdown).with(comment, file: filename, line: test_case[:number])
+ expect(specs.changed_specs_files(ee: :only))
+ .to match_array(%w[ee/spec/foo_spec.rb ee/spec/bar_spec.rb ee/spec/zab_spec.rb])
end
-
- specs.add_suggestions_for_feature_category(filename)
end
end
end
diff --git a/spec/workers/packages/npm/deprecate_package_worker_spec.rb b/spec/workers/packages/npm/deprecate_package_worker_spec.rb
new file mode 100644
index 00000000000..100a8a3af73
--- /dev/null
+++ b/spec/workers/packages/npm/deprecate_package_worker_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Npm::DeprecatePackageWorker, feature_category: :package_registry do
+ describe '#perform' do
+ let_it_be(:project) { create(:project) }
+ let(:worker) { described_class.new }
+ let(:params) do
+ {
+ package_name: 'package_name',
+ versions: {
+ '1.0.1' => {
+ name: 'package_name',
+ deprecated: 'This version is deprecated'
+ }
+ }
+ }
+ end
+
+ include_examples 'an idempotent worker' do
+ let(:job_args) { [project.id, params] }
+
+ it 'calls the deprecation service' do
+ expect(::Packages::Npm::DeprecatePackageService).to receive(:new).with(project, params) do
+ double.tap do |service|
+ expect(service).to receive(:execute)
+ end
+ end
+
+ worker.perform(*job_args)
+ end
+ end
+ end
+end
diff --git a/tooling/danger/specs.rb b/tooling/danger/specs.rb
index 5359e71f8cc..68cab141656 100644
--- a/tooling/danger/specs.rb
+++ b/tooling/danger/specs.rb
@@ -1,55 +1,19 @@
# frozen_string_literal: true
-require_relative 'suggestor'
+Dir[File.expand_path('specs/*_suggestion.rb', __dir__)].each { |file| require file }
module Tooling
module Danger
module Specs
- include ::Tooling::Danger::Suggestor
-
SPEC_FILES_REGEX = 'spec/'
EE_PREFIX = 'ee/'
- PROJECT_FACTORIES = %w[
- :project
- :project_empty_repo
- :forked_project_with_submodules
- :project_with_design
+ SUGGESTIONS = [
+ FeatureCategorySuggestion,
+ MatchWithArraySuggestion,
+ ProjectFactorySuggestion
].freeze
- PROJECT_FACTORY_REGEX = /
- ^\+? # Start of the line, which may or may not have a `+`
- (?<head>\s*) # 0-many leading whitespace captured in a group named head
- let!? # Literal `let` which may or may not end in `!`
- (?<tail> # capture group named tail
- \([^)]+\) # Two parenthesis with any non-parenthesis characters between them
- \s*\{\s* # Opening curly brace surrounded by 0-many whitespace characters
- create\( # literal
- (?:#{PROJECT_FACTORIES.join('|')}) # Any of the project factory names
- \W # Non-word character, avoid matching factories like :project_authorization
- ) # end capture group named tail
- /x.freeze
-
- PROJECT_FACTORY_REPLACEMENT = '\k<head>let_it_be\k<tail>'
- PROJECT_FACTORY_SUGGESTION = <<~SUGGEST_COMMENT
- Project creations are very slow. Use `let_it_be`, `build` or `build_stubbed` if possible.
- See [testing best practices](https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#optimize-factory-usage)
- for background information and alternative options.
- SUGGEST_COMMENT
-
- MATCH_WITH_ARRAY_REGEX = /(?<to>to\(?\s*)(?<matcher>match|eq)(?<expectation>[( ]?\[(?=.*,)[^\]]+)/.freeze
- MATCH_WITH_ARRAY_REPLACEMENT = '\k<to>match_array\k<expectation>'
- MATCH_WITH_ARRAY_SUGGESTION = <<~SUGGEST_COMMENT
- If order of the result is not important, please consider using `match_array` to avoid flakiness.
- SUGGEST_COMMENT
-
- RSPEC_TOP_LEVEL_DESCRIBE_REGEX = /^\+.?RSpec\.describe(.+)/.freeze
- FEATURE_CATEGORY_SUGGESTION = <<~SUGGESTION_MARKDOWN
- Consider adding `feature_category: <feature_category_name>` for this example if it is not set already.
- See [testing best practices](https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#feature-category-metadata).
- SUGGESTION_MARKDOWN
- FEATURE_CATEGORY_KEYWORD = 'feature_category'
-
def changed_specs_files(ee: :include)
changed_files = helper.all_changed_files
folder_prefix =
@@ -65,45 +29,9 @@ module Tooling
changed_files.grep(%r{\A#{folder_prefix}#{SPEC_FILES_REGEX}})
end
- def add_suggestions_for_match_with_array(filename)
- add_suggestion(
- filename: filename,
- regex: MATCH_WITH_ARRAY_REGEX,
- replacement: MATCH_WITH_ARRAY_REPLACEMENT,
- comment_text: MATCH_WITH_ARRAY_SUGGESTION
- )
- end
-
- def add_suggestions_for_project_factory_usage(filename)
- add_suggestion(
- filename: filename,
- regex: PROJECT_FACTORY_REGEX,
- replacement: PROJECT_FACTORY_REPLACEMENT,
- comment_text: PROJECT_FACTORY_SUGGESTION
- )
- end
-
- def add_suggestions_for_feature_category(filename)
- file_lines = project_helper.file_lines(filename)
- changed_lines = helper.changed_lines(filename)
-
- changed_lines.each_with_index do |changed_line, i|
- next unless changed_line =~ RSPEC_TOP_LEVEL_DESCRIBE_REGEX
-
- line_number = file_lines.find_index(changed_line.delete_prefix('+'))
- next unless line_number
-
- # Get the top level RSpec.describe line and the next 5 lines
- lines_to_check = file_lines[line_number, 5]
- # Remove all the lines after the first one that ends in `do`
- last_line_number_of_describe_declaration = lines_to_check.index { |line| line.end_with?(' do') }
- lines_to_check = lines_to_check[0..last_line_number_of_describe_declaration]
-
- next if lines_to_check.any? { |line| line.include?(FEATURE_CATEGORY_KEYWORD) }
-
- suggested_line = file_lines[line_number]
-
- markdown(comment(FEATURE_CATEGORY_SUGGESTION, suggested_line), file: filename, line: line_number.succ)
+ def add_suggestions_for(filename)
+ SUGGESTIONS.each do |suggestion|
+ suggestion.new(filename, context: self).suggest
end
end
end
diff --git a/tooling/danger/specs/feature_category_suggestion.rb b/tooling/danger/specs/feature_category_suggestion.rb
new file mode 100644
index 00000000000..5acf73c8956
--- /dev/null
+++ b/tooling/danger/specs/feature_category_suggestion.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require_relative '../suggestion'
+
+module Tooling
+ module Danger
+ module Specs
+ class FeatureCategorySuggestion < Suggestion
+ RSPEC_TOP_LEVEL_DESCRIBE_REGEX = /^\+.?RSpec\.describe(.+)/
+ SUGGESTION = <<~SUGGESTION_MARKDOWN
+ Consider adding `feature_category: <feature_category_name>` for this example if it is not set already.
+ See [testing best practices](https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#feature-category-metadata).
+ SUGGESTION_MARKDOWN
+ FEATURE_CATEGORY_KEYWORD = 'feature_category'
+
+ def suggest
+ file_lines = project_helper.file_lines(filename)
+ changed_lines = helper.changed_lines(filename)
+
+ changed_lines.each do |changed_line|
+ next unless changed_line =~ RSPEC_TOP_LEVEL_DESCRIBE_REGEX
+
+ line_number = file_lines.find_index(changed_line.delete_prefix('+'))
+ next unless line_number
+
+ # Get the top level RSpec.describe line and the next 5 lines
+ lines_to_check = file_lines[line_number, 5]
+ # Remove all the lines after the first one that ends in `do`
+ last_line_number_of_describe_declaration = lines_to_check.index { |line| line.end_with?(' do') }
+ lines_to_check = lines_to_check[0..last_line_number_of_describe_declaration]
+
+ next if lines_to_check.any? { |line| line.include?(FEATURE_CATEGORY_KEYWORD) }
+
+ suggested_line = file_lines[line_number]
+
+ markdown(comment(SUGGESTION, suggested_line), file: filename, line: line_number.succ)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/tooling/danger/specs/match_with_array_suggestion.rb b/tooling/danger/specs/match_with_array_suggestion.rb
new file mode 100644
index 00000000000..eb0f7a1a832
--- /dev/null
+++ b/tooling/danger/specs/match_with_array_suggestion.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative '../suggestion'
+
+module Tooling
+ module Danger
+ module Specs
+ class MatchWithArraySuggestion < Suggestion
+ MATCH = /(?<to>to\(?\s*)(?<matcher>match|eq)(?<expectation>[( ]?\[(?=.*,)[^\]]+)/
+ REPLACEMENT = '\k<to>match_array\k<expectation>'
+ SUGGESTION = <<~SUGGEST_COMMENT
+ If order of the result is not important, please consider using `match_array` to avoid flakiness.
+ SUGGEST_COMMENT
+ end
+ end
+ end
+end
diff --git a/tooling/danger/specs/project_factory_suggestion.rb b/tooling/danger/specs/project_factory_suggestion.rb
new file mode 100644
index 00000000000..4e5a70ac8e5
--- /dev/null
+++ b/tooling/danger/specs/project_factory_suggestion.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require_relative '../suggestion'
+
+module Tooling
+ module Danger
+ module Specs
+ class ProjectFactorySuggestion < Suggestion
+ PROJECT_FACTORIES = %w[
+ :project
+ :project_empty_repo
+ :forked_project_with_submodules
+ :project_with_design
+ ].freeze
+
+ MATCH = /
+ ^\+? # Start of the line, which may or may not have a `+`
+ (?<head>\s*) # 0-many leading whitespace captured in a group named head
+ let!? # Literal `let` which may or may not end in `!`
+ (?<tail> # capture group named tail
+ \([^)]+\) # Two parenthesis with any non-parenthesis characters between them
+ \s*\{\s* # Opening curly brace surrounded by 0-many whitespace characters
+ create\( # literal
+ (?:#{PROJECT_FACTORIES.join('|')}) # Any of the project factory names
+ \W # Non-word character, avoid matching factories like :project_badge
+ ) # end capture group named tail
+ /x
+
+ REPLACEMENT = '\k<head>let_it_be\k<tail>'
+ SUGGESTION = <<~SUGGEST_COMMENT
+ Project creations are very slow. Use `let_it_be`, `build` or `build_stubbed` if possible.
+ See [testing best practices](https://docs.gitlab.com/ee/development/testing_guide/best_practices.html#optimize-factory-usage)
+ for background information and alternative options.
+ SUGGEST_COMMENT
+ end
+ end
+ end
+end
diff --git a/tooling/danger/suggestion.rb b/tooling/danger/suggestion.rb
new file mode 100644
index 00000000000..da3c6b0e76f
--- /dev/null
+++ b/tooling/danger/suggestion.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'forwardable'
+require_relative 'suggestor'
+
+module Tooling
+ module Danger
+ # A basic suggestion.
+ #
+ # A subclass needs to define the following constants:
+ # * MATCH (Regexp) - A Regexp to match file lines
+ # * REPLACEMENT (String) - A suggestion replacement text
+ # * SUGGESTION (String) - A suggestion text
+ #
+ # @see Suggestor
+ class Suggestion
+ extend Forwardable
+ include ::Tooling::Danger::Suggestor
+
+ def_delegators :@context, :helper, :project_helper, :markdown
+
+ attr_reader :filename
+
+ def initialize(filename, context:)
+ @filename = filename
+ @context = context
+ end
+
+ def suggest
+ add_suggestion(
+ filename: filename,
+ regex: self.class::MATCH,
+ replacement: self.class::REPLACEMENT,
+ comment_text: self.class::SUGGESTION
+ )
+ end
+ end
+ end
+end