diff --git a/.eslintignore b/.eslintignore
deleted file mode 100644
index 5428964b1ce..00000000000
--- a/.eslintignore
+++ /dev/null
@@ -1,13 +0,0 @@
-/app/assets/javascripts/locale/**/app.js
-/builds/
-/coverage/
-/coverage-frontend/
-/node_modules/
-/public/
-/tmp/
-/vendor/
-/sitespeed-result/
-/fixtures/**/*.graphql
-# Storybook build artifacts
-/storybook/public
-spec/fixtures/**/*.graphql
diff --git a/.eslintrc.yml b/.eslintrc.yml
deleted file mode 100644
index 8a6f53d7b5a..00000000000
--- a/.eslintrc.yml
+++ /dev/null
@@ -1,318 +0,0 @@
-extends:
- - plugin:@gitlab/default
- - plugin:@gitlab/i18n
- - plugin:no-jquery/slim
- - plugin:no-jquery/deprecated-3.4
- - plugin:no-unsanitized/recommended-legacy
- - ./tooling/eslint-config/conditionally_ignore.js
-globals:
- __webpack_public_path__: true
- gl: false
- gon: false
- localStorage: false
- IS_EE: false
-plugins:
- - no-jquery
- - local-rules
-settings:
- import/resolver:
- webpack:
- config: './config/webpack.config.js'
-rules:
- import/no-commonjs: error
- import/no-default-export: off
- no-underscore-dangle:
- - error
- - allow:
- - __
- - _links
- import/no-unresolved:
- - error
- - ignore:
- # In FOSS, these import paths are rewritten using
- # NormalModuleReplacementPlugin, which import/no-unresolved doesn't
- # consider. See
- # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89831.
- - '^(ee|jh)_component/'
- lines-between-class-members: off
- # all offenses of no-jquery/no-animate-toggle are false positives ( $toast.show() )
- no-jquery/no-animate-toggle: off
- no-jquery/no-event-shorthand: off
- no-jquery/no-serialize: error
- promise/always-return: off
- promise/no-callback-in-promise: off
- '@gitlab/no-global-event-off': error
- '@gitlab/vue-no-new-non-primitive-in-template':
- - error
- - allowNames:
- - 'class(es)?$'
- - '^style$'
- - '^to$'
- - '^$'
- - '^variables$'
- - 'attrs?$'
- '@gitlab/vue-no-undef-apollo-properties': error
- '@gitlab/tailwind-no-interpolation': error
- '@gitlab/vue-tailwind-no-interpolation': error
- no-param-reassign:
- - error
- - props: true
- ignorePropertyModificationsFor:
- - acc
- - accumulator
- - el
- - element
- - state
- ignorePropertyModificationsForRegex:
- - '^draft'
- import/order:
- - error
- - groups:
- - builtin
- - external
- - internal
- - parent
- - sibling
- - index
- pathGroups:
- - pattern: ~/**
- group: internal
- - pattern: emojis/**
- group: internal
- - pattern: '{ee_,jh_,}empty_states/**'
- group: internal
- - pattern: '{ee_,jh_,}icons/**'
- group: internal
- - pattern: '{ee_,jh_,}images/**'
- group: internal
- - pattern: vendor/**
- group: internal
- - pattern: shared_queries/**
- group: internal
- - pattern: '{ee_,}spec/**'
- group: internal
- - pattern: '{ee_,jh_,}jest/**'
- group: internal
- - pattern: '{ee_,jh_,any_}else_ce/**'
- group: internal
- - pattern: ee/**
- group: internal
- - pattern: '{ee_,jh_,}component/**'
- group: internal
- - pattern: jh_else_ee/**
- group: internal
- - pattern: jh/**
- group: internal
- - pattern: '{test_,}helpers/**'
- group: internal
- - pattern: test_fixtures/**
- group: internal
- alphabetize:
- order: ignore
- 'no-restricted-syntax':
- - error
- - selector: ImportSpecifier[imported.name='GlSkeletonLoading']
- message: 'Migrate to GlSkeletonLoader, or import GlDeprecatedSkeletonLoading.'
- - selector: ImportSpecifier[imported.name='GlSafeHtmlDirective']
- message: 'Use directive at ~/vue_shared/directives/safe_html.js instead.'
- - selector: Literal[value=/docs.gitlab.+\u002Fee/]
- message: 'No hard coded url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`'
- - selector: TemplateElement[value.cooked=/docs.gitlab.+\u002Fee/]
- message: 'No hard coded url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`'
- - selector: Literal[value=/(?=.*docs.gitlab.*)(?!.*\u002Fee\b.*)/]
- message: 'No hard coded url, use `DOCS_URL` in `jh_else_ce/lib/utils/url_utility`'
- - selector: TemplateElement[value.cooked=/(?=.*docs.gitlab.*)(?!.*\u002Fee\b.*)/]
- message: 'No hard coded url, use `DOCS_URL` in `jh_else_ce/lib/utils/url_utility`'
- - selector: Literal[value=/(?=.*about.gitlab.*)(?!.*\u002Fblog\b.*)/]
- message: 'No hard coded url, use `PROMO_URL` in `jh_else_ce/lib/utils/url_utility`'
- - selector: TemplateElement[value.cooked=/(?=.*about.gitlab.*)(?!.*\u002Fblog\b.*)/]
- message: 'No hard coded url, use `PROMO_URL` in `jh_else_ce/lib/utils/url_utility`'
- - selector: TemplateLiteral[expressions.0.name=DOCS_URL] > TemplateElement[value.cooked=/\u002Fjh|\u002Fee/]
- message: '`/ee` or `/jh` path found in docs url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`'
- # This can be removed once GitLab is on Vue 3
- - selector: MemberExpression[object.type='ThisExpression'][property.name=/(\$delete|\$set)/]
- message: "Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead."
- no-restricted-properties:
- - error
- - object: window
- property: open
- message: 'Use `visitUrl` in `jh_else_ce/lib/utils/url_utility` to avoid cross-site leaks.'
- # This can be removed once GitLab is on Vue 3
- - object: vm
- property: $delete
- message: "Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead."
- # This can be removed once GitLab is on Vue 3
- - object: Vue
- property: delete
- message: "Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead."
- # This can be removed once GitLab is on Vue 3
- - object: vm
- property: $set
- message: "Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead."
- # This can be removed once GitLab is on Vue 3
- - object: Vue
- property: set
- message: "Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead."
- no-restricted-imports:
- - error
- - paths:
- - name: mousetrap
- message: 'Import { Mousetrap } from ~/lib/mousetrap instead.'
- - name: vuex
- message: 'See our documentation on "Migrating from VueX" for tips on how to avoid adding new VueX stores.'
- - name: '@sentry/browser'
- message: Use "import * as Sentry from '~/sentry/sentry_browser_wrapper';" instead
- patterns:
- - group: ['react', 'react-dom/*']
- message: We do not allow usage of React in our codebase except for the graphql_explorer
- unicorn/prefer-dom-node-dataset:
- - error
- no-unsanitized/method:
- - error
- - escape:
- methods: ['sanitize']
- no-unsanitized/property:
- - error
- - escape:
- methods: ['sanitize']
- # This rule will be enabled later.
- unicorn/no-array-callback-reference: off
- vue/no-undef-components:
- - error
- - ignorePatterns:
- - '^router-link$'
- - '^router-view$'
- - '^gl-emoji$'
- local-rules/require-valid-help-page-path: 'error'
- local-rules/vue-require-valid-help-page-link-component: 'error'
-overrides:
- - files:
- - '{,ee/,jh/}spec/frontend*/**/*'
- rules:
- '@gitlab/require-i18n-strings': off
- '@gitlab/no-runtime-template-compiler': off
- '@gitlab/tailwind-no-interpolation': off
- '@gitlab/vue-tailwind-no-interpolation': off
- 'require-await': error
- 'import/no-dynamic-require': off
- 'no-import-assign': off
- 'no-restricted-syntax':
- - error
- - selector: CallExpression[callee.object.name=/(wrapper|vm)/][callee.property.name="setData"]
- message: 'Avoid using "setData" on VTU wrapper'
- - selector: MemberExpression[object.type!='ThisExpression'][property.type='Identifier'][property.name='$nextTick']
- message: 'Using $nextTick from a component instance is discouraged. Import nextTick directly from the Vue package.'
- - selector: Identifier[name='setImmediate']
- message: 'Prefer explicit waitForPromises (or equivalent), or jest.runAllTimers (or equivalent) to vague setImmediate calls.'
- - selector: ImportSpecifier[imported.name='GlSkeletonLoading']
- message: 'Migrate to GlSkeletonLoader, or import GlDeprecatedSkeletonLoading.'
- - selector: CallExpression[arguments.length=1][arguments.0.type='Literal'] CallExpression[callee.property.name='toBe'] CallExpression[callee.property.name='attributes'][arguments.length=1][arguments.0.value='disabled']
- message: Avoid asserting disabled attribute exact value, because Vue.js 2 and Vue.js 3 renders it differently. Use toBeDefined / toBeUndefined instead
- - selector: MemberExpression[object.object.name='Vue'][object.property.name='config'][property.name='errorHandler']
- message: 'Use setErrorHandler/resetVueErrorHandler from helpers/set_vue_error_handler.js instead.'
- - selector: Literal[value=/docs.gitlab.+\u002Fee/]
- message: 'No hard coded url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`'
- - selector: TemplateElement[value.cooked=/docs.gitlab.+\u002Fee/]
- message: 'No hard coded url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`'
- - selector: Literal[value=/(?=.*docs.gitlab.*)(?!.*\u002Fee\b.*)/]
- message: 'No hard coded url, use `DOCS_URL` in `jh_else_ce/lib/utils/url_utility`'
- - selector: TemplateElement[value.cooked=/(?=.*docs.gitlab.*)(?!.*\u002Fee\b.*)/]
- message: 'No hard coded url, use `DOCS_URL` in `jh_else_ce/lib/utils/url_utility`'
- - selector: Literal[value=/(?=.*about.gitlab.*)(?!.*\u002Fblog\b.*)/]
- message: 'No hard coded url, use `PROMO_URL` in `jh_else_ce/lib/utils/url_utility`'
- - selector: TemplateElement[value.cooked=/(?=.*about.gitlab.*)(?!.*\u002Fblog\b.*)/]
- message: 'No hard coded url, use `PROMO_URL` in `jh_else_ce/lib/utils/url_utility`'
- - selector: TemplateLiteral[expressions.0.name=DOCS_URL] > TemplateElement[value.cooked=/\u002Fjh|\u002Fee/]
- message: '`/ee` or `/jh` path found in docs url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`'
- # This can be removed once GitLab is on Vue 3
- - selector: CallExpression[callee.property.name=/(\$delete|\$set)/]
- message: "Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead."
- no-restricted-properties:
- - error
- # This can be removed once GitLab is on Vue 3
- - object: Vue
- property: delete
- message: "Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead."
- # This can be removed once GitLab is on Vue 3
- - object: Vue
- property: set
- message: "Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead."
- no-unsanitized/method: off
- no-unsanitized/property: off
- local-rules/require-valid-help-page-path: off
- local-rules/vue-require-valid-help-page-link-component: off
- no-restricted-imports:
- - error
- - paths:
- - name: mousetrap
- message: 'Import { Mousetrap } from ~/lib/mousetrap instead.'
- - name: vuex
- message: 'See our documentation on "Migrating from VueX" for tips on how to avoid adding new VueX stores.'
- - name: '@sentry/browser'
- message: Use "import * as Sentry from '~/sentry/sentry_browser_wrapper';" instead
- - name: ~/locale
- importNames:
- - __
- - s__
- message: 'Do not externalize strings in specs: https://docs.gitlab.com/ee/development/i18n/externalization.html#test-files-jest'
- - files:
- - 'config/**/*'
- - 'scripts/**/*'
- - '*.config.js'
- - '*.config.*.js'
- - 'jest_resolver.js'
- rules:
- '@gitlab/require-i18n-strings': off
- import/no-extraneous-dependencies: off
- import/no-commonjs: off
- import/no-nodejs-modules: off
- filenames/match-regex: off
- no-console: off
- - files:
- - '*.stories.js'
- rules:
- filenames/match-regex: off
- '@gitlab/require-i18n-strings': off
- - files:
- - '*.graphql'
- plugins:
- - '@graphql-eslint'
- parserOptions:
- parser: '@graphql-eslint/eslint-plugin'
- operations: '{,ee/,jh/}app/**/*.graphql'
- schema: './tmp/tests/graphql/gitlab_schema_apollo.graphql'
- rules:
- filenames/match-regex: off
- spaced-comment: off
- # TODO: We need a way to include this rule + support ee_else_ce fragments
- #'@graphql-eslint/unique-fragment-name': error
- # TODO: Uncomment these rules when then `schema` is available
- #'@graphql-eslint/fragments-on-composite-type': error
- #'@graphql-eslint/known-argument-names': error
- #'@graphql-eslint/known-type-names': error
- '@graphql-eslint/no-anonymous-operations': error
- '@graphql-eslint/unique-operation-name': error
- '@graphql-eslint/require-id-when-available': error
- '@graphql-eslint/no-unused-variables': error
- '@graphql-eslint/no-unused-fragments': error
- '@graphql-eslint/no-duplicate-fields': error
- - files:
- - '{,ee/}spec/contracts/consumer/**/*'
- rules:
- '@gitlab/require-i18n-strings': off
- - files:
- - 'app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql'
- - 'app/assets/javascripts/projects/settings/repository/branch_rules/graphql/mutations/create_branch_rule.mutation.graphql'
- - 'app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql'
- - 'ee/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql'
- - 'ee/app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql'
- rules:
- '@graphql-eslint/require-id-when-available': off
- - files:
- - '{,spec/}tooling/**/*'
- rules:
- 'no-undef': off
- 'import/no-commonjs': off
- 'import/no-extraneous-dependencies': off
- 'no-restricted-syntax': off
- '@gitlab/require-i18n-strings': off
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 729f5967193..9f779e6bbeb 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -473,7 +473,8 @@
# Code patterns + .ci-patterns
.code-patterns: &code-patterns
- - ".{eslintrc.yml,eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
+ - ".{gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
+ - "eslint.config.mjs"
- ".browserslistrc"
- ".stylelintrc"
- "{,ee/,jh/}{app,bin,config,db,elastic,generator_templates,gems,haml_lint,lib,locale,public,scripts,sidekiq_cluster,storybook,symbol,vendor}/**/*"
@@ -501,7 +502,8 @@
# .code-patterns + .backstage-patterns
.code-backstage-patterns: &code-backstage-patterns
- - ".{eslintrc.yml,eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
+ - ".{gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
+ - "eslint.config.mjs"
- ".browserslistrc"
- ".stylelintrc"
- "{,ee/,jh/}{app,bin,config,db,elastic,generator_templates,gems,haml_lint,lib,locale,public,scripts,sidekiq_cluster,storybook,symbol,vendor}/**/*"
@@ -536,7 +538,8 @@
# .code-patterns + .qa-patterns
.code-qa-patterns: &code-qa-patterns
- - ".{eslintrc.yml,eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
+ - ".{gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
+ - "eslint.config.mjs"
- ".browserslistrc"
- ".stylelintrc"
- "{,ee/,jh/}{app,bin,config,db,elastic,generator_templates,gems,haml_lint,lib,locale,public,scripts,sidekiq_cluster,storybook,symbol,vendor}/**/*"
@@ -566,7 +569,8 @@
# .code-patterns + .backstage-patterns + .qa-patterns
.code-backstage-qa-patterns: &code-backstage-qa-patterns
- - ".{eslintrc.yml,eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
+ - ".{gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
+ - "eslint.config.mjs"
- ".browserslistrc"
- ".stylelintrc"
- "{,ee/,jh/}{app,bin,config,db,elastic,generator_templates,gems,haml_lint,lib,locale,public,scripts,sidekiq_cluster,storybook,symbol,vendor}/**/*"
@@ -614,7 +618,8 @@
- ".stylelintrc"
- "Dockerfile.assets"
- "vendor/assets/**/*"
- - ".{eslintrc.yml,eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
+ - ".{gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
+ - "eslint.config.mjs"
- "*_VERSION"
- "{,jh/}Gemfile{,.lock}"
- "{,jh/}Gemfile.next{,.lock}"
diff --git a/.rubocop_todo/lint/void.yml b/.rubocop_todo/lint/void.yml
index c1b6f44f369..afb43b7e250 100644
--- a/.rubocop_todo/lint/void.yml
+++ b/.rubocop_todo/lint/void.yml
@@ -3,5 +3,4 @@
Lint/Void:
Details: grace period
Exclude:
- - 'ee/lib/gitlab/llm/ai_message.rb'
- 'spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb'
diff --git a/.rubocop_todo/rails/inverse_of.yml b/.rubocop_todo/rails/inverse_of.yml
index fffabfb92f1..c8dbb9b7a0e 100644
--- a/.rubocop_todo/rails/inverse_of.yml
+++ b/.rubocop_todo/rails/inverse_of.yml
@@ -61,7 +61,6 @@ Rails/InverseOf:
- 'ee/app/models/ee/service_desk_setting.rb'
- 'ee/app/models/ee/user.rb'
- 'ee/app/models/elastic/reindexing_subtask.rb'
- - 'ee/app/models/elastic/reindexing_task.rb'
- 'ee/app/models/geo/event.rb'
- 'ee/app/models/geo/event_log.rb'
- 'ee/app/models/geo/job_artifact_registry.rb'
diff --git a/Gemfile b/Gemfile
index 7785a13d7b9..647630e9471 100644
--- a/Gemfile
+++ b/Gemfile
@@ -387,9 +387,9 @@ gem 'gitlab-license', '~> 2.5', feature_category: :shared
gem 'rack-attack', '~> 6.7.0' # rubocop:todo Gemfile/MissingFeatureCategory
# Sentry integration
-gem 'sentry-ruby', '~> 5.19.0', feature_category: :observability
-gem 'sentry-rails', '~> 5.19.0', feature_category: :observability
-gem 'sentry-sidekiq', '~> 5.19.0', feature_category: :observability
+gem 'sentry-ruby', '~> 5.21.0', feature_category: :observability
+gem 'sentry-rails', '~> 5.21.0', feature_category: :observability
+gem 'sentry-sidekiq', '~> 5.21.0', feature_category: :observability
# PostgreSQL query parsing
#
@@ -582,7 +582,7 @@ group :test do
gem 'shoulda-matchers', '~> 5.1.0', require: false # rubocop:todo Gemfile/MissingFeatureCategory
gem 'email_spec', '~> 2.2.0' # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'webmock', '~> 3.23.0', feature_category: :shared
+ gem 'webmock', '~> 3.24.0', feature_category: :shared
gem 'rails-controller-testing' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'concurrent-ruby', '~> 1.1' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'test-prof', '~> 1.4.0', feature_category: :tooling
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 252c2237df6..00b830e04be 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -661,11 +661,11 @@
{"name":"sawyer","version":"0.9.2","platform":"ruby","checksum":"fa3a72d62a4525517b18857ddb78926aab3424de0129be6772a8e2ba240e7aca"},
{"name":"sd_notify","version":"0.1.1","platform":"ruby","checksum":"cbc7ac6caa7cedd26b30a72b5eeb6f36050dc0752df263452ea24fb5a4ad3131"},
{"name":"seed-fu","version":"2.3.7","platform":"ruby","checksum":"f19673443e9af799b730e3d4eca6a89b39e5a36825015dffd00d02ea3365cf74"},
-{"name":"selenium-webdriver","version":"4.23.0","platform":"ruby","checksum":"490aeddee879cfea58a4db6628338d60a905bc56cd5e1a60dfbaa9090a19b801"},
+{"name":"selenium-webdriver","version":"4.25.0","platform":"ruby","checksum":"7e11abf2b0fd56df61d98b6d59d621781cf103261d941df3510837547bd4a0d5"},
{"name":"semver_dialects","version":"3.4.3","platform":"ruby","checksum":"ae3ea99f7806693ab031df3121017c102f1a35f4fc2524674055cb446fb9cc82"},
-{"name":"sentry-rails","version":"5.19.0","platform":"ruby","checksum":"d4ad5323feea8e876f9feb2f50b126a3be3b4f6e137d37c360c31d52b6861995"},
-{"name":"sentry-ruby","version":"5.19.0","platform":"ruby","checksum":"0ddf89f246840a5c50df6c68b8eb59ad23ee4adb4a91187a414bb196cee1838b"},
-{"name":"sentry-sidekiq","version":"5.19.0","platform":"ruby","checksum":"1b16ec4b15b35dcbdd182494d612aae7ec5c923a9ed6814aed1b56103feecb80"},
+{"name":"sentry-rails","version":"5.21.0","platform":"ruby","checksum":"b5a943d199aff0d3cb94dbac4eb3e00622dd0c55fd1be0cffd43a7e09f0ad602"},
+{"name":"sentry-ruby","version":"5.21.0","platform":"ruby","checksum":"294e0dd59afce7e08ba22a4e880924345c75c3e858dc8ee23553716793f78629"},
+{"name":"sentry-sidekiq","version":"5.21.0","platform":"ruby","checksum":"6df54ec79238f69d9d4b7647bcd2a192a4702f3a39edffd63a455203430e90e2"},
{"name":"shellany","version":"0.0.1","platform":"ruby","checksum":"0e127a9132698766d7e752e82cdac8250b6adbd09e6c0a7fbbb6f61964fedee7"},
{"name":"shoulda-matchers","version":"5.1.0","platform":"ruby","checksum":"a01d20589989e9653ab4a28c67d9db2b82bcf0a2496cf01d5e1a95a4aaaf5b07"},
{"name":"sidekiq-cron","version":"1.12.0","platform":"ruby","checksum":"6663080a454088bd88773a0da3ae91e554b8a2e8b06cfc629529a83fd1a3096c"},
@@ -767,7 +767,7 @@
{"name":"warning","version":"1.3.0","platform":"ruby","checksum":"23695a5d8e50bd5c46068931b529bee0b28e4982cbcefbe77d867800dde8069e"},
{"name":"webauthn","version":"3.0.0","platform":"ruby","checksum":"3f77d422c2a8a4b31e56cf42f83414bd066e0506e9896936e1730262dc4a20e6"},
{"name":"webfinger","version":"2.1.3","platform":"ruby","checksum":"567a52bde77fb38ca6b67e55db755f988766ec4651c1d24916a65aa70540695c"},
-{"name":"webmock","version":"3.23.1","platform":"ruby","checksum":"0fa738c0767d1c4ec8cc57f6b21998f0c238c8a5b32450df1c847f2767140d95"},
+{"name":"webmock","version":"3.24.0","platform":"ruby","checksum":"be01357f6fc773606337ca79f3ba332b7d52cbe5c27587671abc0572dbec7122"},
{"name":"webrick","version":"1.8.1","platform":"ruby","checksum":"19411ec6912911fd3df13559110127ea2badd0c035f7762873f58afc803e158f"},
{"name":"websocket","version":"1.2.10","platform":"ruby","checksum":"2cc1a4a79b6e63637b326b4273e46adcddf7871caa5dc5711f2ca4061a629fa8"},
{"name":"websocket-driver","version":"0.7.6","platform":"java","checksum":"bc894b9e9d5aee55ac04b61003e1957c4ef411a5a048199587d0499785b505c3"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 0ee6e89309d..4bca14c6622 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1694,7 +1694,7 @@ GEM
seed-fu (2.3.7)
activerecord (>= 3.1)
activesupport (>= 3.1)
- selenium-webdriver (4.23.0)
+ selenium-webdriver (4.25.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
@@ -1705,14 +1705,14 @@ GEM
pastel (~> 0.8.0)
thor (~> 1.3)
tty-command (~> 0.10.1)
- sentry-rails (5.19.0)
+ sentry-rails (5.21.0)
railties (>= 5.0)
- sentry-ruby (~> 5.19.0)
- sentry-ruby (5.19.0)
+ sentry-ruby (~> 5.21.0)
+ sentry-ruby (5.21.0)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
- sentry-sidekiq (5.19.0)
- sentry-ruby (~> 5.19.0)
+ sentry-sidekiq (5.21.0)
+ sentry-ruby (~> 5.21.0)
sidekiq (>= 3.0)
shellany (0.0.1)
shoulda-matchers (5.1.0)
@@ -1930,7 +1930,7 @@ GEM
activesupport
faraday (~> 2.0)
faraday-follow_redirects
- webmock (3.23.1)
+ webmock (3.24.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@@ -2272,9 +2272,9 @@ DEPENDENCIES
seed-fu (~> 2.3.7)
selenium-webdriver (~> 4.21, >= 4.21.1)
semver_dialects (~> 3.0)
- sentry-rails (~> 5.19.0)
- sentry-ruby (~> 5.19.0)
- sentry-sidekiq (~> 5.19.0)
+ sentry-rails (~> 5.21.0)
+ sentry-ruby (~> 5.21.0)
+ sentry-sidekiq (~> 5.21.0)
shoulda-matchers (~> 5.1.0)
sidekiq!
sidekiq-cron (~> 1.12.0)
@@ -2319,7 +2319,7 @@ DEPENDENCIES
vmstat (~> 2.3.0)
warning (~> 1.3.0)
webauthn (~> 3.0)
- webmock (~> 3.23.0)
+ webmock (~> 3.24.0)
webrick (~> 1.8.1)
wikicloth (= 0.8.1)
yajl-ruby (~> 1.4.3)
diff --git a/Gemfile.next.checksum b/Gemfile.next.checksum
index e2e2e1f60ae..98ce22d65b0 100644
--- a/Gemfile.next.checksum
+++ b/Gemfile.next.checksum
@@ -674,11 +674,11 @@
{"name":"sawyer","version":"0.9.2","platform":"ruby","checksum":"fa3a72d62a4525517b18857ddb78926aab3424de0129be6772a8e2ba240e7aca"},
{"name":"sd_notify","version":"0.1.1","platform":"ruby","checksum":"cbc7ac6caa7cedd26b30a72b5eeb6f36050dc0752df263452ea24fb5a4ad3131"},
{"name":"seed-fu","version":"2.3.7","platform":"ruby","checksum":"f19673443e9af799b730e3d4eca6a89b39e5a36825015dffd00d02ea3365cf74"},
-{"name":"selenium-webdriver","version":"4.23.0","platform":"ruby","checksum":"490aeddee879cfea58a4db6628338d60a905bc56cd5e1a60dfbaa9090a19b801"},
+{"name":"selenium-webdriver","version":"4.25.0","platform":"ruby","checksum":"7e11abf2b0fd56df61d98b6d59d621781cf103261d941df3510837547bd4a0d5"},
{"name":"semver_dialects","version":"3.4.3","platform":"ruby","checksum":"ae3ea99f7806693ab031df3121017c102f1a35f4fc2524674055cb446fb9cc82"},
-{"name":"sentry-rails","version":"5.19.0","platform":"ruby","checksum":"d4ad5323feea8e876f9feb2f50b126a3be3b4f6e137d37c360c31d52b6861995"},
-{"name":"sentry-ruby","version":"5.19.0","platform":"ruby","checksum":"0ddf89f246840a5c50df6c68b8eb59ad23ee4adb4a91187a414bb196cee1838b"},
-{"name":"sentry-sidekiq","version":"5.19.0","platform":"ruby","checksum":"1b16ec4b15b35dcbdd182494d612aae7ec5c923a9ed6814aed1b56103feecb80"},
+{"name":"sentry-rails","version":"5.21.0","platform":"ruby","checksum":"b5a943d199aff0d3cb94dbac4eb3e00622dd0c55fd1be0cffd43a7e09f0ad602"},
+{"name":"sentry-ruby","version":"5.21.0","platform":"ruby","checksum":"294e0dd59afce7e08ba22a4e880924345c75c3e858dc8ee23553716793f78629"},
+{"name":"sentry-sidekiq","version":"5.21.0","platform":"ruby","checksum":"6df54ec79238f69d9d4b7647bcd2a192a4702f3a39edffd63a455203430e90e2"},
{"name":"shellany","version":"0.0.1","platform":"ruby","checksum":"0e127a9132698766d7e752e82cdac8250b6adbd09e6c0a7fbbb6f61964fedee7"},
{"name":"shoulda-matchers","version":"5.1.0","platform":"ruby","checksum":"a01d20589989e9653ab4a28c67d9db2b82bcf0a2496cf01d5e1a95a4aaaf5b07"},
{"name":"sidekiq-cron","version":"1.12.0","platform":"ruby","checksum":"6663080a454088bd88773a0da3ae91e554b8a2e8b06cfc629529a83fd1a3096c"},
@@ -782,7 +782,7 @@
{"name":"warning","version":"1.3.0","platform":"ruby","checksum":"23695a5d8e50bd5c46068931b529bee0b28e4982cbcefbe77d867800dde8069e"},
{"name":"webauthn","version":"3.0.0","platform":"ruby","checksum":"3f77d422c2a8a4b31e56cf42f83414bd066e0506e9896936e1730262dc4a20e6"},
{"name":"webfinger","version":"2.1.3","platform":"ruby","checksum":"567a52bde77fb38ca6b67e55db755f988766ec4651c1d24916a65aa70540695c"},
-{"name":"webmock","version":"3.23.1","platform":"ruby","checksum":"0fa738c0767d1c4ec8cc57f6b21998f0c238c8a5b32450df1c847f2767140d95"},
+{"name":"webmock","version":"3.24.0","platform":"ruby","checksum":"be01357f6fc773606337ca79f3ba332b7d52cbe5c27587671abc0572dbec7122"},
{"name":"webrick","version":"1.8.1","platform":"ruby","checksum":"19411ec6912911fd3df13559110127ea2badd0c035f7762873f58afc803e158f"},
{"name":"websocket","version":"1.2.10","platform":"ruby","checksum":"2cc1a4a79b6e63637b326b4273e46adcddf7871caa5dc5711f2ca4061a629fa8"},
{"name":"websocket-driver","version":"0.7.6","platform":"java","checksum":"bc894b9e9d5aee55ac04b61003e1957c4ef411a5a048199587d0499785b505c3"},
diff --git a/Gemfile.next.lock b/Gemfile.next.lock
index 8265a43b5bb..c992b5948da 100644
--- a/Gemfile.next.lock
+++ b/Gemfile.next.lock
@@ -1720,7 +1720,7 @@ GEM
seed-fu (2.3.7)
activerecord (>= 3.1)
activesupport (>= 3.1)
- selenium-webdriver (4.23.0)
+ selenium-webdriver (4.25.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
@@ -1731,14 +1731,14 @@ GEM
pastel (~> 0.8.0)
thor (~> 1.3)
tty-command (~> 0.10.1)
- sentry-rails (5.19.0)
+ sentry-rails (5.21.0)
railties (>= 5.0)
- sentry-ruby (~> 5.19.0)
- sentry-ruby (5.19.0)
+ sentry-ruby (~> 5.21.0)
+ sentry-ruby (5.21.0)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
- sentry-sidekiq (5.19.0)
- sentry-ruby (~> 5.19.0)
+ sentry-sidekiq (5.21.0)
+ sentry-ruby (~> 5.21.0)
sidekiq (>= 3.0)
shellany (0.0.1)
shoulda-matchers (5.1.0)
@@ -1957,7 +1957,7 @@ GEM
activesupport
faraday (~> 2.0)
faraday-follow_redirects
- webmock (3.23.1)
+ webmock (3.24.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@@ -2299,9 +2299,9 @@ DEPENDENCIES
seed-fu (~> 2.3.7)
selenium-webdriver (~> 4.21, >= 4.21.1)
semver_dialects (~> 3.0)
- sentry-rails (~> 5.19.0)
- sentry-ruby (~> 5.19.0)
- sentry-sidekiq (~> 5.19.0)
+ sentry-rails (~> 5.21.0)
+ sentry-ruby (~> 5.21.0)
+ sentry-sidekiq (~> 5.21.0)
shoulda-matchers (~> 5.1.0)
sidekiq!
sidekiq-cron (~> 1.12.0)
@@ -2346,7 +2346,7 @@ DEPENDENCIES
vmstat (~> 2.3.0)
warning (~> 1.3.0)
webauthn (~> 3.0)
- webmock (~> 3.23.0)
+ webmock (~> 3.24.0)
webrick (~> 1.8.1)
wikicloth (= 0.8.1)
yajl-ruby (~> 1.4.3)
diff --git a/app/assets/javascripts/vue_shared/components/chronic_duration_input.vue b/app/assets/javascripts/admin/application_settings/runner_token_expiration/components/chronic_duration_input.vue
similarity index 100%
rename from app/assets/javascripts/vue_shared/components/chronic_duration_input.vue
rename to app/assets/javascripts/admin/application_settings/runner_token_expiration/components/chronic_duration_input.vue
diff --git a/app/assets/javascripts/admin/application_settings/runner_token_expiration/components/expiration_intervals.vue b/app/assets/javascripts/admin/application_settings/runner_token_expiration/components/expiration_intervals.vue
index 371a26d2664..695806a5fa6 100644
--- a/app/assets/javascripts/admin/application_settings/runner_token_expiration/components/expiration_intervals.vue
+++ b/app/assets/javascripts/admin/application_settings/runner_token_expiration/components/expiration_intervals.vue
@@ -1,7 +1,7 @@
+
+
+
+ {{ errorMessageBodyAlert }}
+
+ {{
+ s__('PackageRegistry|Delete this package')
+ }}
+ {{
+ s__('PackageRegistry|Show packages with errors')
+ }}
+
+
+
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
index b93ea37e7dc..3dcaeb66a66 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
@@ -1,11 +1,10 @@
@@ -205,22 +149,11 @@ export default {
-
- {{ errorMessageBodyAlert }}
-
- {{
- $options.i18n.deleteThisPackage
- }}
- {{
- s__('PackageRegistry|Show packages with errors')
- }}
-
-
+
0;
},
@@ -557,7 +554,6 @@ export default {
variant="confirm"
type="submit"
data-testid="wiki-submit-button"
- :disabled="disableSubmitButton"
>{{ submitButtonText }}
-/**
- * Allows to toggle slots based on an array of slot names.
- */
-export default {
- name: 'SlotSwitch',
-
- props: {
- activeSlotNames: {
- type: Array,
- required: true,
- },
-
- tagName: {
- type: String,
- required: false,
- default: 'div',
- },
- },
-
- computed: {
- allSlotNames() {
- // eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots
- return Object.keys(this.$slots);
- },
- },
-};
-
-
-
-
-
-
-
-
-
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
index 88639e6f0c9..a5459f98f0a 100644
--- a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
@@ -10,8 +10,10 @@ import {
GlTooltip,
GlTooltipDirective,
} from '@gitlab/ui';
+import { escapeRegExp } from 'lodash';
import { __, s__, sprintf } from '~/locale';
import { isScopedLabel } from '~/lib/utils/common_utils';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/shared/work_item_link_child_metadata.vue';
import RichTimestampTooltip from '../rich_timestamp_tooltip.vue';
import WorkItemTypeIcon from '../work_item_type_icon.vue';
@@ -22,6 +24,7 @@ import {
WIDGET_TYPE_ASSIGNEES,
WIDGET_TYPE_LABELS,
LINKED_CATEGORIES_MAP,
+ INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION,
} from '../../constants';
import WorkItemRelationshipIcons from './work_item_relationship_icons.vue';
@@ -50,6 +53,14 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [glFeatureFlagMixin()],
+ inject: {
+ preventRouterNav: {
+ from: INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION,
+ default: false,
+ },
+ isGroup: {},
+ },
props: {
childItem: {
type: Object,
@@ -137,11 +148,40 @@ export default {
return item.linkType !== LINKED_CATEGORIES_MAP.RELATES_TO;
});
},
+ issueAsWorkItem() {
+ return (
+ !this.isGroup &&
+ this.glFeatures.workItemsViewPreference &&
+ gon.current_user_use_work_items_view
+ );
+ },
},
methods: {
showScopedLabel(label) {
return isScopedLabel(label) && this.allowsScopedLabels;
},
+ handleTitleClick(e) {
+ const workItem = this.childItem;
+ if (e.metaKey || e.ctrlKey) {
+ return;
+ }
+ const escapedFullPath = escapeRegExp(this.workItemFullPath);
+ // eslint-disable-next-line no-useless-escape
+ const regex = new RegExp(`groups\/${escapedFullPath}\/-\/(work_items|epics)\/\\d+`);
+ const isWorkItemPath = regex.test(workItem.webUrl);
+
+ if (!(isWorkItemPath || this.issueAsWorkItem) || this.preventRouterNav) {
+ this.$emit('click', e);
+ } else {
+ e.preventDefault();
+ this.$router.push({
+ name: 'workItem',
+ params: {
+ iid: workItem.iid,
+ },
+ });
+ }
+ },
},
};
@@ -171,10 +211,10 @@ export default {
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index 1592cb02a2d..037c95a4254 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -864,6 +864,7 @@ export default {
:can-update-children="canUpdateChildren"
:confidential="workItem.confidential"
:allowed-child-types="allowedChildTypes"
+ :is-drawer="isDrawer"
@show-modal="openInModal"
@addChild="$emit('addChild')"
@childrenLoaded="hasChildren = $event"
diff --git a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
index 4cf34a5c6af..1ad6c053a51 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
@@ -3,6 +3,7 @@ import { GlAlert, GlModal } from '@gitlab/ui';
import { s__ } from '~/locale';
import { removeHierarchyChild } from '../graphql/cache_utils';
import deleteWorkItemMutation from '../graphql/delete_work_item.mutation.graphql';
+import { INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION } from '../constants';
export default {
WORK_ITEM_DETAIL_MODAL_ID: 'work-item-detail-modal',
@@ -15,6 +16,9 @@ export default {
GlModal,
WorkItemDetail: () => import('./work_item_detail.vue'),
},
+ provide: {
+ [INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION]: true,
+ },
props: {
parentId: {
type: String,
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
index fe3e798cb1a..c7edd4a9a55 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
@@ -16,6 +16,7 @@ import {
WORKITEM_TREE_SHOWLABELS_LOCALSTORAGEKEY,
WORK_ITEM_TYPE_VALUE_EPIC,
WIDGET_TYPE_HIERARCHY,
+ INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION,
} from '../../constants';
import {
findHierarchyWidgets,
@@ -48,6 +49,11 @@ export default {
WorkItemRolledUpData,
},
inject: ['hasSubepicsFeature'],
+ provide() {
+ return {
+ [INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION]: !this.isDrawer,
+ };
+ },
props: {
fullPath: {
type: String,
@@ -96,6 +102,11 @@ export default {
required: false,
default: () => [],
},
+ isDrawer: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 698b383b47b..5f59a6b42ef 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -372,3 +372,7 @@ export const WORK_ITEM_BASE_ROUTE_MAP = {
export const WORKITEM_LINKS_SHOWLABELS_LOCALSTORAGEKEY = 'workItemLinks.showLabels';
export const WORKITEM_TREE_SHOWLABELS_LOCALSTORAGEKEY = 'workItemTree.showLabels';
export const WORKITEM_RELATIONSHIPS_SHOWLABELS_LOCALSTORAGEKEY = 'workItemRelationships.showLabels';
+
+export const INJECTION_LINK_CHILD_PREVENT_ROUTER_NAVIGATION = Symbol(
+ 'injection:prevent-router-navigation',
+);
diff --git a/app/models/service_desk_setting.rb b/app/models/service_desk_setting.rb
index 03f0e2ccdaf..758e8cadacd 100644
--- a/app/models/service_desk_setting.rb
+++ b/app/models/service_desk_setting.rb
@@ -19,11 +19,14 @@ class ServiceDeskSetting < ApplicationRecord
allow_blank: true,
format: { with: /\A[a-z0-9_]+\z/, message: ->(setting, data) { _("can contain only lowercase letters, digits, and '_'.") } }
+ # Don't use Devise.email_regexp or URI::MailTo::EMAIL_REGEXP to be a bit more restrictive
+ # on the format of an email. For example because we don't want to allow `+` and other
+ # subaddress delimeters in the local part.
validates :custom_email,
length: { maximum: 255 },
uniqueness: true,
allow_nil: true,
- format: Gitlab::Utils::Email::EMAIL_REGEXP_WITH_ANCHORS
+ format: Gitlab::Email::ServiceDesk::CustomEmail::EMAIL_REGEXP_WITH_ANCHORS
validates :custom_email_credential,
presence: true,
diff --git a/app/models/user.rb b/app/models/user.rb
index db4c640075f..b74d7dbfc53 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1665,7 +1665,8 @@ class User < ApplicationRecord
counts = Organizations::OrganizationUser
.owners
- .joins('INNER JOIN ownerships ON ownerships.organization_id = organization_users.organization_id')
+ .where('organization_users.organization_id = organizations.id')
+ .group(:organization_id)
.having('count(organization_users.user_id) = 1')
Organizations::Organization
diff --git a/app/services/packages/conan/create_package_service.rb b/app/services/packages/conan/create_package_service.rb
index 35046d8776e..3153cb40d3f 100644
--- a/app/services/packages/conan/create_package_service.rb
+++ b/app/services/packages/conan/create_package_service.rb
@@ -4,7 +4,7 @@ module Packages
module Conan
class CreatePackageService < ::Packages::CreatePackageService
def execute
- create_package!(:conan,
+ created_package = create_package!(:conan,
name: params[:package_name],
version: params[:package_version],
conan_metadatum_attributes: {
@@ -12,6 +12,10 @@ module Packages
package_channel: params[:package_channel]
}
)
+
+ ServiceResponse.success(payload: { package: created_package })
+ rescue ActiveRecord::RecordInvalid => e
+ ServiceResponse.error(message: e.message, reason: :record_invalid)
end
end
end
diff --git a/app/validators/json_schemas/build_metadata_secrets.json b/app/validators/json_schemas/build_metadata_secrets.json
index 952f9cf5cda..f9122ff811c 100644
--- a/app/validators/json_schemas/build_metadata_secrets.json
+++ b/app/validators/json_schemas/build_metadata_secrets.json
@@ -39,6 +39,18 @@
},
"additionalProperties": false
},
+ "^gitlab_secrets_manager$": {
+ "type": "object",
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
"^gcp_secret_manager$": {
"type": "object",
"required": [
@@ -198,6 +210,11 @@
"required": [
"akeyless"
]
+ },
+ {
+ "required": [
+ "gitlab_secrets_manager"
+ ]
}
],
"additionalProperties": false
diff --git a/config/helpers/vite_plugin_images.mjs b/config/helpers/vite_plugin_images.mjs
index 993612c6e89..1175b4c7645 100644
--- a/config/helpers/vite_plugin_images.mjs
+++ b/config/helpers/vite_plugin_images.mjs
@@ -9,15 +9,17 @@ const imagesPaths = [
async function getAllFiles(dir, prependPath = '') {
const result = [];
- let files = []
+ let files = [];
try {
files = await readdir(dir, { withFileTypes: true });
- } catch(e) {}
+ // eslint-disable-next-line no-empty
+ } catch (e) {}
for (const file of files) {
const filePath = path.join(dir, file.name);
if (file.isDirectory()) {
+ // eslint-disable-next-line no-await-in-loop
const nestedFiles = await getAllFiles(filePath, `${prependPath}${file.name}/`);
result.push(...nestedFiles);
} else {
@@ -32,19 +34,26 @@ export async function ImagesPlugin() {
return {
name: 'vite-plugin-gitlab-images',
async config() {
- const [CEfiles, EEfiles, JHfiles] = await Promise.all(imagesPaths.map(async imagesPath => await getAllFiles(imagesPath)));
+ const [CEfiles, EEfiles, JHfiles] = await Promise.all(
+ // eslint-disable-next-line no-return-await
+ imagesPaths.map(async (imagesPath) => await getAllFiles(imagesPath)),
+ );
const [CEpath, EEpath, JHpath] = imagesPaths;
- const mappings = [[CEpath, CEfiles], [EEpath, EEfiles], [JHpath, JHfiles]].reduce((acc, [filesPath, filenames]) => {
- filenames.forEach(filename => {
+ const mappings = [
+ [CEpath, CEfiles],
+ [EEpath, EEfiles],
+ [JHpath, JHfiles],
+ ].reduce((acc, [filesPath, filenames]) => {
+ filenames.forEach((filename) => {
acc[filename] = path.resolve(filesPath, filename);
});
return acc;
}, {});
- const alias = Object.keys(mappings).map(mapping => {
+ const alias = Object.keys(mappings).map((mapping) => {
return {
find: mapping,
replacement: mappings[mapping],
- }
+ };
});
return {
resolve: {
diff --git a/config/metrics/schema/internal_events.json b/config/metrics/schema/internal_events.json
index d06c777b3a2..a7e035ec296 100644
--- a/config/metrics/schema/internal_events.json
+++ b/config/metrics/schema/internal_events.json
@@ -44,7 +44,12 @@
"type": "number"
}
},
- "additionalProperties": false
+ "additionalProperties": {
+ "type": [
+ "string",
+ "number"
+ ]
+ }
}
},
"additionalProperties": false
diff --git a/db/post_migrate/20241015085833_drop_ci_pipelines_config.rb b/db/post_migrate/20241015085833_drop_ci_pipelines_config.rb
index 274704ec6c4..589cb96e358 100644
--- a/db/post_migrate/20241015085833_drop_ci_pipelines_config.rb
+++ b/db/post_migrate/20241015085833_drop_ci_pipelines_config.rb
@@ -4,9 +4,9 @@ class DropCiPipelinesConfig < Gitlab::Database::Migration[2.2]
milestone '17.6'
def up
- execute(<<~SQL)
- ALTER TABLE p_ci_pipelines_config DETACH PARTITION ci_pipelines_config;
+ drop_table(:ci_pipelines_config, if_exists: true)
+ execute(<<~SQL)
CREATE TABLE IF NOT EXISTS #{fully_qualified_partition_name(100)}
PARTITION OF p_ci_pipelines_config FOR VALUES IN (100);
@@ -16,8 +16,6 @@ class DropCiPipelinesConfig < Gitlab::Database::Migration[2.2]
CREATE TABLE IF NOT EXISTS #{fully_qualified_partition_name(102)}
PARTITION OF p_ci_pipelines_config FOR VALUES IN (102);
SQL
-
- drop_table(:ci_pipelines_config, if_exists: true)
end
def down
diff --git a/doc/administration/dedicated/configure_instance.md b/doc/administration/dedicated/configure_instance.md
index 49fadf9c762..d447f660a4f 100644
--- a/doc/administration/dedicated/configure_instance.md
+++ b/doc/administration/dedicated/configure_instance.md
@@ -104,34 +104,48 @@ Even if a change request meets the minimum lead time, it might not be applied du
### Bring your own domain (BYOD)
-You can add a [custom hostname](../../subscriptions/gitlab_dedicated/index.md#bring-your-own-domain) for your GitLab Dedicated instance. Optionally, you can also provide a custom hostname for the bundled container registry and KAS services.
+You can use a [custom hostname](../../subscriptions/gitlab_dedicated/index.md#bring-your-own-domain) to access your GitLab Dedicated instance. You can also provide a custom hostname for the bundled container registry and Kubernetes Agent Server (KAS) services.
-Prerequisites:
+#### Let's Encrypt certificates
-- Access to your domain's server control panel to set up DNS records.
+GitLab Dedicated integrates with [Let's Encrypt](https://letsencrypt.org/), a free, automated, and open source certificate authority. When you use a custom hostname, Let's Encrypt automatically issues and renews SSL/TLS certificates for your domain.
+
+This integration uses the [`http-01` challenge](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) to obtain certificates through Let's Encrypt.
#### Set up DNS records
-Custom domains require a:
+To use a custom hostname with GitLab Dedicated, you must update your domain's DNS records.
-- `CNAME` record: Add a `CNAME` record that points your custom hostname to `tenant_name.gitlab-dedicated.com`.
+Prerequisites:
- ```plaintext
- gitlab.my-company.com. CNAME tenant_name.gitlab-dedicated.com
- ```
+- Access to your domain host's DNS settings.
-- `CAA` record: If your domain has an existing `CAA` (Certification Authority Authorization) record, [add a `CAA` record for Let's Encrypt](https://letsencrypt.org/docs/caa/). This allows Let's Encrypt to also issue certificates for your domain.
+To set up DNS records for a custom hostname with GitLab Dedicated:
- ```plaintext
- example.com. IN CAA 0 issue "pki.goog"
- example.com. IN CAA 0 issue "letsencrypt.org"
- ```
+1. Sign in to your domain host's website.
- In this example, the `CAA` record defines Google Trust Services (`"pki.goog"`) and Let's Encrypt (`"letsencrypt.org"`) as certificate authorities that are allowed to issue certificates for your domain.
+1. Go to the DNS settings.
-#### Add a custom hostname
+1. Add a `CNAME` record that points your custom hostname to your GitLab Dedicated tenant. For example:
-To add a custom hostname after your instance is created, submit a [support ticket](https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=4414917877650).
+ ```plaintext
+ gitlab.my-company.com. CNAME my-tenant.gitlab-dedicated.com
+ ```
+
+1. Optional. If your domain has an existing `CAA` record, update it to include [Let's Encrypt](https://letsencrypt.org/docs/caa/) as a valid certificate authority. If your domain does not have any `CAA` records, you can skip this step. For example:
+
+ ```plaintext
+ example.com. IN CAA 0 issue "pki.goog"
+ example.com. IN CAA 0 issue "letsencrypt.org"
+ ```
+
+ In this example, the `CAA` record defines Google Trust Services (`pki.goog`) and Let's Encrypt (`letsencrypt.org`) as certificate authorities that are allowed to issue certificates for your domain.
+
+1. Save your changes and wait for the DNS changes to propagate.
+
+#### Add your custom hostname
+
+To add a custom hostname to your existing GitLab Dedicated instance, submit a [support ticket](https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=4414917877650).
### SMTP email service
diff --git a/doc/administration/dedicated/img/high_level_architecture_diagram_v17_0.png b/doc/administration/dedicated/img/high_level_architecture_diagram_v17_0.png
deleted file mode 100644
index 91e20dafbbb..00000000000
Binary files a/doc/administration/dedicated/img/high_level_architecture_diagram_v17_0.png and /dev/null differ
diff --git a/doc/administration/dedicated/img/high_level_architecture_diagram_v18_0.png b/doc/administration/dedicated/img/high_level_architecture_diagram_v18_0.png
new file mode 100644
index 00000000000..29f2cb97557
Binary files /dev/null and b/doc/administration/dedicated/img/high_level_architecture_diagram_v18_0.png differ
diff --git a/doc/administration/dedicated/index.md b/doc/administration/dedicated/index.md
index 818dd77513c..ebd0e58a572 100644
--- a/doc/administration/dedicated/index.md
+++ b/doc/administration/dedicated/index.md
@@ -25,7 +25,7 @@ This page collects a set of architectural documents and diagrams for GitLab Dedi
The following diagram shows a high-level overview of the architecture for GitLab Dedicated,
where various AWS accounts managed by GitLab and customers are controlled by a Switchboard application.
-
+
When managing GitLab Dedicated tenant instances:
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 4933512ecb3..4d28a954927 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -119,16 +119,17 @@ page, with these behaviors:
- It doesn't pick people whose Slack or [GitLab status](../user/profile/index.md#set-your-current-status):
- Contains the string `OOO`, `PTO`, `Parental Leave`, `Friends and Family`, or `Conference`.
- Emoji is from one of these categories:
- - **On leave** - 🌴 `:palm_tree:`, 🏖️ `:beach:`, ⛱ `:beach_umbrella:`, 🏖 `:beach_with_umbrella:`, 🌞 `:sun_with_face:`, 🎡 `:ferris_wheel:`, 🏙 `:cityscape:`
- - **Out sick** - 🌡️ `:thermometer:`, 🤒 `:face_with_thermometer:`
+ - **On leave** - 🌴 `palm_tree`, 🏖️ `beach`, ⛱ `beach_umbrella`, 🏖 `beach_with_umbrella`, 🌞 `sun_with_face`, 🎡 `ferris_wheel`, 🏙 `cityscape`
+ - **Out sick** - 🌡️ `thermometer`, 🤒 `face_with_thermometer`
+ - Important: The status emojis are not detected when present on the free text input **status message**. They have to be set on your GitLab **status emoji** by clicking on the emoji selector beside the text input.
- It doesn't pick people who are already assigned a number of reviews that is equal to
or greater than their chosen "review limit". The review limit is the maximum number of
reviews people are ready to handle at a time. Set a review limit by using one of the following
as a Slack or [GitLab status](../user/profile/index.md#set-your-current-status):
- - 2️⃣ - `:two:`
- - 3️⃣ - `:three:`
- - 4️⃣ - `:four:`
- - 5️⃣ - `:five:`
+ - 2️⃣ - `two`
+ - 3️⃣ - `three`
+ - 4️⃣ - `four`
+ - 5️⃣ - `five`
The minimum review limit is 2️⃣. The reason for not being able to completely turn oneself off
for reviews has been discussed [in this issue](https://gitlab.com/gitlab-org/quality/engineering-productivity/team/-/issues/377).
diff --git a/doc/development/internal_analytics/internal_event_instrumentation/metric_definition_guide.md b/doc/development/internal_analytics/internal_event_instrumentation/metric_definition_guide.md
index 8dc91a25a27..d34cf4ac24d 100644
--- a/doc/development/internal_analytics/internal_event_instrumentation/metric_definition_guide.md
+++ b/doc/development/internal_analytics/internal_event_instrumentation/metric_definition_guide.md
@@ -176,6 +176,14 @@ Whereas, this filter is even more restricted and only includes `pull_package` ev
property: deploy_token
```
+Filters support also [custom additional properties](quick_start.md#additional-properties):
+
+```yaml
+- name: pull_package
+ filter:
+ custom_key: custom_value
+```
+
Filters only support matching of exact values and not wildcards or regular expressions.
## Aggregated metrics
diff --git a/doc/development/internal_analytics/internal_event_instrumentation/quick_start.md b/doc/development/internal_analytics/internal_event_instrumentation/quick_start.md
index 7aaeb81ee5d..841f800a81f 100644
--- a/doc/development/internal_analytics/internal_event_instrumentation/quick_start.md
+++ b/doc/development/internal_analytics/internal_event_instrumentation/quick_start.md
@@ -85,10 +85,7 @@ Tracking classes already have three built-in properties:
- `value`(numeric)
The arbitrary naming and typing of the these three properties is due to constraints from the data extraction process.
-It's recommended to use these properties first, even if their name does not match with the data you want to track.
-This recommendation is particularly important if you want to leverage these attributes as
-[metric filters](metric_definition_guide.md#filters). You can further describe what is the actual data being tracked
-by using the `description` property in the YAML definition of the event. For an example, see
+It's recommended to use these properties first, even if their name does not match with the data you want to track. You can further describe what is the actual data being tracked by using the `description` property in the YAML definition of the event. For an example, see
[`create_ci_internal_pipeline.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/537ea367dab731e886e6040d8399c430fdb67ab7/config/events/create_ci_internal_pipeline.yml):
```ruby
@@ -127,9 +124,7 @@ track_internal_event(
)
```
-Please add custom properties only in addition to the built-in properties.
-
-Custom rules can not be used as [metric filters](metric_definition_guide.md#filters).
+Please add custom properties only in addition to the built-in properties. Additional properties can only have string or numeric values.
#### Controller and API helpers
diff --git a/doc/user/group/import/index.md b/doc/user/group/import/index.md
index 2578f24ebae..34b36d0d259 100644
--- a/doc/user/group/import/index.md
+++ b/doc/user/group/import/index.md
@@ -22,8 +22,8 @@ You can migrate GitLab groups:
- Between groups in the same GitLab instance.
WARNING:
-Migrating GitLab groups and projects by using direct transfer is [currently unavailable](https://status.gitlab.com). We don't have an
-estimated time for resolution. For more information, please [contact support](https://about.gitlab.com/support/).
+Migrating GitLab.com groups and projects by using direct transfer is [unavailable](https://status.gitlab.com).
+For more information, contact [GitLab Support](https://about.gitlab.com/support/).
Migration by direct transfer creates a new copy of the group. If you want to move groups instead of copying groups, you
can [transfer groups](../manage.md#transfer-a-group) if the groups are in the same GitLab instance. Transferring groups
diff --git a/doc/user/project/import/bitbucket_server.md b/doc/user/project/import/bitbucket_server.md
index 7527f6fad64..c54a4f6229d 100644
--- a/doc/user/project/import/bitbucket_server.md
+++ b/doc/user/project/import/bitbucket_server.md
@@ -18,9 +18,9 @@ DETAILS:
Import your projects from Bitbucket Server to GitLab.
WARNING:
-Importing from Bitbucket Server to GitLab.com is [currently unavailable](https://status.gitlab.com). We don't have an
-estimated time for resolution. For more information, please [contact support](https://about.gitlab.com/support/).
-This unavailability doesn't affect [importing from Bitbucket Cloud](bitbucket.md).
+Importing from Bitbucket Server to GitLab.com is [unavailable](https://status.gitlab.com).
+For more information, contact [GitLab Support](https://about.gitlab.com/support/).
+[Importing from Bitbucket Cloud](bitbucket.md) is not affected.
## Prerequisites
diff --git a/doc/user/project/import/gitea.md b/doc/user/project/import/gitea.md
index 16a89073d41..41cb86c7abf 100644
--- a/doc/user/project/import/gitea.md
+++ b/doc/user/project/import/gitea.md
@@ -17,8 +17,8 @@ DETAILS:
Import your projects from Gitea to GitLab.
WARNING:
-Importing from Gitea to GitLab.com is [currently unavailable](https://status.gitlab.com). We don't have an
-estimated time for resolution. For more information, please [contact support](https://about.gitlab.com/support/).
+Importing from Gitea to GitLab.com is [unavailable](https://status.gitlab.com).
+For more information, contact [GitLab Support](https://about.gitlab.com/support/).
The Gitea importer can import:
diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md
index c669d0801f5..c8d3a295ecb 100644
--- a/doc/user/project/import/github.md
+++ b/doc/user/project/import/github.md
@@ -18,8 +18,8 @@ You can import your GitHub projects from either GitHub.com or GitHub Enterprise.
migrate or import any types of groups or organizations from GitHub to GitLab.
WARNING:
-Importing from GitHub to GitLab.com is [currently unavailable](https://status.gitlab.com). We don't have an
-estimated time for resolution. For more information, please [contact support](https://about.gitlab.com/support/).
+Importing from GitHub to GitLab.com is [unavailable](https://status.gitlab.com).
+For more information, contact [GitLab Support](https://about.gitlab.com/support/).
Imported issues, merge requests, comments, and events have an **Imported** badge in GitLab.
diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md
index 8dfa4de9dcb..54fe24f4bd6 100644
--- a/doc/user/project/issues/managing_issues.md
+++ b/doc/user/project/issues/managing_issues.md
@@ -43,8 +43,11 @@ Generate a detailed description for an issue based on a short summary you provid
Prerequisites:
- You must belong to at least one group with the [experiment and beta features setting](../../gitlab_duo/turn_on_off.md#turn-on-beta-and-experimental-features) enabled.
-- You must have permission to view the issue.
+- You must have permission to create an issue.
- Only available for the plain text editor.
+- Only available when creating a new issue.
+ For a proposal to add support for generating descriptions when editing existing issues, see
+ [issue 474141](https://gitlab.com/gitlab-org/gitlab/-/issues/474141).
To generate an issue description:
@@ -59,22 +62,6 @@ Provide feedback on this experimental feature in [issue 409844](https://gitlab.c
**Data usage**: When you use this feature, the text you enter is sent to
the [large language model listed on the GitLab Duo page](../../gitlab_duo/index.md#issue-description-generation).
-### Remove a task list item
-
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/377307) in GitLab 15.9.
-
-Prerequisites:
-
-- You must have at least the Reporter role for the project, or be the author or assignee of the issue.
-
-In an issue description with task list items:
-
-1. Hover over a task list item and select the options menu (**{ellipsis_v}**).
-1. Select **Delete**.
-
-The task list item is removed from the issue description.
-Any nested task list items are moved up a nested level.
-
## Bulk edit issues from a project
You can edit multiple issues at a time when you're in a project.
@@ -153,14 +140,16 @@ To move an issue:
1. Search for a project to move the issue to.
1. Select **Move**.
+You can also use the `/move` [quick action](../quick_actions.md) in a comment or description.
+
### Moving tasks when the parent issue is moved
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/371252) in GitLab 16.9 [with a flag](../../../administration/feature_flags.md) named `move_issue_children`. Disabled by default.
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/371252) in GitLab 16.11.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/371252) in GitLab 17.3. Feature flag `move_issue_children` removed.
-When you move an issue to another project, all its child tasks are also
-moved to the target project and remain associated as child tasks on the moved issue.
+When you move an issue to another project, all its child tasks are also moved to the target project
+and remain as child tasks of the moved issue.
Each task is moved the same way as the parent, that is, it's closed in the original project and
copied to the target project.
@@ -224,7 +213,31 @@ To do it:
1. To exit the Rails console, enter `quit`.
-## Reorder list items in the issue description
+## Description lists and task lists
+
+When you use ordered lists, unordered lists, or task lists in issue descriptions, you can:
+
+- Reorder list items with drag and drop
+- Delete list items
+- [Convert task list items to GitLab Tasks](../../tasks.md#from-a-task-list-item)
+
+### Delete a task list item
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/377307) in GitLab 15.9.
+
+Prerequisites:
+
+- You must have at least the Reporter role for the project, or be the author or assignee of the issue.
+
+In an issue description with task list items:
+
+1. Hover over a task list item and select the options menu (**{ellipsis_v}**).
+1. Select **Delete**.
+
+The task list item is removed from the issue description.
+Any nested task list items are moved up a nested level.
+
+### Reorder list items in the issue description
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15260) in GitLab 15.0.
@@ -261,6 +274,8 @@ To close an issue, you can either:
1. Select **Plan > Issues**, then select your issue to view it.
1. In the upper-right corner, select **Issue actions** (**{ellipsis_v}**) and then **Close issue**.
+You can also use the `/close` [quick action](../quick_actions.md) in a comment or description.
+
### Reopen a closed issue
Prerequisites:
@@ -270,6 +285,8 @@ Prerequisites:
To reopen a closed issue, in the upper-right corner, select **Issue actions** (**{ellipsis_v}**) and then **Reopen issue**.
A reopened issue is no different from any other open issue.
+You can also use the `/reopen` [quick action](../quick_actions.md) in a comment or description.
+
### Closing issues automatically
You can close issues automatically by using certain words, called a _closing pattern_,
@@ -438,13 +455,12 @@ DETAILS:
You can promote an issue to an [epic](../../group/epics/index.md) in the immediate parent group.
-NOTE:
-Promoting a confidential issue to an epic makes all information
-related to the issue public, as epics are public to group members.
+Promoting a confidential issue to an epic creates a
+[confidential epic](../../group/epics/manage_epics.md#make-an-epic-confidential), retaining
+confidentiality.
When an issue is promoted to an epic:
-- If the issue was confidential, an additional warning is displayed first.
- An epic is created in the same group as the project of the issue.
- Subscribers of the issue are notified that the epic was created.
@@ -494,7 +510,11 @@ To add an issue to an [iteration](../../group/iterations/index.md):
1. From the dropdown list, select the iteration to add this issue to.
1. Select any area outside the dropdown list.
-Alternatively, you can use the `/iteration` [quick action](../quick_actions.md#issues-merge-requests-and-epics).
+To add an issue to an iteration, you can also:
+
+- Use the `/iteration` [quick action](../quick_actions.md#issues-merge-requests-and-epics)
+- Drag an issue into an iteration list in a board
+- Bulk edit issues from the issues list
## View all issues assigned to you
@@ -557,8 +577,7 @@ To filter the list issues for text in a title or description:
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Plan > Issues**.
1. Above the list of issues, in the **Search or filter results** text box, enter the searched phrase.
-1. In the dropdown list that appears, select **Search for this text**.
-1. Select the text box again, and in the dropdown list that appears, select **Search Within**, and then either **Titles** or **Descriptions**.
+1. In the dropdown list that appears, select **Search within**, and then either **Titles** or **Descriptions**.
1. Press Enter or select the search icon (**{search}**).
Filtering issues uses [PostgreSQL full text search](https://www.postgresql.org/docs/current/textsearch-intro.html)
@@ -567,7 +586,7 @@ to match meaningful and significant words to answer a query.
For example, if you search for `I am securing information for M&A`,
GitLab can return results with `securing`, `secured`,
or `information` in the title or description.
-However, GitLab won't match the sentence or the words `I`, `am` or `M&A` exactly,
+However, GitLab doesn't match the sentence or the words `I`, `am` or `M&A` exactly,
as they aren't deemed lexically meaningful or significant.
It's a limitation of PostgreSQL full text search.
@@ -591,7 +610,7 @@ You can use the OR operator (**is one of: `||`**) when you [filter the list of i
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Plan > Issues**.
-1. In the **Search** box, type the issue ID. For example, enter filter `#10` to return only issue 10.
+1. In the **Search** box, type `#` followed by the issue ID. For example, enter filter `#10` to return only issue 10.

diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md
index 3b1e2b4780a..12dcba17323 100644
--- a/doc/user/project/milestones/index.md
+++ b/doc/user/project/milestones/index.md
@@ -10,7 +10,10 @@ DETAILS:
**Tier:** Free, Premium, Ultimate
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
-Milestones in GitLab are a way to track issues and merge requests created to achieve a broader goal in a certain period of time.
+Milestones in GitLab are a way to track issues and merge requests created to achieve a broader goal
+in a certain period of time, such as a program increment or upcoming release.
+When you use milestones together with [iterations](../../group/iterations/index.md), you can track
+work across two concurrent timeboxes with different start and end dates.
Milestones allow you to organize issues and merge requests into a cohesive group, with an optional start date and an optional due date.
@@ -18,11 +21,14 @@ Milestones allow you to organize issues and merge requests into a cohesive group
Milestones can be used to track releases. To do so:
-1. Set the milestone due date to represent the release date of your release and leave the milestone start date blank.
+1. Set the milestone due date to represent the release date of your release.
+ If you do not have a defined start date for your release cycle, you can leave the milestone start
+ date blank.
1. Set the milestone title to the version of your release, such as `Version 9.4`.
-1. Add an issue to your release by associating the desired milestone from the issue's right-hand sidebar.
+1. Add issues to your release by selecting the milestone from the issue's right sidebar.
-Additionally, you can integrate milestones with the [Releases feature](../releases/index.md#associate-milestones-with-a-release).
+Additionally, to automatically generate release evidence when you create your release, integrate
+milestones with the [Releases feature](../releases/index.md#associate-milestones-with-a-release).
## Project milestones and group milestones
@@ -44,7 +50,7 @@ To view the milestone list:
1. Select **Plan > Milestones**.
In a project, GitLab displays milestones that belong to the project.
-In a group, GitLab displays milestones that belong to the group and all projects in the group.
+In a group, GitLab displays milestones that belong to the group and all projects and subgroups in the group.
### View milestones in a project with issues turned off
@@ -98,16 +104,19 @@ The tabs below the title and description show the following:
The milestone view contains a [burndown and burnup chart](burndown_and_burnup_charts.md),
showing the progress of completing a milestone.
-
+
#### Milestone sidebar
-The milestone sidebar on the milestone view shows the following:
+The sidebar on the milestone view shows the following:
- Percentage complete, which is calculated as number of closed issues divided by total number of issues.
- The start date and due date.
- The total time spent on all issues and merge requests assigned to the milestone.
- The total issue weight of all issues assigned to the milestone.
+- The count of total, open, closed, and merged merge requests.
+- Links to associated releases.
+- The milestone's reference you can copy to your clipboard.

@@ -185,7 +194,8 @@ To delete a milestone:
## Promote a project milestone to a group milestone
If you are expanding the number of projects in a group, you might want to share the same milestones
-among this group's projects. You can also promote project milestones to group milestones to
+among this group's projects.
+You can promote project milestones to the parent group to
make them available to other projects in the same group.
Promoting a milestone merges all project milestones across all projects in this group with the same
@@ -223,7 +233,11 @@ To assign or unassign a milestone:
You can select from both project and group milestones.
1. Select the milestone you want to assign.
-You can also use the `/assign` [quick action](../quick_actions.md) in a comment.
+To assign or unassign a milestone, you can also:
+
+- Use the `/milestone` [quick action](../quick_actions.md) in a comment or description
+- Drag an issue to a [milestone list](../issue_board.md#milestone-lists) in a board
+- [Bulk edit issues](../issues/managing_issues.md#bulk-edit-issues-from-a-project) from the issues list
## Filter issues and merge requests by milestone
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 00000000000..21bc504e264
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,683 @@
+/* eslint-disable import/no-default-export */
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import localRules from 'eslint-plugin-local-rules';
+import js from '@eslint/js';
+import { FlatCompat } from '@eslint/eslintrc';
+
+const filename = fileURLToPath(import.meta.url);
+const dirname = path.dirname(filename);
+const compat = new FlatCompat({
+ baseDirectory: dirname,
+ recommendedConfig: js.configs.recommended,
+ allConfig: js.configs.all,
+});
+
+const jestConfig = {
+ files: ['{,ee/}spec/frontend/**/*.js'],
+
+ settings: {
+ // We have to teach eslint-plugin-import what node modules we use
+ // otherwise there is an error when it tries to resolve them
+ 'import/core-modules': ['events', 'fs', 'path'],
+ 'import/resolver': {
+ jest: {
+ jestConfigFile: 'jest.config.js',
+ },
+ },
+ },
+
+ rules: {
+ '@gitlab/vtu-no-explicit-wrapper-destroy': 'error',
+ 'jest/expect-expect': [
+ 'off',
+ {
+ assertFunctionNames: ['expect*', 'assert*', 'testAction'],
+ },
+ ],
+ '@gitlab/no-global-event-off': 'off',
+ 'import/no-unresolved': [
+ 'error',
+ // The test fixtures and graphql schema are dynamically generated in CI
+ // during the `frontend-fixtures` and `graphql-schema-dump` jobs.
+ // They may not be present during linting.
+ {
+ ignore: ['^test_fixtures/', 'tmp/tests/graphql/gitlab_schema.graphql'],
+ },
+ ],
+ },
+};
+
+export default [
+ {
+ ignores: [
+ 'app/assets/javascripts/locale/**/app.js',
+ 'builds/',
+ 'coverage/',
+ 'coverage-frontend/',
+ 'node_modules/',
+ 'public/',
+ 'tmp/',
+ 'vendor/',
+ 'sitespeed-result/',
+ 'fixtures/**/*.graphql',
+ 'storybook/public',
+ 'spec/fixtures/**/*.graphql',
+ ],
+ },
+ ...compat.extends(
+ 'plugin:@gitlab/default',
+ 'plugin:@gitlab/i18n',
+ 'plugin:no-jquery/slim',
+ 'plugin:no-jquery/deprecated-3.4',
+ 'plugin:no-unsanitized/recommended-legacy',
+ './tooling/eslint-config/conditionally_ignore.js',
+ 'plugin:@gitlab/jest',
+ ),
+ ...compat.plugins('no-jquery', '@graphql-eslint'),
+ {
+ files: ['**/*.{js,vue}'],
+
+ plugins: {
+ 'local-rules': localRules,
+ },
+
+ languageOptions: {
+ globals: {
+ __webpack_public_path__: true,
+ gl: false,
+ gon: false,
+ localStorage: false,
+ IS_EE: false,
+ },
+ },
+
+ settings: {
+ 'import/resolver': {
+ webpack: {
+ config: './config/webpack.config.js',
+ },
+ },
+ },
+
+ rules: {
+ 'import/no-commonjs': 'error',
+ 'import/no-default-export': 'off',
+
+ 'no-underscore-dangle': [
+ 'error',
+ {
+ allow: ['__', '_links'],
+ },
+ ],
+
+ 'import/no-unresolved': [
+ 'error',
+ {
+ ignore: ['^(ee|jh)_component/'],
+ },
+ ],
+
+ 'lines-between-class-members': 'off',
+ 'no-jquery/no-animate-toggle': 'off',
+ 'no-jquery/no-event-shorthand': 'off',
+ 'no-jquery/no-serialize': 'error',
+ 'promise/always-return': 'off',
+ 'promise/no-callback-in-promise': 'off',
+ '@gitlab/no-global-event-off': 'error',
+
+ '@gitlab/vue-no-new-non-primitive-in-template': [
+ 'error',
+ {
+ allowNames: ['class(es)?$', '^style$', '^to$', '^$', '^variables$', 'attrs?$'],
+ },
+ ],
+
+ '@gitlab/vue-no-undef-apollo-properties': 'error',
+ '@gitlab/tailwind-no-interpolation': 'error',
+ '@gitlab/vue-tailwind-no-interpolation': 'error',
+
+ 'no-param-reassign': [
+ 'error',
+ {
+ props: true,
+ ignorePropertyModificationsFor: ['acc', 'accumulator', 'el', 'element', 'state'],
+ ignorePropertyModificationsForRegex: ['^draft'],
+ },
+ ],
+
+ 'import/order': [
+ 'error',
+ {
+ groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
+
+ pathGroups: [
+ {
+ pattern: '~/**',
+ group: 'internal',
+ },
+ {
+ pattern: 'emojis/**',
+ group: 'internal',
+ },
+ {
+ pattern: '{ee_,jh_,}empty_states/**',
+ group: 'internal',
+ },
+ {
+ pattern: '{ee_,jh_,}icons/**',
+ group: 'internal',
+ },
+ {
+ pattern: '{ee_,jh_,}images/**',
+ group: 'internal',
+ },
+ {
+ pattern: 'vendor/**',
+ group: 'internal',
+ },
+ {
+ pattern: 'shared_queries/**',
+ group: 'internal',
+ },
+ {
+ pattern: '{ee_,}spec/**',
+ group: 'internal',
+ },
+ {
+ pattern: '{ee_,jh_,}jest/**',
+ group: 'internal',
+ },
+ {
+ pattern: '{ee_,jh_,any_}else_ce/**',
+ group: 'internal',
+ },
+ {
+ pattern: 'ee/**',
+ group: 'internal',
+ },
+ {
+ pattern: '{ee_,jh_,}component/**',
+ group: 'internal',
+ },
+ {
+ pattern: 'jh_else_ee/**',
+ group: 'internal',
+ },
+ {
+ pattern: 'jh/**',
+ group: 'internal',
+ },
+ {
+ pattern: '{test_,}helpers/**',
+ group: 'internal',
+ },
+ {
+ pattern: 'test_fixtures/**',
+ group: 'internal',
+ },
+ ],
+
+ alphabetize: {
+ order: 'ignore',
+ },
+ },
+ ],
+
+ 'no-restricted-syntax': [
+ 'error',
+ {
+ selector: "ImportSpecifier[imported.name='GlSkeletonLoading']",
+ message: 'Migrate to GlSkeletonLoader, or import GlDeprecatedSkeletonLoading.',
+ },
+ {
+ selector: "ImportSpecifier[imported.name='GlSafeHtmlDirective']",
+ message: 'Use directive at ~/vue_shared/directives/safe_html.js instead.',
+ },
+ {
+ selector: 'Literal[value=/docs.gitlab.+\\u002Fee/]',
+ message:
+ 'No hard coded url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`',
+ },
+ {
+ selector: 'TemplateElement[value.cooked=/docs.gitlab.+\\u002Fee/]',
+ message:
+ 'No hard coded url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`',
+ },
+ {
+ selector: 'Literal[value=/(?=.*docs.gitlab.*)(?!.*\\u002Fee\\b.*)/]',
+ message: 'No hard coded url, use `DOCS_URL` in `jh_else_ce/lib/utils/url_utility`',
+ },
+ {
+ selector: 'TemplateElement[value.cooked=/(?=.*docs.gitlab.*)(?!.*\\u002Fee\\b.*)/]',
+ message: 'No hard coded url, use `DOCS_URL` in `jh_else_ce/lib/utils/url_utility`',
+ },
+ {
+ selector: 'Literal[value=/(?=.*about.gitlab.*)(?!.*\\u002Fblog\\b.*)/]',
+ message: 'No hard coded url, use `PROMO_URL` in `jh_else_ce/lib/utils/url_utility`',
+ },
+ {
+ selector: 'TemplateElement[value.cooked=/(?=.*about.gitlab.*)(?!.*\\u002Fblog\\b.*)/]',
+ message: 'No hard coded url, use `PROMO_URL` in `jh_else_ce/lib/utils/url_utility`',
+ },
+ {
+ selector:
+ 'TemplateLiteral[expressions.0.name=DOCS_URL] > TemplateElement[value.cooked=/\\u002Fjh|\\u002Fee/]',
+ message:
+ '`/ee` or `/jh` path found in docs url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`',
+ },
+ {
+ selector:
+ "MemberExpression[object.type='ThisExpression'][property.name=/(\\$delete|\\$set)/]",
+ message:
+ "Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead.",
+ },
+ ],
+
+ 'no-restricted-properties': [
+ 'error',
+ {
+ object: 'window',
+ property: 'open',
+ message:
+ 'Use `visitUrl` in `jh_else_ce/lib/utils/url_utility` to avoid cross-site leaks.',
+ },
+ {
+ object: 'vm',
+ property: '$delete',
+ message:
+ "Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead.",
+ },
+ {
+ object: 'Vue',
+ property: 'delete',
+ message:
+ "Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead.",
+ },
+ {
+ object: 'vm',
+ property: '$set',
+ message:
+ "Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead.",
+ },
+ {
+ object: 'Vue',
+ property: 'set',
+ message:
+ "Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead.",
+ },
+ ],
+
+ 'no-restricted-imports': [
+ 'error',
+ {
+ paths: [
+ {
+ name: 'mousetrap',
+ message: 'Import { Mousetrap } from ~/lib/mousetrap instead.',
+ },
+ {
+ name: 'vuex',
+ message:
+ 'See our documentation on "Migrating from VueX" for tips on how to avoid adding new VueX stores.',
+ },
+ {
+ name: '@sentry/browser',
+ message: 'Use "import * as Sentry from \'~/sentry/sentry_browser_wrapper\';" instead',
+ },
+ ],
+
+ patterns: [
+ {
+ group: ['react', 'react-dom/*'],
+ message:
+ 'We do not allow usage of React in our codebase except for the graphql_explorer',
+ },
+ ],
+ },
+ ],
+
+ 'unicorn/prefer-dom-node-dataset': ['error'],
+
+ 'no-unsanitized/method': [
+ 'error',
+ {
+ escape: {
+ methods: ['sanitize'],
+ },
+ },
+ ],
+
+ 'no-unsanitized/property': [
+ 'error',
+ {
+ escape: {
+ methods: ['sanitize'],
+ },
+ },
+ ],
+
+ 'unicorn/no-array-callback-reference': 'off',
+
+ 'vue/no-undef-components': [
+ 'error',
+ {
+ ignorePatterns: ['^router-link$', '^router-view$', '^gl-emoji$'],
+ },
+ ],
+
+ 'local-rules/require-valid-help-page-path': 'error',
+ 'local-rules/vue-require-valid-help-page-link-component': 'error',
+ },
+ },
+ {
+ files: ['{,ee/,jh/}spec/frontend*/**/*'],
+
+ rules: {
+ '@gitlab/require-i18n-strings': 'off',
+ '@gitlab/no-runtime-template-compiler': 'off',
+ '@gitlab/tailwind-no-interpolation': 'off',
+ '@gitlab/vue-tailwind-no-interpolation': 'off',
+ 'require-await': 'error',
+ 'import/no-dynamic-require': 'off',
+ 'no-import-assign': 'off',
+
+ 'no-restricted-syntax': [
+ 'error',
+ {
+ selector:
+ 'CallExpression[callee.object.name=/(wrapper|vm)/][callee.property.name="setData"]',
+ message: 'Avoid using "setData" on VTU wrapper',
+ },
+ {
+ selector:
+ "MemberExpression[object.type!='ThisExpression'][property.type='Identifier'][property.name='$nextTick']",
+ message:
+ 'Using $nextTick from a component instance is discouraged. Import nextTick directly from the Vue package.',
+ },
+ {
+ selector: "Identifier[name='setImmediate']",
+ message:
+ 'Prefer explicit waitForPromises (or equivalent), or jest.runAllTimers (or equivalent) to vague setImmediate calls.',
+ },
+ {
+ selector: "ImportSpecifier[imported.name='GlSkeletonLoading']",
+ message: 'Migrate to GlSkeletonLoader, or import GlDeprecatedSkeletonLoading.',
+ },
+ {
+ selector:
+ "CallExpression[arguments.length=1][arguments.0.type='Literal'] CallExpression[callee.property.name='toBe'] CallExpression[callee.property.name='attributes'][arguments.length=1][arguments.0.value='disabled']",
+ message:
+ 'Avoid asserting disabled attribute exact value, because Vue.js 2 and Vue.js 3 renders it differently. Use toBeDefined / toBeUndefined instead',
+ },
+ {
+ selector:
+ "MemberExpression[object.object.name='Vue'][object.property.name='config'][property.name='errorHandler']",
+ message:
+ 'Use setErrorHandler/resetVueErrorHandler from helpers/set_vue_error_handler.js instead.',
+ },
+ {
+ selector: 'Literal[value=/docs.gitlab.+\\u002Fee/]',
+ message:
+ 'No hard coded url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`',
+ },
+ {
+ selector: 'TemplateElement[value.cooked=/docs.gitlab.+\\u002Fee/]',
+ message:
+ 'No hard coded url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`',
+ },
+ {
+ selector: 'Literal[value=/(?=.*docs.gitlab.*)(?!.*\\u002Fee\\b.*)/]',
+ message: 'No hard coded url, use `DOCS_URL` in `jh_else_ce/lib/utils/url_utility`',
+ },
+ {
+ selector: 'TemplateElement[value.cooked=/(?=.*docs.gitlab.*)(?!.*\\u002Fee\\b.*)/]',
+ message: 'No hard coded url, use `DOCS_URL` in `jh_else_ce/lib/utils/url_utility`',
+ },
+ {
+ selector: 'Literal[value=/(?=.*about.gitlab.*)(?!.*\\u002Fblog\\b.*)/]',
+ message: 'No hard coded url, use `PROMO_URL` in `jh_else_ce/lib/utils/url_utility`',
+ },
+ {
+ selector: 'TemplateElement[value.cooked=/(?=.*about.gitlab.*)(?!.*\\u002Fblog\\b.*)/]',
+ message: 'No hard coded url, use `PROMO_URL` in `jh_else_ce/lib/utils/url_utility`',
+ },
+ {
+ selector:
+ 'TemplateLiteral[expressions.0.name=DOCS_URL] > TemplateElement[value.cooked=/\\u002Fjh|\\u002Fee/]',
+ message:
+ '`/ee` or `/jh` path found in docs url, use `DOCS_URL_IN_EE_DIR` in `jh_else_ce/lib/utils/url_utility`',
+ },
+ {
+ selector: 'CallExpression[callee.property.name=/(\\$delete|\\$set)/]',
+ message:
+ "Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead.",
+ },
+ ],
+
+ 'no-restricted-properties': [
+ 'error',
+ {
+ object: 'Vue',
+ property: 'delete',
+ message:
+ "Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead.",
+ },
+ {
+ object: 'Vue',
+ property: 'set',
+ message:
+ "Vue 2's set/delete methods are not available in Vue 3. Create/assign new objects with the desired properties instead.",
+ },
+ ],
+
+ 'no-unsanitized/method': 'off',
+ 'no-unsanitized/property': 'off',
+ 'local-rules/require-valid-help-page-path': 'off',
+ 'local-rules/vue-require-valid-help-page-link-component': 'off',
+
+ 'no-restricted-imports': [
+ 'error',
+ {
+ paths: [
+ {
+ name: 'mousetrap',
+ message: 'Import { Mousetrap } from ~/lib/mousetrap instead.',
+ },
+ {
+ name: 'vuex',
+ message:
+ 'See our documentation on "Migrating from VueX" for tips on how to avoid adding new VueX stores.',
+ },
+ {
+ name: '@sentry/browser',
+ message: 'Use "import * as Sentry from \'~/sentry/sentry_browser_wrapper\';" instead',
+ },
+ {
+ name: '~/locale',
+ importNames: ['__', 's__'],
+ message:
+ 'Do not externalize strings in specs: https://docs.gitlab.com/ee/development/i18n/externalization.html#test-files-jest',
+ },
+ ],
+ },
+ ],
+ },
+ },
+ {
+ files: [
+ 'config/**/*',
+ 'scripts/**/*',
+ '**/*.config.js',
+ '**/*.config.*.js',
+ '**/jest_resolver.js',
+ 'eslint.config.mjs',
+ ],
+
+ rules: {
+ '@gitlab/require-i18n-strings': 'off',
+ 'import/no-extraneous-dependencies': 'off',
+ 'import/no-commonjs': 'off',
+ 'import/no-nodejs-modules': 'off',
+ 'filenames/match-regex': 'off',
+ 'no-console': 'off',
+ },
+ },
+ {
+ files: ['**/*.stories.js'],
+
+ rules: {
+ 'filenames/match-regex': 'off',
+ '@gitlab/require-i18n-strings': 'off',
+ },
+ },
+ {
+ files: ['**/*.graphql'],
+
+ languageOptions: {
+ ecmaVersion: 5,
+ sourceType: 'script',
+
+ parserOptions: {
+ parser: '@graphql-eslint/eslint-plugin',
+ operations: '{,ee/,jh/}app/**/*.graphql',
+ schema: './tmp/tests/graphql/gitlab_schema_apollo.graphql',
+ },
+ },
+
+ rules: {
+ 'filenames/match-regex': 'off',
+ 'spaced-comment': 'off',
+ '@graphql-eslint/no-anonymous-operations': 'error',
+ '@graphql-eslint/unique-operation-name': 'error',
+ '@graphql-eslint/require-id-when-available': 'error',
+ '@graphql-eslint/no-unused-variables': 'error',
+ '@graphql-eslint/no-unused-fragments': 'error',
+ '@graphql-eslint/no-duplicate-fields': 'error',
+ },
+ },
+ {
+ files: [
+ 'app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql',
+ 'app/assets/javascripts/projects/settings/repository/branch_rules/graphql/mutations/create_branch_rule.mutation.graphql',
+ 'app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql',
+ 'ee/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql',
+ 'ee/app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql',
+ ],
+
+ rules: {
+ '@graphql-eslint/require-id-when-available': 'off',
+ },
+ },
+ {
+ files: ['{,spec/}tooling/**/*'],
+
+ rules: {
+ 'no-undef': 'off',
+ 'import/no-commonjs': 'off',
+ 'import/no-extraneous-dependencies': 'off',
+ 'no-restricted-syntax': 'off',
+ '@gitlab/require-i18n-strings': 'off',
+ },
+ },
+
+ // JIRA subscriptions config
+ {
+ files: ['app/assets/javascripts/jira_connect/subscriptions/**/*.{js,vue}'],
+
+ languageOptions: {
+ globals: {
+ AP: 'readonly',
+ },
+ },
+
+ rules: {
+ '@gitlab/require-i18n-strings': 'off',
+ '@gitlab/vue-require-i18n-strings': 'off',
+ },
+ },
+
+ // Storybook config
+ {
+ files: ['storybook/**/*.{js,vue}'],
+
+ rules: {
+ '@gitlab/require-i18n-strings': 'off',
+ 'import/no-extraneous-dependencies': 'off',
+ 'import/no-commonjs': 'off',
+ 'import/no-nodejs-modules': 'off',
+ 'filenames/match-regex': 'off',
+ 'no-console': 'off',
+ 'import/no-unresolved': 'off',
+ },
+ },
+
+ // Circular dependencies overrides
+ {
+ files: [
+ // https://gitlab.com/gitlab-org/gitlab/issues/37987
+ 'ee/app/assets/javascripts/vue_shared/**/*.{js,vue}',
+ // https://gitlab.com/gitlab-org/gitlab/issues/28716
+ '{,ee/}app/assets/javascripts/filtered_search/**/*.js',
+ // https://gitlab.com/gitlab-org/gitlab/issues/28719
+ 'app/assets/javascripts/image_diff/**/*.js',
+ ],
+
+ rules: {
+ 'import/no-cycle': 'off',
+ },
+ },
+
+ // Web IDE config
+ {
+ files: ['app/assets/javascripts/ide/**/*.{js,vue}'],
+
+ rules: {
+ // https://gitlab.com/gitlab-org/gitlab/issues/28717
+ 'import/no-cycle': 'off',
+ // https://gitlab.com/gitlab-org/gitlab/issues/33024
+ 'promise/no-nesting': 'off',
+ },
+ },
+
+ // Jest config
+ jestConfig,
+
+ // Integration tests config
+ {
+ files: ['{,ee/}spec/frontend_integration/**/*.js'],
+
+ settings: {
+ ...jestConfig.settings,
+ 'import/resolver': {
+ jest: {
+ jestConfigFile: 'jest.config.integration.js',
+ },
+ },
+ },
+
+ rules: {
+ ...jestConfig.rules,
+ 'no-restricted-imports': ['error', 'fs'],
+ },
+
+ languageOptions: {
+ globals: {
+ mockServer: false,
+ },
+ },
+ },
+
+ // Consumer specs config
+ {
+ files: ['{,ee/}spec/contracts/consumer/**/*.js'],
+
+ settings: {
+ 'import/core-modules': ['@pact-foundation/pact', 'jest-pact'],
+ },
+
+ rules: {
+ '@gitlab/require-i18n-strings': 'off',
+ },
+ },
+];
diff --git a/lib/api/helpers/packages/conan/api_helpers.rb b/lib/api/helpers/packages/conan/api_helpers.rb
index e0c6beb80ff..4c734468c27 100644
--- a/lib/api/helpers/packages/conan/api_helpers.rb
+++ b/lib/api/helpers/packages/conan/api_helpers.rb
@@ -176,11 +176,17 @@ module API
end
def find_or_create_package
- package || ::Packages::Conan::CreatePackageService.new(
+ return package if package
+
+ service_response = ::Packages::Conan::CreatePackageService.new(
project,
current_user,
params.merge(build: current_authenticated_job)
).execute
+
+ bad_request!(service_response.message) if service_response.error?
+
+ service_response[:package]
end
def track_push_package_event
diff --git a/lib/gitlab/ci/jwt_v2.rb b/lib/gitlab/ci/jwt_v2.rb
index 7892c4ad757..5638a3c42a3 100644
--- a/lib/gitlab/ci/jwt_v2.rb
+++ b/lib/gitlab/ci/jwt_v2.rb
@@ -8,7 +8,9 @@ module Gitlab
GITLAB_HOSTED_RUNNER = 'gitlab-hosted'
SELF_HOSTED_RUNNER = 'self-hosted'
- def self.for_build(build, aud:, sub_components: [:project_path, :ref_type, :ref], target_audience: nil)
+ def self.for_build(
+ build, aud:, sub_components: [:project_path, :ref_type,
+ :ref], target_audience: nil)
new(build, ttl: build.metadata_timeout, aud: aud, sub_components: sub_components,
target_audience: target_audience).encoded
end
diff --git a/lib/gitlab/email/service_desk/custom_email.rb b/lib/gitlab/email/service_desk/custom_email.rb
index c7e99750d8b..e78cef7feba 100644
--- a/lib/gitlab/email/service_desk/custom_email.rb
+++ b/lib/gitlab/email/service_desk/custom_email.rb
@@ -8,6 +8,7 @@ module Gitlab
# incoming_email and service_desk_email.
module CustomEmail
REPLY_ADDRESS_KEY_REGEXP = /\+([0-9a-f]{32})@/
+ EMAIL_REGEXP_WITH_ANCHORS = /\A(?>[a-zA-Z0-9]+|[\-._]+){1,255}@[\w\-.]{1,255}\.{1}[a-zA-Z]{2,63}\z/
class << self
def reply_address(issue, reply_key)
@@ -53,7 +54,7 @@ module Gitlab
def find_service_desk_setting_from_reply_address(email, key)
potential_custom_email = email.sub("+#{key}", '')
- return unless Gitlab::Utils::Email::EMAIL_REGEXP_WITH_ANCHORS.match?(potential_custom_email)
+ return unless EMAIL_REGEXP_WITH_ANCHORS.match?(potential_custom_email)
ServiceDeskSetting.find_by_custom_email(potential_custom_email)
end
diff --git a/lib/gitlab/event_store/subscription.rb b/lib/gitlab/event_store/subscription.rb
index 7c22232de0b..d74259b0bdd 100644
--- a/lib/gitlab/event_store/subscription.rb
+++ b/lib/gitlab/event_store/subscription.rb
@@ -68,7 +68,7 @@ module Gitlab
def events_worker_args(event_class, events)
events
- .map { |event| event.data.deep_stringify_keys }
+ .map { |event| event.data.deep_stringify_keys.to_h }
.each_slice(group_size)
.map { |events_data_group| [event_class.name, events_data_group] }
end
diff --git a/lib/gitlab/internal_events.rb b/lib/gitlab/internal_events.rb
index 35d115a9e5b..395d6780b7b 100644
--- a/lib/gitlab/internal_events.rb
+++ b/lib/gitlab/internal_events.rb
@@ -18,14 +18,14 @@ module Gitlab
Gitlab::Tracking::EventValidator.new(event_name, additional_properties, kwargs).validate!
extra = custom_additional_properties(additional_properties)
- additional_properties = additional_properties.slice(*base_additional_properties.keys)
+ base_additional_properties = additional_properties.slice(*base_additional_properties_keys)
project = kwargs[:project]
kwargs[:namespace] ||= project.namespace if project
update_redis_values(event_name, additional_properties, kwargs)
- trigger_snowplow_event(event_name, category, additional_properties, extra, kwargs) if send_snowplow_event
- send_application_instrumentation_event(event_name, additional_properties, kwargs) if send_snowplow_event
+ trigger_snowplow_event(event_name, category, base_additional_properties, extra, kwargs) if send_snowplow_event
+ send_application_instrumentation_event(event_name, base_additional_properties, kwargs) if send_snowplow_event
if Feature.enabled?(:early_access_program, kwargs[:user], type: :wip)
create_early_access_program_event(event_name, category, additional_properties[:label], kwargs)
@@ -65,7 +65,7 @@ module Gitlab
end
def custom_additional_properties(additional_properties)
- additional_properties.except(*base_additional_properties.keys)
+ additional_properties.except(*base_additional_properties_keys)
end
def update_total_counter(event_selection_rule)
@@ -160,8 +160,8 @@ module Gitlab
end
strong_memoize_attr :gitlab_sdk_client
- def base_additional_properties
- Gitlab::Tracking::EventValidator::BASE_ADDITIONAL_PROPERTIES
+ def base_additional_properties_keys
+ Gitlab::Tracking::EventValidator::BASE_ADDITIONAL_PROPERTIES.keys
end
end
end
diff --git a/lib/gitlab/topology_service_client/base_service.rb b/lib/gitlab/topology_service_client/base_service.rb
new file mode 100644
index 00000000000..a70481590a0
--- /dev/null
+++ b/lib/gitlab/topology_service_client/base_service.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'gitlab/cells/topology_service'
+
+module Gitlab
+ module TopologyServiceClient
+ class BaseService
+ def initialize
+ raise NotImplementedError unless enabled?
+ end
+
+ private
+
+ def client
+ @client ||= service_class.new(
+ topology_service_address,
+ service_credentials
+ )
+ end
+
+ def cell_name
+ @cell_name ||= Gitlab.config.cell.name
+ end
+
+ def service_credentials
+ # mTls will be implemented later in Phase 5: https://gitlab.com/groups/gitlab-org/-/epics/14281
+ :this_channel_is_insecure
+ end
+
+ def topology_service_address
+ Gitlab.config.topology_service.address
+ end
+
+ def enabled?
+ Gitlab.config.topology_service.respond_to?(:enabled) && Gitlab.config.topology_service.enabled &&
+ Gitlab.config.cell.respond_to?(:name) && cell_name.present?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/topology_service_client/cell_service.rb b/lib/gitlab/topology_service_client/cell_service.rb
new file mode 100644
index 00000000000..f9ee9d80be8
--- /dev/null
+++ b/lib/gitlab/topology_service_client/cell_service.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'gitlab/cells/topology_service'
+
+module Gitlab
+ module TopologyServiceClient
+ class CellService < BaseService
+ def get_cell_info
+ response = client.get_cell(Gitlab::Cells::TopologyService::GetCellRequest.new(cell_name: cell_name))
+ response.cell_info
+ rescue GRPC::NotFound
+ Gitlab::AppLogger.error(message: "Cell '#{cell_name}' not found on Topology Service")
+ nil
+ end
+
+ private
+
+ def service_class
+ Gitlab::Cells::TopologyService::CellService::Stub
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/tracking/event_validator.rb b/lib/gitlab/tracking/event_validator.rb
index 06017097db8..c0e5b9fe316 100644
--- a/lib/gitlab/tracking/event_validator.rb
+++ b/lib/gitlab/tracking/event_validator.rb
@@ -11,6 +11,7 @@ module Gitlab
property: [String],
value: [Integer, Float]
}.freeze
+ CUSTOM_PROPERTIES_CLASSES = [String, Integer, Float].freeze
def initialize(event_name, additional_properties, kwargs)
@event_name = event_name
@@ -56,10 +57,13 @@ module Gitlab
# skip base properties validation. To be done in a separate MR as we have some non-compliant definitions
custom_properties = additional_properties.except(*BASE_ADDITIONAL_PROPERTIES.keys)
event_definition_attributes = Gitlab::Tracking::EventDefinition.find(event_name).to_h
+ allowed_types = CUSTOM_PROPERTIES_CLASSES
custom_properties.each_key do |key|
unless event_definition_attributes[:additional_properties].include?(key)
raise InvalidPropertyError, "Unknown additional property: #{key}"
end
+
+ validate_property!(custom_properties, key, *allowed_types)
end
end
end
diff --git a/lib/gitlab/usage_data_counters/hll_redis_legacy_events.yml b/lib/gitlab/usage_data_counters/hll_redis_legacy_events.yml
index f84348b05ed..b95da54b559 100644
--- a/lib/gitlab/usage_data_counters/hll_redis_legacy_events.yml
+++ b/lib/gitlab/usage_data_counters/hll_redis_legacy_events.yml
@@ -42,6 +42,7 @@
- i_ci_secrets_management_gcp_secret_manager_build_created
- i_ci_secrets_management_id_tokens_build_created
- i_ci_secrets_management_vault_build_created
+- i_ci_secrets_management_gitlab_secrets_manager_build_created
- i_code_review_click_diff_view_setting
- i_code_review_click_file_browser_setting
- i_code_review_click_single_file_mode_setting
diff --git a/lib/gitlab/utils/email.rb b/lib/gitlab/utils/email.rb
index 3679b5c444b..bde94d94960 100644
--- a/lib/gitlab/utils/email.rb
+++ b/lib/gitlab/utils/email.rb
@@ -5,12 +5,8 @@ module Gitlab
module Email
extend self
- # Don't use Devise.email_regexp or URI::MailTo::EMAIL_REGEXP to be a bit more restrictive
- # on the format of an email. Especially for custom email addresses which cannot contain a `+`
- # in app/models/service_desk_setting.rb
- EMAIL_REGEXP = /[\w\-._]+@[\w\-.]+\.{1}[a-zA-Z]{2,}/
+ EMAIL_REGEXP = %r{(?>[a-zA-Z0-9]+|[\-._!#$%&'*+\/=?^{|}~]+){1,255}@[\w\-.]{1,253}\.{1}[a-zA-Z]{2,63}}
EMAIL_REGEXP_WITH_CAPTURING_GROUP = /(#{EMAIL_REGEXP})/
- EMAIL_REGEXP_WITH_ANCHORS = /\A#{EMAIL_REGEXP.source}\z/
# Replaces most visible characters with * to obfuscate an email address
# deform adds a fix number of * to ensure the address cannot be guessed. Also obfuscates TLD with **
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 3b4cf3b7031..b7a1a028bf0 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -38691,9 +38691,6 @@ msgstr ""
msgid "Package type must be Terraform Module"
msgstr ""
-msgid "PackageRegistry|%{count} packages were not published to the registry. Remove these packages and try again."
-msgstr ""
-
msgid "PackageRegistry|%{linkStart}Wildcards%{linkEnd} such as `my-package-*` are supported."
msgstr ""
@@ -38907,6 +38904,9 @@ msgstr ""
msgid "PackageRegistry|Failed to load version data"
msgstr ""
+msgid "PackageRegistry|Failed to publish %{count} packages. Delete these packages and try again."
+msgstr ""
+
msgid "PackageRegistry|For more information on Composer packages in GitLab, %{linkStart}see the documentation.%{linkEnd}"
msgstr ""
@@ -39218,7 +39218,7 @@ msgstr ""
msgid "PackageRegistry|There was an error publishing %{count} packages"
msgstr ""
-msgid "PackageRegistry|There was an error publishing a %{packageName} package"
+msgid "PackageRegistry|There was an error publishing %{packageName}"
msgstr ""
msgid "PackageRegistry|This NuGet package has no dependencies."
diff --git a/package.json b/package.json
index 04e4a84f709..2c751b9b5c1 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"dev-server": "NODE_OPTIONS=\"${NODE_OPTIONS:=--max-old-space-size=5120}\" node scripts/frontend/webpack_dev_server.js",
"file-coverage": "scripts/frontend/file_test_coverage.js",
"lint-docs": "scripts/lint-doc.sh",
- "internal:eslint": "eslint --cache --max-warnings 0 --report-unused-disable-directives --ext .js,.vue,.graphql",
+ "internal:eslint": "eslint --cache --max-warnings 0 --report-unused-disable-directives",
"internal:stylelint": "stylelint -q --rd '{ee/,}app/assets/{stylesheets/**/*.{css,scss},builds/tailwind.css}'",
"preinternal:stylelint": "yarn run tailwindcss:build",
"prejest": "yarn check-dependencies",
@@ -76,7 +76,7 @@
"@gitlab/fonts": "^1.3.0",
"@gitlab/query-language": "^0.0.5-a-20241017",
"@gitlab/svgs": "3.119.0",
- "@gitlab/ui": "97.3.0",
+ "@gitlab/ui": "98.4.0",
"@gitlab/web-ide": "^0.0.1-dev-20240909013227",
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
"@rails/actioncable": "7.0.8-4",
@@ -254,6 +254,8 @@
"yaml": "^2.0.0-10"
},
"devDependencies": {
+ "@eslint/eslintrc": "^3.1.0",
+ "@eslint/js": "^9.13.0",
"@gitlab/eslint-plugin": "20.4.1",
"@gitlab/stylelint-config": "6.2.2",
"@graphql-eslint/eslint-plugin": "3.20.1",
diff --git a/qa/gems/gitlab-cng/lib/gitlab/cng/lib/deployment/installation.rb b/qa/gems/gitlab-cng/lib/gitlab/cng/lib/deployment/installation.rb
index ac3e09e9852..79ecf048986 100644
--- a/qa/gems/gitlab-cng/lib/gitlab/cng/lib/deployment/installation.rb
+++ b/qa/gems/gitlab-cng/lib/gitlab/cng/lib/deployment/installation.rb
@@ -189,6 +189,7 @@ module Gitlab
.deep_merge(license_values)
.deep_merge(env_values)
.deep_merge(configuration.values)
+ .deep_merge(ResourcePresets.resource_values(ci ? ResourcePresets::HIGH : ResourcePresets::DEFAULT))
.deep_stringify_keys
.to_yaml
diff --git a/qa/gems/gitlab-cng/lib/gitlab/cng/lib/deployment/resource_presets.rb b/qa/gems/gitlab-cng/lib/gitlab/cng/lib/deployment/resource_presets.rb
new file mode 100644
index 00000000000..15a4f326074
--- /dev/null
+++ b/qa/gems/gitlab-cng/lib/gitlab/cng/lib/deployment/resource_presets.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Cng
+ module Deployment
+ # Kubernetes resource request/limit presets optimised for different usecases
+ #
+ class ResourcePresets
+ DEFAULT = "default"
+ HIGH = "high"
+
+ class << self
+ # Kubernetes resources values for given preset
+ #
+ # @param [String] preset_name
+ # @return [Hash]
+ def resource_values(preset_name)
+ presets.fetch(preset_name)
+ end
+
+ private
+
+ # Different resources presets and replicas count
+ #
+ # Prefer vertical scaling over hpa for test stability
+ # Waiting for new pods to scale will lead to test flakiness and makes log reading harder
+ #
+ # @return [Hash]
+ def presets
+ @presets ||= {
+ # Default preset for local deployments
+ DEFAULT => {
+ gitlab: {
+ webservice: {
+ workerProcesses: 2,
+ minReplicas: 1,
+ resources: resources("1500m", "3Gi")
+ },
+ sidekiq: {
+ concurrency: 20,
+ minReplicas: 1,
+ resources: resources("900m", "1.6Gi"),
+ hpa: {
+ cpu: { targetAverageValue: "800m" }
+ }
+ },
+ kas: {
+ minReplicas: 1,
+ resources: resources("10m", "45Mi")
+ },
+ gitlab_shell: {
+ minReplicas: 1,
+ resources: resources("80m", "16Mi")
+ },
+ gitaly: {
+ resources: resources("300m", "300Mi")
+ }
+ },
+ registry: {
+ resources: resources("40m", "20Mi"),
+ hpa: {
+ minReplicas: 1,
+ **cpu_utilization
+ }
+ },
+ minio: {
+ resources: resources("9m", "128Mi")
+ }
+ },
+ # This preset is optimised for running e2e tests in parallel
+ HIGH => {
+ gitlab: {
+ webservice: {
+ workerProcesses: 4,
+ minReplicas: 1,
+ resources: resources(3, "4.5Gi"),
+ hpa: cpu_utilization
+ },
+ sidekiq: {
+ concurrency: 30,
+ minReplicas: 1,
+ resources: resources("1200m", "2Gi"),
+ hpa: cpu_utilization
+ },
+ kas: {
+ minReplicas: 1,
+ resources: resources("40m", "64Mi"),
+ hpa: cpu_utilization
+ },
+ gitlab_shell: {
+ minReplicas: 1,
+ resources: resources("24m", "32Mi"),
+ hpa: cpu_utilization
+ },
+ gitaly: {
+ resources: resources("450m", "450Mi")
+ }
+ },
+ registry: {
+ resources: resources("50m", "32Mi"),
+ hpa: {
+ minReplicas: 1,
+ **cpu_utilization
+ }
+ },
+ minio: {
+ resources: resources("15m", "256Mi")
+ }
+ }
+ }
+ end
+
+ # Kubernetes resources configuration
+ #
+ # Set limits equal to requests by default for simplicity
+ #
+ # @param [] cpu_r
+ # @param [String] memory_r
+ # @param [] cpu_l
+ # @param [String] memory_l
+ # @return [Hash]
+ def resources(cpu_r, memory_r, cpu_l = nil, memory_l = nil)
+ cpu_l ||= cpu_r
+ memory_l ||= memory_r
+
+ {
+ requests: {
+ cpu: cpu_r,
+ memory: memory_r
+ },
+ limits: {
+ cpu: cpu_l,
+ memory: memory_l
+ }
+ }
+ end
+
+ # Common hpa cpu utilization config
+ #
+ # @return [Hash]
+ def cpu_utilization
+ @cpu_utilization ||= {
+ cpu: {
+ targetType: "Utilization",
+ targetAverageUtilization: 90
+ }
+ }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/gems/gitlab-cng/lib/gitlab/cng/lib/kind/cluster.rb b/qa/gems/gitlab-cng/lib/gitlab/cng/lib/kind/cluster.rb
index 563adef31b0..ea8062f8d9c 100644
--- a/qa/gems/gitlab-cng/lib/gitlab/cng/lib/kind/cluster.rb
+++ b/qa/gems/gitlab-cng/lib/gitlab/cng/lib/kind/cluster.rb
@@ -48,6 +48,7 @@ module Gitlab
create_cluster
update_server_url
+ install_metrics_server
log("Cluster '#{name}' created", :success)
rescue Helpers::Shell::CommandFailure
# Exit cleanly without stacktrace if shell command fails
diff --git a/qa/gems/gitlab-cng/spec/unit/gitlab/cng/deployment/installation_spec.rb b/qa/gems/gitlab-cng/spec/unit/gitlab/cng/deployment/installation_spec.rb
index b1ffb7e90bf..56235a93450 100644
--- a/qa/gems/gitlab-cng/spec/unit/gitlab/cng/deployment/installation_spec.rb
+++ b/qa/gems/gitlab-cng/spec/unit/gitlab/cng/deployment/installation_spec.rb
@@ -41,6 +41,12 @@ RSpec.describe Gitlab::Cng::Deployment::Installation, :aggregate_failures do
)
end
+ let(:resources_values) do
+ Gitlab::Cng::Deployment::ResourcePresets.resource_values(
+ ci ? Gitlab::Cng::Deployment::ResourcePresets::HIGH : Gitlab::Cng::Deployment::ResourcePresets::DEFAULT
+ )
+ end
+
let(:expected_values_yml) do
{
global: {
@@ -55,7 +61,7 @@ RSpec.describe Gitlab::Cng::Deployment::Installation, :aggregate_failures do
license: { secret: "gitlab-license" }
},
**config_values
- }.deep_stringify_keys.to_yaml
+ }.deep_merge(resources_values).deep_stringify_keys.to_yaml
end
before do
diff --git a/qa/gems/gitlab-cng/spec/unit/gitlab/cng/deployment/resource_presets_spec.rb b/qa/gems/gitlab-cng/spec/unit/gitlab/cng/deployment/resource_presets_spec.rb
new file mode 100644
index 00000000000..9b53282dcaf
--- /dev/null
+++ b/qa/gems/gitlab-cng/spec/unit/gitlab/cng/deployment/resource_presets_spec.rb
@@ -0,0 +1,154 @@
+# frozen_string_literal: true
+
+RSpec.describe Gitlab::Cng::Deployment::ResourcePresets do
+ it "returns default resources values preset" do
+ expect(described_class.resource_values(described_class::DEFAULT)).to eq({
+ gitlab: {
+ webservice: {
+ workerProcesses: 2,
+ minReplicas: 1,
+ resources: {
+ requests: { cpu: "1500m", memory: "3Gi" },
+ limits: { cpu: "1500m", memory: "3Gi" }
+ }
+ },
+ sidekiq: {
+ concurrency: 20,
+ minReplicas: 1,
+ resources: {
+ requests: { cpu: "900m", memory: "1.6Gi" },
+ limits: { cpu: "900m", memory: "1.6Gi" }
+ },
+ hpa: {
+ cpu: { targetAverageValue: "800m" }
+ }
+ },
+ kas: {
+ minReplicas: 1,
+ resources: {
+ requests: { cpu: "10m", memory: "45Mi" },
+ limits: { cpu: "10m", memory: "45Mi" }
+ }
+ },
+ gitlab_shell: {
+ minReplicas: 1,
+ resources: {
+ requests: { cpu: "80m", memory: "16Mi" },
+ limits: { cpu: "80m", memory: "16Mi" }
+ }
+ },
+ gitaly: {
+ resources: {
+ requests: { cpu: "300m", memory: "300Mi" },
+ limits: { cpu: "300m", memory: "300Mi" }
+ }
+ }
+ },
+ registry: {
+ resources: {
+ requests: { cpu: "40m", memory: "20Mi" },
+ limits: { cpu: "40m", memory: "20Mi" }
+ },
+ hpa: {
+ minReplicas: 1,
+ cpu: {
+ targetType: "Utilization",
+ targetAverageUtilization: 90
+ }
+ }
+ },
+ minio: {
+ resources: {
+ requests: { cpu: "9m", memory: "128Mi" },
+ limits: { cpu: "9m", memory: "128Mi" }
+ }
+ }
+ })
+ end
+
+ it "returns high resources values preset" do
+ expect(described_class.resource_values(described_class::HIGH)).to eq({
+ gitlab: {
+ webservice: {
+ workerProcesses: 4,
+ minReplicas: 1,
+ resources: {
+ requests: { cpu: 3, memory: "4.5Gi" },
+ limits: { cpu: 3, memory: "4.5Gi" }
+ },
+ hpa: {
+ cpu: {
+ targetType: "Utilization",
+ targetAverageUtilization: 90
+ }
+ }
+ },
+ sidekiq: {
+ concurrency: 30,
+ minReplicas: 1,
+ resources: {
+ requests: { cpu: "1200m", memory: "2Gi" },
+ limits: { cpu: "1200m", memory: "2Gi" }
+ },
+ hpa: {
+ cpu: {
+ targetType: "Utilization",
+ targetAverageUtilization: 90
+ }
+ }
+ },
+ kas: {
+ minReplicas: 1,
+ resources: {
+ requests: { cpu: "40m", memory: "64Mi" },
+ limits: { cpu: "40m", memory: "64Mi" }
+ },
+ hpa: {
+ cpu: {
+ targetType: "Utilization",
+ targetAverageUtilization: 90
+ }
+ }
+ },
+ gitlab_shell: {
+ minReplicas: 1,
+ resources: {
+ requests: { cpu: "24m", memory: "32Mi" },
+ limits: { cpu: "24m", memory: "32Mi" }
+ },
+ hpa: {
+ cpu: {
+ targetType: "Utilization",
+ targetAverageUtilization: 90
+ }
+ }
+ },
+ gitaly: {
+ resources: {
+ requests: { cpu: "450m", memory: "450Mi" },
+ limits: { cpu: "450m", memory: "450Mi" }
+ }
+ }
+ },
+ registry: {
+ resources: {
+ requests: { cpu: "50m", memory: "32Mi" },
+ limits: { cpu: "50m", memory: "32Mi" }
+ },
+ hpa: {
+ minReplicas: 1,
+ cpu: {
+ targetType: "Utilization",
+ targetAverageUtilization: 90
+ }
+ }
+ },
+ minio: {
+ resources: {
+ requests: { cpu: "15m", memory: "256Mi" },
+ limits: { cpu: "15m", memory: "256Mi" }
+ }
+ }
+ })
+ end
+end
diff --git a/qa/gems/gitlab-cng/spec/unit/gitlab/cng/kind/cluster_spec.rb b/qa/gems/gitlab-cng/spec/unit/gitlab/cng/kind/cluster_spec.rb
index 1430fc13682..d365febd913 100644
--- a/qa/gems/gitlab-cng/spec/unit/gitlab/cng/kind/cluster_spec.rb
+++ b/qa/gems/gitlab-cng/spec/unit/gitlab/cng/kind/cluster_spec.rb
@@ -84,11 +84,11 @@ RSpec.describe Gitlab::Cng::Kind::Cluster do
it "creates cluster with ci specific configuration", :aggregate_failures do
expect { cluster.create }.to output(/Cluster '#{name}' created/).to_stdout
- expect(helm).not_to have_received(:add_helm_chart).with(
+ expect(helm).to have_received(:add_helm_chart).with(
"metrics-server",
"https://kubernetes-sigs.github.io/metrics-server/"
)
- expect(helm).not_to have_received(:upgrade).with(
+ expect(helm).to have_received(:upgrade).with(
"metrics-server",
"metrics-server/metrics-server",
namespace: "kube-system",
diff --git a/scripts/frontend/find_frontend_files.mjs b/scripts/frontend/find_frontend_files.mjs
index f2387994283..e469076e499 100755
--- a/scripts/frontend/find_frontend_files.mjs
+++ b/scripts/frontend/find_frontend_files.mjs
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
import Runtime from 'jest-runtime';
import { readConfig } from 'jest-config';
-import createJestConfig from '../../jest.config.base.js';
+import createJestConfig from '../../jest.config.base';
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../../');
diff --git a/scripts/frontend/preinstall.mjs b/scripts/frontend/preinstall.mjs
index 09d980344ea..80e85bf226a 100644
--- a/scripts/frontend/preinstall.mjs
+++ b/scripts/frontend/preinstall.mjs
@@ -31,7 +31,7 @@ async function getCurrentTopLevelPatterns() {
return serializeAliasedDependencyPatterns(dependencies)
.concat(serializeAliasedDependencyPatterns(devDependencies))
- .filter(isAliasedDependency);
+ .filter(dep => isAliasedDependency(dep));
} catch {
return [];
}
diff --git a/scripts/frontend/quarantined_vue3_specs.txt b/scripts/frontend/quarantined_vue3_specs.txt
index f8a9f414ad3..5f3c101282d 100644
--- a/scripts/frontend/quarantined_vue3_specs.txt
+++ b/scripts/frontend/quarantined_vue3_specs.txt
@@ -439,7 +439,6 @@ spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
spec/frontend/vue_popovers_spec.js
spec/frontend/vue_shared/components/alert_details_table_spec.js
spec/frontend/vue_shared/components/badges/beta_badge_spec.js
-spec/frontend/vue_shared/components/chronic_duration_input_spec.js
spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js
spec/frontend/vue_shared/components/confirm_modal_spec.js
@@ -465,7 +464,6 @@ spec/frontend/vue_shared/components/registry/registry_search_spec.js
spec/frontend/vue_shared/components/runner_instructions/instructions/runner_aws_instructions_spec.js
spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
spec/frontend/vue_shared/components/segmented_control_button_group_spec.js
-spec/frontend/vue_shared/components/slot_switch_spec.js
spec/frontend/vue_shared/components/smart_virtual_list_spec.js
spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js
diff --git a/scripts/frontend/tailwindcss.mjs b/scripts/frontend/tailwindcss.mjs
index 5932b3c20a0..2ce7e43d5ff 100644
--- a/scripts/frontend/tailwindcss.mjs
+++ b/scripts/frontend/tailwindcss.mjs
@@ -59,11 +59,14 @@ export function webpackTailwindCompilerPlugin({ shouldWatch = true }) {
}
if (wasScriptCalledDirectly()) {
- build().then(() => {
- console.log('Tailwind utils built successfully')
- }).catch(e => {
- console.warn('Building Tailwind utils produced an error')
- console.error(e);
- process.exitCode = 1;
- });
+ build()
+ // eslint-disable-next-line promise/always-return
+ .then(() => {
+ console.log('Tailwind utils built successfully');
+ })
+ .catch((e) => {
+ console.warn('Building Tailwind utils produced an error');
+ console.error(e);
+ process.exitCode = 1;
+ });
}
diff --git a/scripts/internal_events/cli/flows/metric_definer.rb b/scripts/internal_events/cli/flows/metric_definer.rb
index 17ea14772d0..ca509acc19a 100755
--- a/scripts/internal_events/cli/flows/metric_definer.rb
+++ b/scripts/internal_events/cli/flows/metric_definer.rb
@@ -418,8 +418,15 @@ module InternalEventsCli
q.convert ->(input) { input.split(',').map(&:to_i).uniq }
q.validate %r{^(\d|\s|,)*$}
q.messages[:valid?] = "Inputs for #{formatted_prop} must be numeric"
- else
+ elsif property == 'property' || property == 'label'
q.convert ->(input) { input.split(',').map(&:strip).uniq }
+ else
+ q.convert ->(input) do
+ input.split(',').map do |value|
+ val = value.strip
+ cast_if_numeric(val)
+ end.uniq
+ end
end
end
@@ -431,6 +438,13 @@ module InternalEventsCli
inputs.map { |input| { property => input } }.uniq
end
+ def cast_if_numeric(text)
+ float = Float(text)
+ float % 1 == 0 ? float.to_i : float
+ rescue ArgumentError
+ text
+ end
+
# Helper for #prompt_for_event_filters
#
# Gets all the permutations of the provided property values.
diff --git a/scripts/lint/check_mermaid.mjs b/scripts/lint/check_mermaid.mjs
index c5def7ed2c7..ea4b1da8173 100755
--- a/scripts/lint/check_mermaid.mjs
+++ b/scripts/lint/check_mermaid.mjs
@@ -71,7 +71,6 @@ await Promise.all(
if (errors > 0) {
console.log(`Total errors: ${errors}`);
- // eslint-disable-next-line no-restricted-syntax
console.log(`To fix these errors, see https://docs.gitlab.com/ee/development/documentation/testing/#mermaid-chart-linting.`);
process.exit(1);
}
diff --git a/spec/contracts/consumer/.eslintrc.yml b/spec/contracts/consumer/.eslintrc.yml
deleted file mode 100644
index e4b380714d3..00000000000
--- a/spec/contracts/consumer/.eslintrc.yml
+++ /dev/null
@@ -1,7 +0,0 @@
----
-extends:
- - 'plugin:@gitlab/jest'
-settings:
- import/core-modules:
- - '@pact-foundation/pact'
- - jest-pact
diff --git a/spec/dot_gitlab_ci/rules_spec.rb b/spec/dot_gitlab_ci/rules_spec.rb
index e90bd612434..8cc0940ba1c 100644
--- a/spec/dot_gitlab_ci/rules_spec.rb
+++ b/spec/dot_gitlab_ci/rules_spec.rb
@@ -219,7 +219,7 @@ RSpec.describe '.gitlab/ci/rules.gitlab-ci.yml', feature_category: :tooling do
Dir.glob('.github/*') +
Dir.glob('.gitlab/{issue,merge_request}_templates/**/*') +
Dir.glob('.gitlab/*.toml') +
- Dir.glob('{,**/}.{DS_Store,eslintrc.yml,gitignore,gitkeep,keep}', File::FNM_DOTMATCH) +
+ Dir.glob('{,**/}.{DS_Store,gitignore,gitkeep,keep}', File::FNM_DOTMATCH) +
Dir.glob('{,vendor/}gems/*/.*') +
Dir.glob('{.git,.lefthook,.ruby-lsp}/**/*') +
Dir.glob('{file_hooks,log}/**/*') +
diff --git a/spec/fixtures/scripts/internal_events/events/event_with_multiple_custom_properties.yml b/spec/fixtures/scripts/internal_events/events/event_with_multiple_custom_properties.yml
new file mode 100644
index 00000000000..7e91c993dbf
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/events/event_with_multiple_custom_properties.yml
@@ -0,0 +1,29 @@
+---
+description: Engineer uses Internal Event CLI to define a new event
+internal_events: true
+action: internal_events_cli_used
+identifiers:
+- project
+- namespace
+- user
+additional_properties:
+ label:
+ description: TODO
+ custom_key1:
+ description: The extra custom property name 1
+ custom_key2:
+ description: The extra custom property name 2
+ custom_key3:
+ description: The extra custom property name 3
+ custom_key4:
+ description: The extra custom property name 3
+product_group: analytics_instrumentation
+milestone: '16.6'
+introduced_by_url: TODO
+distributions:
+- ce
+- ee
+tiers:
+- free
+- premium
+- ultimate
diff --git a/spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event_custom_key_filter.yml b/spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event_custom_key_filter.yml
new file mode 100644
index 00000000000..a391affc7cc
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event_custom_key_filter.yml
@@ -0,0 +1,38 @@
+---
+key_path: redis_hll_counters.count_distinct_user_id_from_failed_usage_attempts_under_60s_monthly
+description: Monthly count of unique users who tried and failed to define an internal event using the CLI
+product_group: analytics_instrumentation
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '16.6'
+introduced_by_url: TODO
+time_frame: 28d
+data_source: internal_events
+data_category: optional
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+tiers:
+- free
+- premium
+- ultimate
+events:
+- name: internal_events_cli_used
+ unique: user.id
+ filter:
+ label: failure
+ custom_key1: metrics
+ custom_key3: 30
+ custom_key4: 12.4
+- name: internal_events_cli_used
+ unique: user.id
+ filter:
+ label: failure
+ custom_key1: metrics
+ custom_key3: 13
+ custom_key4: 12.4
diff --git a/spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_custom_key_filter.yml b/spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_custom_key_filter.yml
new file mode 100644
index 00000000000..03d818e32ef
--- /dev/null
+++ b/spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_custom_key_filter.yml
@@ -0,0 +1,38 @@
+---
+key_path: redis_hll_counters.count_distinct_user_id_from_failed_usage_attempts_under_60s_weekly
+description: Weekly count of unique users who tried and failed to define an internal event using the CLI
+product_group: analytics_instrumentation
+performance_indicator_type: []
+value_type: number
+status: active
+milestone: '16.6'
+introduced_by_url: TODO
+time_frame: 7d
+data_source: internal_events
+data_category: optional
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+tiers:
+- free
+- premium
+- ultimate
+events:
+- name: internal_events_cli_used
+ unique: user.id
+ filter:
+ label: failure
+ custom_key1: metrics
+ custom_key3: 30
+ custom_key4: 12.4
+- name: internal_events_cli_used
+ unique: user.id
+ filter:
+ label: failure
+ custom_key1: metrics
+ custom_key3: 13
+ custom_key4: 12.4
diff --git a/spec/fixtures/scripts/internal_events/new_metrics.yml b/spec/fixtures/scripts/internal_events/new_metrics.yml
index f9c1bbaacdf..c14c252af28 100644
--- a/spec/fixtures/scripts/internal_events/new_metrics.yml
+++ b/spec/fixtures/scripts/internal_events/new_metrics.yml
@@ -366,6 +366,38 @@
- path: config/metrics/counts_7d/count_distinct_user_id_from_failed_usage_attempts_under_60s_weekly.yml
content: spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_all_additional_props.yml
+- description: Create a weekly/monthly metric for a single event with custom additional properties filters
+ inputs:
+ files:
+ - path: config/events/internal_events_cli_used.yml
+ content: spec/fixtures/scripts/internal_events/events/event_with_multiple_custom_properties.yml
+ keystrokes:
+ - "2\n" # Enum-select: New Metric -- calculate how often one or more existing events occur over time
+ - "1\n" # Enum-select: Single event -- count occurrences of a specific event or user interaction
+ - "internal_events_cli_used" # Filters to this event
+ - "\n" # Select: config/events/internal_events_cli_used.yml
+ - "\e[B\e[B\e[B" # Arrow down to: Weekly count of unique users where label/value is...
+ - "\n" # Select: Weekly count of unique users where label/value is...
+ - "failure\n" # Input value for "label" filter
+ - "metrics\n" # Input value for "custom_key1" filter
+ - "\n" # Skip "custom_key2" filter
+ - "30,13\n" # Input value for "custom_key3" filter
+ - "12.4\n" # Input value for "custom_key4" filter
+ - "who tried and failed to define an internal event using the CLI\n" # Input description
+ - "failed_usage_attempts_under_60s\n" # Input metric key path
+ - "\n" # Submit weekly description for monthly
+ - "\n" # Submit weekly name for monthly
+ - "1\n" # Enum-select: Copy & continue
+ - "y\n" # Create file
+ - "y\n" # Create file
+ - "5\n" # Exit
+ outputs:
+ files:
+ - path: config/metrics/counts_28d/count_distinct_user_id_from_failed_usage_attempts_under_60s_monthly.yml
+ content: spec/fixtures/scripts/internal_events/metrics/user_id_28d_single_event_custom_key_filter.yml
+ - path: config/metrics/counts_7d/count_distinct_user_id_from_failed_usage_attempts_under_60s_weekly.yml
+ content: spec/fixtures/scripts/internal_events/metrics/user_id_7d_single_event_custom_key_filter.yml
+
- description: Create a weekly/monthly metric for multiple events with and without additional properties
inputs:
files:
diff --git a/spec/frontend/.eslintrc.yml b/spec/frontend/.eslintrc.yml
deleted file mode 100644
index 200f539fb3e..00000000000
--- a/spec/frontend/.eslintrc.yml
+++ /dev/null
@@ -1,29 +0,0 @@
----
-extends:
- - 'plugin:@gitlab/jest'
-settings:
- # We have to teach eslint-plugin-import what node modules we use
- # otherwise there is an error when it tries to resolve them
- import/core-modules:
- - events
- - fs
- - path
- import/resolver:
- jest:
- jestConfigFile: 'jest.config.js'
-rules:
- '@gitlab/vtu-no-explicit-wrapper-destroy': error
- jest/expect-expect:
- - off
- - assertFunctionNames:
- - 'expect*'
- - 'assert*'
- - 'testAction'
- "@gitlab/no-global-event-off":
- - off
- import/no-unresolved:
- - error
- # The test fixtures and graphql schema are dynamically generated in CI
- # during the `frontend-fixtures` and `graphql-schema-dump` jobs.
- # They may not be present during linting.
- - ignore: ['^test_fixtures\/', 'tmp/tests/graphql/gitlab_schema.graphql']
diff --git a/spec/frontend/vue_shared/components/chronic_duration_input_spec.js b/spec/frontend/admin/application_settings/runner_token_expiration/components/chronic_duration_input_spec.js
similarity index 90%
rename from spec/frontend/vue_shared/components/chronic_duration_input_spec.js
rename to spec/frontend/admin/application_settings/runner_token_expiration/components/chronic_duration_input_spec.js
index 374babe3a97..80d86b0fcfc 100644
--- a/spec/frontend/vue_shared/components/chronic_duration_input_spec.js
+++ b/spec/frontend/admin/application_settings/runner_token_expiration/components/chronic_duration_input_spec.js
@@ -1,12 +1,14 @@
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import ChronicDurationInput from '~/vue_shared/components/chronic_duration_input.vue';
+import { GlFormInput } from '@gitlab/ui';
+import ChronicDurationInput from '~/admin/application_settings/runner_token_expiration/components/chronic_duration_input.vue';
const MOCK_VALUE = 2 * 3600 + 20 * 60;
-describe('vue_shared/components/chronic_duration_input', () => {
+describe('admin/application_settings/runner_token_expiration/components/chronic_duration_input', () => {
let wrapper;
let textElement;
+ let textFormInput;
let hiddenElement;
afterEach(() => {
@@ -15,8 +17,10 @@ describe('vue_shared/components/chronic_duration_input', () => {
});
const findComponents = () => {
- textElement = wrapper.find('input[type=text]').element;
+ textElement = wrapper.findComponent(GlFormInput).element;
hiddenElement = wrapper.find('input[type=hidden]').element;
+
+ textFormInput = wrapper.findComponent(GlFormInput);
};
const createComponent = (props = {}) => {
@@ -44,8 +48,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
const createAndDispatch = async (initialValue, humanReadableInput) => {
createComponent({ value: initialValue });
await nextTick();
- textElement.value = humanReadableInput;
- textElement.dispatchEvent(new Event('input'));
+ textFormInput.vm.$emit('input', humanReadableInput);
};
describe('when starting with no value and receiving human-readable input', () => {
@@ -111,8 +114,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
});
it('emits valid with user input', async () => {
- textElement.value = '1m10s';
- textElement.dispatchEvent(new Event('input'));
+ textFormInput.vm.$emit('input', '1m10s');
await nextTick();
expect(wrapper.emitted('valid')).toEqual([
@@ -126,8 +128,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
expect(hiddenElement.validity.customError).toBe(false);
expect(hiddenElement.validationMessage).toBe('');
- textElement.value = '';
- textElement.dispatchEvent(new Event('input'));
+ textFormInput.vm.$emit('input', '');
await nextTick();
expect(wrapper.emitted('valid')).toEqual([
@@ -144,8 +145,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
});
it('emits invalid with user input', async () => {
- textElement.value = 'gobbledygook';
- textElement.dispatchEvent(new Event('input'));
+ textFormInput.vm.$emit('input', 'gobbledygook');
await nextTick();
expect(wrapper.emitted('valid')).toEqual([
@@ -203,8 +203,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
});
it('emits valid when input is integer', async () => {
- textElement.value = '2hr20min';
- textElement.dispatchEvent(new Event('input'));
+ textFormInput.vm.$emit('input', '2hr20min');
await nextTick();
expect(wrapper.emitted('change')).toEqual([[MOCK_VALUE]]);
@@ -221,8 +220,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
});
it('emits valid when input is decimal', async () => {
- textElement.value = '1.5s';
- textElement.dispatchEvent(new Event('input'));
+ textFormInput.vm.$emit('input', '1.5s');
await nextTick();
expect(wrapper.emitted('change')).toEqual([[1.5]]);
@@ -245,8 +243,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
});
it('emits valid when input is integer', async () => {
- textElement.value = '2hr20min';
- textElement.dispatchEvent(new Event('input'));
+ textFormInput.vm.$emit('input', '2hr20min');
await nextTick();
expect(wrapper.emitted('change')).toEqual([[MOCK_VALUE]]);
@@ -263,8 +260,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
});
it('emits invalid when input is decimal', async () => {
- textElement.value = '1.5s';
- textElement.dispatchEvent(new Event('input'));
+ textFormInput.vm.$emit('input', '1.5s');
await nextTick();
expect(wrapper.emitted('change')).toBeUndefined();
@@ -310,8 +306,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
});
it('passes updated prop via v-model', async () => {
- textElement.value = '2hr20min';
- textElement.dispatchEvent(new Event('input'));
+ textFormInput.vm.$emit('input', '2hr20min');
await nextTick();
expect(textElement.value).toBe('2hr20min');
@@ -321,8 +316,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
describe('change', () => {
it('passes user input to parent via v-model', async () => {
- textElement.value = '2hr20min';
- textElement.dispatchEvent(new Event('input'));
+ textFormInput.vm.$emit('input', '2hr20min');
await nextTick();
expect(wrapper.findComponent(ChronicDurationInput).props('value')).toBe(MOCK_VALUE);
@@ -369,8 +363,7 @@ describe('vue_shared/components/chronic_duration_input', () => {
});
it('creates form data with user-specified value', async () => {
- textElement.value = '1m10s';
- textElement.dispatchEvent(new Event('input'));
+ textFormInput.vm.$emit('input', '1m10s');
await nextTick();
const formData = new FormData(wrapper.find('[data-testid=myForm]').element);
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_errors_count_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_errors_count_spec.js
new file mode 100644
index 00000000000..01ebbec0f84
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_errors_count_spec.js
@@ -0,0 +1,117 @@
+import { GlAlert, GlButton } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { TEST_HOST } from 'spec/test_constants';
+import PackageErrorsCount from '~/packages_and_registries/package_registry/components/list/package_errors_count.vue';
+import { packageData } from '../../mock_data';
+
+describe('PackageErrorsCount', () => {
+ let wrapper;
+
+ const firstPackage = packageData();
+ const errorPackage = {
+ ...packageData(),
+ id: 'gid://gitlab/Packages::Package/121',
+ status: 'ERROR',
+ name: 'error package',
+ };
+
+ const findErrorPackageAlert = () => wrapper.findComponent(GlAlert);
+ const findErrorAlertButton = () => findErrorPackageAlert().findComponent(GlButton);
+
+ const mountComponent = ({ props = {}, stubs = {} } = {}) => {
+ wrapper = shallowMountExtended(PackageErrorsCount, {
+ propsData: {
+ ...props,
+ },
+ stubs: {
+ ...stubs,
+ },
+ });
+ };
+
+ describe('when an error package is present', () => {
+ beforeEach(() => {
+ mountComponent({ props: { errorPackages: [errorPackage] } });
+ });
+
+ it('should display an alert with default body message', () => {
+ expect(findErrorPackageAlert().exists()).toBe(true);
+ expect(findErrorPackageAlert().props('title')).toBe(
+ 'There was an error publishing error package',
+ );
+ expect(findErrorPackageAlert().text()).toBe(
+ 'There was a timeout and the package was not published. Delete this package and try again.',
+ );
+ });
+
+ it('should display alert body with message set in `statusMessage`', () => {
+ mountComponent({
+ props: {
+ errorPackages: [{ ...errorPackage, statusMessage: 'custom error message' }],
+ },
+ });
+
+ expect(findErrorPackageAlert().exists()).toBe(true);
+ expect(findErrorPackageAlert().props('title')).toBe(
+ 'There was an error publishing error package',
+ );
+ expect(findErrorPackageAlert().text()).toBe('custom error message');
+ });
+
+ describe('`Delete this package` button', () => {
+ beforeEach(() => {
+ mountComponent({
+ props: { errorPackages: [errorPackage] },
+ stubs: { GlAlert },
+ });
+ });
+
+ it('displays the button within the alert', () => {
+ expect(findErrorAlertButton().text()).toBe('Delete this package');
+ });
+
+ it('when clicked emits `confirm-delete` event', () => {
+ findErrorAlertButton().vm.$emit('click');
+
+ expect(wrapper.emitted('confirm-delete')[0][0]).toStrictEqual([errorPackage]);
+ });
+ });
+ });
+
+ describe('when multiple error packages are present', () => {
+ beforeEach(() => {
+ mountComponent({
+ props: { errorPackages: [{ ...firstPackage, status: errorPackage.status }, errorPackage] },
+ });
+ });
+
+ it('should display an alert with default body message', () => {
+ expect(findErrorPackageAlert().props('title')).toBe(
+ 'There was an error publishing 2 packages',
+ );
+ expect(findErrorPackageAlert().text()).toBe(
+ 'Failed to publish 2 packages. Delete these packages and try again.',
+ );
+ });
+
+ describe('`Show packages with errors` button', () => {
+ beforeEach(() => {
+ setWindowLocation(`${TEST_HOST}/foo?type=maven&after=1234`);
+ mountComponent({
+ props: {
+ errorPackages: [{ ...firstPackage, status: errorPackage.status }, errorPackage],
+ },
+ stubs: { GlAlert },
+ });
+ });
+
+ it('is shown with correct href within the alert', () => {
+ expect(findErrorAlertButton().text()).toBe('Show packages with errors');
+ expect(findErrorAlertButton().attributes('href')).toBe(
+ `${TEST_HOST}/foo?type=maven&status=error`,
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
index 554c1cc3334..23c5f09d847 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
@@ -1,13 +1,12 @@
-import { GlAlert, GlButton } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { TEST_HOST } from 'spec/test_constants';
import { stubComponent } from 'helpers/stub_component';
import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
-import setWindowLocation from 'helpers/set_window_location_helper';
+import PackageErrorsCount from '~/packages_and_registries/package_registry/components/list/package_errors_count.vue';
+
import {
DELETE_PACKAGE_TRACKING_ACTION,
DELETE_PACKAGES_TRACKING_ACTION,
@@ -52,8 +51,7 @@ describe('packages_list', () => {
const findEmptySlot = () => wrapper.findComponent(EmptySlotStub);
const findRegistryList = () => wrapper.findComponent(RegistryList);
const findPackagesListRow = () => wrapper.findComponent(PackagesListRow);
- const findErrorPackageAlert = () => wrapper.findComponent(GlAlert);
- const findErrorAlertButton = () => findErrorPackageAlert().findComponent(GlButton);
+ const findPackageErrorsCount = () => wrapper.findComponent(PackageErrorsCount);
const findDeletePackagesModal = () => wrapper.findComponent(DeleteModal);
const showMock = jest.fn();
@@ -138,8 +136,8 @@ describe('packages_list', () => {
expect(findDeletePackagesModal().props('showRequestForwardingContent')).toBe(false);
});
- it('does not have an error alert displayed', () => {
- expect(findErrorPackageAlert().exists()).toBe(false);
+ it('renders PackageErrorsCount component', () => {
+ expect(findPackageErrorsCount().props('errorPackages')).toEqual([]);
});
});
@@ -284,51 +282,23 @@ describe('packages_list', () => {
});
});
- describe('when an error package is present', () => {
+ describe('when error packages are present', () => {
beforeEach(() => {
mountComponent({ props: { list: [firstPackage, errorPackage] } });
});
- it('should display an alert with default body message', () => {
- expect(findErrorPackageAlert().exists()).toBe(true);
- expect(findErrorPackageAlert().props('title')).toBe(
- 'There was an error publishing a error package package',
- );
- expect(findErrorPackageAlert().text()).toBe(
- 'There was a timeout and the package was not published. Delete this package and try again.',
- );
+ it('renders PackageErrorsCount component with props', () => {
+ expect(findPackageErrorsCount().props('errorPackages')).toStrictEqual([errorPackage]);
});
- it('should display alert body with message set in `statusMessage`', () => {
- mountComponent({
- props: { list: [firstPackage, { ...errorPackage, statusMessage: 'custom error message' }] },
- });
+ it('and PackageErrorsCount component emits `confirm-delete`, modal component is shown', async () => {
+ findPackageErrorsCount().vm.$emit('confirm-delete', [errorPackage]);
- expect(findErrorPackageAlert().exists()).toBe(true);
- expect(findErrorPackageAlert().props('title')).toBe(
- 'There was an error publishing a error package package',
- );
- expect(findErrorPackageAlert().text()).toBe('custom error message');
- });
+ expect(showMock).toHaveBeenCalledTimes(1);
- describe('`Delete this package` button', () => {
- beforeEach(() => {
- mountComponent({ props: { list: [firstPackage, errorPackage] }, stubs: { GlAlert } });
- });
+ await nextTick();
- it('displays the button within the alert', () => {
- expect(findErrorAlertButton().text()).toBe('Delete this package');
- });
-
- it('should display the deletion modal when clicked on the `Delete this package` button', async () => {
- findErrorAlertButton().vm.$emit('click');
-
- await nextTick();
-
- expect(showMock).toHaveBeenCalledTimes(1);
-
- expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual([errorPackage]);
- });
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual([errorPackage]);
});
describe('when `hideErrorAlert` is true', () => {
@@ -339,43 +309,7 @@ describe('packages_list', () => {
});
it('does not display alert message', () => {
- expect(findErrorPackageAlert().exists()).toBe(false);
- });
- });
- });
-
- describe('when multiple error packages are present', () => {
- beforeEach(() => {
- mountComponent({
- props: { list: [{ ...firstPackage, status: errorPackage.status }, errorPackage] },
- });
- });
-
- it('should display an alert with default body message', () => {
- expect(findErrorPackageAlert().props('title')).toBe(
- 'There was an error publishing 2 packages',
- );
- expect(findErrorPackageAlert().text()).toBe(
- '2 packages were not published to the registry. Remove these packages and try again.',
- );
- });
-
- describe('`Show packages with errors` button', () => {
- beforeEach(() => {
- setWindowLocation(`${TEST_HOST}/foo?type=maven&after=1234`);
- mountComponent({
- props: {
- list: [{ ...firstPackage, status: errorPackage.status }, errorPackage],
- },
- stubs: { GlAlert },
- });
- });
-
- it('is shown with correct href within the alert', () => {
- expect(findErrorAlertButton().text()).toBe('Show packages with errors');
- expect(findErrorAlertButton().attributes('href')).toBe(
- `${TEST_HOST}/foo?type=maven&status=error`,
- );
+ expect(findPackageErrorsCount().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
index 5bb52da5bbb..41ff5e87ab6 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
@@ -34,7 +34,6 @@ describe('WikiForm', () => {
const findFormat = () => wrapper.find('#wiki_format');
const findMessage = () => wrapper.find('#wiki_message');
const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
- const findSubmitButton = () => wrapper.findByTestId('wiki-submit-button');
const findCancelButton = () => wrapper.findByTestId('wiki-cancel-button');
const findMarkdownHelpLink = () => wrapper.findByTestId('wiki-markdown-help-link');
@@ -384,36 +383,6 @@ describe('WikiForm', () => {
});
});
- describe('submit button state', () => {
- it.each`
- title | content | buttonState | disabledAttr
- ${'something'} | ${'something'} | ${'enabled'} | ${false}
- ${''} | ${'something'} | ${'disabled'} | ${true}
- ${'something'} | ${''} | ${'disabled'} | ${false}
- ${''} | ${''} | ${'disabled'} | ${true}
- `(
- "when title='$title', content='$content', then the button is $buttonState'",
- async ({ title, content, disabledAttr }) => {
- createWrapper({ mountFn: mount });
-
- await findTitle().setValue(title);
- await findMarkdownEditor().vm.$emit('input', content);
-
- expect(findSubmitButton().props().disabled).toBe(disabledAttr);
- },
- );
-
- it.each`
- persisted | buttonLabel
- ${true} | ${'Save changes'}
- ${false} | ${'Create page'}
- `('when persisted=$persisted, label is set to $buttonLabel', ({ persisted, buttonLabel }) => {
- createWrapper({ persisted });
-
- expect(findSubmitButton().text()).toBe(buttonLabel);
- });
- });
-
describe('cancel button state', () => {
it.each`
persisted | redirectLink
diff --git a/spec/frontend/vue_shared/components/slot_switch_spec.js b/spec/frontend/vue_shared/components/slot_switch_spec.js
deleted file mode 100644
index 3a2147c6c89..00000000000
--- a/spec/frontend/vue_shared/components/slot_switch_spec.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { assertProps } from 'helpers/assert_props';
-
-import SlotSwitch from '~/vue_shared/components/slot_switch.vue';
-
-describe('SlotSwitch', () => {
- const slots = {
- first: 'AGP',
- second: 'PCI
',
- };
-
- let wrapper;
-
- const createComponent = (propsData) => {
- wrapper = shallowMount(SlotSwitch, {
- propsData,
- slots,
- });
- };
-
- const getChildrenHtml = () => wrapper.findAll('* *').wrappers.map((c) => c.html());
-
- it('throws an error if activeSlotNames is missing', () => {
- expect(() => assertProps(SlotSwitch, {})).toThrow(
- '[Vue warn]: Missing required prop: "activeSlotNames"',
- );
- });
-
- it('renders no slots if activeSlotNames is empty', () => {
- createComponent({
- activeSlotNames: [],
- });
-
- expect(getChildrenHtml().length).toBe(0);
- });
-
- it('renders one slot if activeSlotNames contains single slot name', () => {
- createComponent({
- activeSlotNames: ['first'],
- });
-
- expect(getChildrenHtml()).toEqual([slots.first]);
- });
-
- it('renders multiple slots if activeSlotNames contains multiple slot names', () => {
- createComponent({
- activeSlotNames: Object.keys(slots),
- });
-
- expect(getChildrenHtml()).toEqual(Object.values(slots));
- });
-});
diff --git a/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js b/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js
index d262db3c338..d63d446b038 100644
--- a/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js
+++ b/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js
@@ -14,6 +14,7 @@ import WorkItemRelationshipIcons from '~/work_items/components/shared/work_item_
import {
workItemTask,
+ workItemEpic,
workItemObjectiveWithChild,
confidentialWorkItemTask,
closedWorkItemTask,
@@ -32,6 +33,8 @@ describe('WorkItemLinkChildContents', () => {
const mockAssignees = ASSIGNEES.assignees.nodes;
const mockLabels = LABELS.labels.nodes;
+ const mockRouterPush = jest.fn();
+
const findStatusBadgeComponent = () =>
wrapper.findByTestId('item-status-icon').findComponent(WorkItemStateBadge);
const findConfidentialIconComponent = () => wrapper.findByTestId('confidential-icon');
@@ -48,13 +51,23 @@ describe('WorkItemLinkChildContents', () => {
canUpdate = true,
childItem = workItemTask,
showLabels = true,
+ workItemFullPath = 'test-project-path',
+ isGroup = false,
} = {}) => {
wrapper = shallowMountExtended(WorkItemLinkChildContents, {
propsData: {
canUpdate,
childItem,
showLabels,
- workItemFullPath: 'test-project-path',
+ workItemFullPath,
+ },
+ provide: {
+ isGroup,
+ },
+ mocks: {
+ $router: {
+ push: mockRouterPush,
+ },
},
});
};
@@ -129,6 +142,29 @@ describe('WorkItemLinkChildContents', () => {
expect(wrapper.emitted('click')).toEqual([[eventObj]]);
});
+
+ describe('when the linked item can be navigated to via Vue Router', () => {
+ const preventDefault = jest.fn();
+ beforeEach(() => {
+ createComponent({
+ childItem: workItemEpic,
+ isGroup: true,
+ workItemFullPath: 'gitlab-org/gitlab-test',
+ });
+
+ findTitleEl().vm.$emit('click', { preventDefault });
+ });
+
+ it('pushes a new router state', () => {
+ expect(mockRouterPush).toHaveBeenCalled();
+ });
+ it('prevents the default event behaviour', () => {
+ expect(preventDefault).toHaveBeenCalled();
+ });
+ it('does not emit a click event', () => {
+ expect(wrapper.emitted('click')).not.toBeDefined();
+ });
+ });
});
describe('item metadata', () => {
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 3b639ddb443..2479cbfcd60 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -1898,14 +1898,15 @@ export const workItemEpic = {
namespace: {
__typename: 'Project',
id: '1',
- fullPath: 'test-project-path',
+ fullPath: 'gitlab-org/gitlab-test',
name: 'Project name',
},
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
- webUrl: '/gitlab-org/gitlab-test/-/work_items/4',
+ webUrl: 'http://127.0.0.1:3000/groups/gitlab-org/gitlab-test/-/work_items/4',
widgets: [
workItemObjectiveMetadataWidgets.ASSIGNEES,
+ workItemObjectiveMetadataWidgets.LINKED_ITEMS,
{
type: 'HIERARCHY',
hasChildren: false,
diff --git a/spec/frontend_integration/.eslintrc.yml b/spec/frontend_integration/.eslintrc.yml
deleted file mode 100644
index 8fff491bdcf..00000000000
--- a/spec/frontend_integration/.eslintrc.yml
+++ /dev/null
@@ -1,12 +0,0 @@
----
-extends: ../frontend/.eslintrc.yml
-settings:
- import/resolver:
- jest:
- jestConfigFile: 'jest.config.integration.js'
-rules:
- no-restricted-imports:
- - error
- - fs
-globals:
- mockServer: false
diff --git a/spec/lib/gitlab/internal_events_spec.rb b/spec/lib/gitlab/internal_events_spec.rb
index 72cc36adf16..16ab9fa3ff9 100644
--- a/spec/lib/gitlab/internal_events_spec.rb
+++ b/spec/lib/gitlab/internal_events_spec.rb
@@ -219,6 +219,11 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana
name: event_name,
time_framed: time_framed,
filter: { label: 'label_name', value: 16.17 }
+ ),
+ Gitlab::Usage::EventSelectionRule.new(
+ name: event_name,
+ time_framed: time_framed,
+ filter: { custom: 'custom_property' }
)
]
end
@@ -244,6 +249,27 @@ RSpec.describe Gitlab::InternalEvents, :snowplow, feature_category: :product_ana
end
end
+ context 'when event selection rule has a filter on a custom property' do
+ let(:custom_properties) { { custom: 'custom_property' } }
+ let(:redis_arguments) do
+ [
+ "filter:[custom:custom_property]-#{week_suffix}",
+ "#{event_name}-#{week_suffix}"
+ ]
+ end
+
+ it 'updates the correct redis keys' do
+ described_class.track_event(
+ event_name,
+ additional_properties: custom_properties,
+ user: user,
+ project: project
+ )
+
+ expect_redis_tracking
+ end
+ end
+
context 'when redis key is overridden in total_counter_redis_key_overrides.yml' do
let(:time_framed) { false }
let(:redis_arguments) { %w[SOME_LEGACY_KEY ANOTHER_LEGACY_KEY A_THIRD_LEGACY_KEY] }
diff --git a/spec/lib/gitlab/topology_service_client/base_service_spec.rb b/spec/lib/gitlab/topology_service_client/base_service_spec.rb
new file mode 100644
index 00000000000..ec4392160ca
--- /dev/null
+++ b/spec/lib/gitlab/topology_service_client/base_service_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::TopologyServiceClient::BaseService, feature_category: :cell do
+ subject(:base_service) { described_class.new }
+
+ describe '#initialize' do
+ context 'when topology service is disabled' do
+ it 'raises an error when topology service is not enabled' do
+ expect(Gitlab.config.topology_service).to receive(:enabled).and_return(false)
+
+ expect { base_service }.to raise_error(NotImplementedError)
+ end
+
+ it 'raises an error when no cell is configured' do
+ allow(Gitlab.config.topology_service).to receive(:enabled).and_return(true)
+ expect(Gitlab.config.cell).to receive(:name).once.and_return(nil)
+
+ expect { base_service }.to raise_error(NotImplementedError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/topology_service_client/cell_service_spec.rb b/spec/lib/gitlab/topology_service_client/cell_service_spec.rb
new file mode 100644
index 00000000000..fef48b3b582
--- /dev/null
+++ b/spec/lib/gitlab/topology_service_client/cell_service_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::TopologyServiceClient::CellService, feature_category: :cell do
+ subject(:cell_service) { described_class.new }
+ let(:service_class) { Gitlab::Cells::TopologyService::CellService::Stub } # gRpc Service Class
+
+ describe '#get_cell_info' do
+ context 'when topology service is disabled' do
+ it 'raises an error when topology service is not enabled' do
+ expect(Gitlab.config.topology_service).to receive(:enabled).and_return(false)
+
+ expect { cell_service }.to raise_error(NotImplementedError)
+ end
+
+ it 'raises an error when no cell is configured' do
+ allow(Gitlab.config.topology_service).to receive(:enabled).and_return(true)
+ expect(Gitlab.config.cell).to receive(:name).once.and_return(nil)
+
+ expect { cell_service }.to raise_error(NotImplementedError)
+ end
+ end
+
+ context 'when topology service is enabled' do
+ before do
+ allow(Gitlab.config.topology_service).to receive(:enabled).once.and_return(true)
+ allow(Gitlab.config.cell).to receive(:name).once.and_return("cell-1")
+ end
+
+ let(:cell_info) do
+ Gitlab::Cells::TopologyService::CellInfo.new(
+ name: "cell-1",
+ address: "127.0.0.1:3000",
+ session_prefix: "cell-1-",
+ sequence_range: Gitlab::Cells::TopologyService::SequenceRange.new(minval: 1, maxval: 1000)
+ )
+ end
+
+ it 'returns the cell information' do
+ expect_next_instance_of(service_class) do |instance|
+ expect(instance).to receive(:get_cell).with(
+ Gitlab::Cells::TopologyService::GetCellRequest.new(cell_name: "cell-1")
+ ).and_return(Gitlab::Cells::TopologyService::GetCellResponse.new(cell_info: cell_info))
+ end
+
+ expect(cell_service.get_cell_info).to eq(cell_info)
+ end
+
+ it 'returns nil if the cell is not found' do
+ expect_next_instance_of(service_class) do |instance|
+ expect(instance).to receive(:get_cell).with(
+ Gitlab::Cells::TopologyService::GetCellRequest.new(cell_name: "cell-1")
+ ).and_raise(GRPC::NotFound)
+ end
+
+ expected_error = "Cell 'cell-1' not found on Topology Service"
+ expect(Gitlab::AppLogger).to receive(:error).with(hash_including(message: expected_error))
+ expect(cell_service.get_cell_info).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/tracking/event_validator_spec.rb b/spec/lib/gitlab/tracking/event_validator_spec.rb
index 52f6775d1cd..1f6fb65ca5e 100644
--- a/spec/lib/gitlab/tracking/event_validator_spec.rb
+++ b/spec/lib/gitlab/tracking/event_validator_spec.rb
@@ -51,11 +51,12 @@ RSpec.describe Gitlab::Tracking::EventValidator, feature_category: :service_ping
end
end
- context 'when a base additional property is invalid' do
+ context 'when an additional property is invalid' do
[
{ label: 123 },
{ value: 'test_value' },
- { property: true }
+ { property: true },
+ { lang: [1, 2] }
].each do |invalid_property|
context "when #{invalid_property.each_key.first} is invalid" do
let(:additional_properties) { invalid_property }
diff --git a/spec/lib/gitlab/utils/email_spec.rb b/spec/lib/gitlab/utils/email_spec.rb
index 9d939a49da6..f1c33fead36 100644
--- a/spec/lib/gitlab/utils/email_spec.rb
+++ b/spec/lib/gitlab/utils/email_spec.rb
@@ -61,6 +61,10 @@ RSpec.describe Gitlab::Utils::Email, feature_category: :service_desk do
'added user@example.com and hello@example.com' | 'added us*****@e*****.c** and he*****@e*****.c**'
'removed user@example.com, hello@example.com and bye@example.com' |
'removed us*****@e*****.c**, he*****@e*****.c** and by*****@e*****.c**'
+ 'added user#@example.com, hello!@example.com and bye$@example.com' |
+ 'added us*****@e*****.c**, he*****@e*****.c** and by*****@e*****.c**'
+ 'added user_@example.com, hello}@example.com and !#$%&\'*+-/=?^_{|}~@example.com' |
+ 'added us*****@e*****.c**, he*****@e*****.c** and !#*****@e*****.c**'
end
with_them do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 92463724421..ee099ba2ee2 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -4866,42 +4866,41 @@ RSpec.describe User, feature_category: :user_profile do
end
describe '#solo_owned_organizations' do
- let_it_be_with_refind(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
- subject(:solo_owned_organizations) { user.solo_owned_organizations }
+ subject { user.solo_owned_organizations }
context 'no owned organizations' do
it { is_expected.to be_empty }
end
context 'has owned organizations' do
- let(:organization) { create(:organization) }
-
- before do
- organization.add_owner(user)
+ let_it_be(:solo_owned_organizations) { create_list(:organization_owner, 2, user: user).map(&:organization) }
+ let_it_be(:multi_owned_organization) do
+ create(:organization, organization_users: [
+ create(:organization_owner, user: user),
+ create(:organization_owner, user: create(:user))
+ ])
end
- context 'not solo owner' do
- let_it_be(:user2) { create(:user) }
-
- before do
- organization.add_owner(user2)
- end
-
- it { is_expected.to be_empty }
+ it 'returns solo-owned organizations' do
+ is_expected.to match_array(solo_owned_organizations)
end
- context 'solo owner' do
- it { is_expected.to include(organization) }
+ it 'does not return multi owned organizations' do
+ is_expected.not_to include(multi_owned_organization)
+ end
+ end
+
+ context 'solo owner with other members' do
+ let_it_be(:organization) do
+ create(:organization, organization_users: [
+ create(:organization_owner, user: user),
+ create(:organization_user, user: create(:user))
+ ])
end
- context 'solo owner with other members' do
- before do
- create(:organization_user, organization: organization)
- end
-
- it { is_expected.to include(organization) }
- end
+ it { is_expected.to include(organization) }
end
end
diff --git a/spec/services/packages/conan/create_package_service_spec.rb b/spec/services/packages/conan/create_package_service_spec.rb
index 7d36751f0e1..bdde64d9988 100644
--- a/spec/services/packages/conan/create_package_service_spec.rb
+++ b/spec/services/packages/conan/create_package_service_spec.rb
@@ -8,7 +8,20 @@ RSpec.describe Packages::Conan::CreatePackageService, feature_category: :package
subject(:service) { described_class.new(project, user, params) }
describe '#execute' do
- subject(:package) { service.execute }
+ let(:package) { service_response.payload.fetch(:package) }
+
+ subject(:service_response) { service.execute }
+
+ shared_examples 'returning an error service response and not creating conan package' do |message:|
+ it_behaves_like 'returning an error service response', message: message
+ it { is_expected.to have_attributes(reason: :record_invalid) }
+
+ it 'does not create a conan package' do
+ expect { service_response }
+ .to not_change { Packages::Package.conan.count }
+ .and not_change { Packages::PackageFile.count }
+ end
+ end
context 'valid params' do
let(:params) do
@@ -20,6 +33,8 @@ RSpec.describe Packages::Conan::CreatePackageService, feature_category: :package
}
end
+ it_behaves_like 'returning a success service response'
+
it 'creates a new package' do
expect(package).to be_valid
expect(package.name).to eq(params[:package_name])
@@ -30,8 +45,14 @@ RSpec.describe Packages::Conan::CreatePackageService, feature_category: :package
end
it_behaves_like 'assigns the package creator'
- it_behaves_like 'assigns build to package'
- it_behaves_like 'assigns status to package'
+
+ it_behaves_like 'assigns build to package' do
+ subject { super().payload.fetch(:package) }
+ end
+
+ it_behaves_like 'assigns status to package' do
+ subject { super().payload.fetch(:package) }
+ end
end
context 'invalid params' do
@@ -44,13 +65,13 @@ RSpec.describe Packages::Conan::CreatePackageService, feature_category: :package
}
end
- it 'fails' do
- expect { package }.to raise_error(ActiveRecord::RecordInvalid, /Conan metadatum package username is invalid/)
- end
+ it_behaves_like 'returning an error service response and not creating conan package',
+ message: 'Validation failed: Conan metadatum package username is invalid'
end
context 'with existing recipe' do
let_it_be(:existing_package) { create(:conan_package, project: project) }
+
let(:params) do
{
package_name: existing_package.name,
@@ -60,9 +81,8 @@ RSpec.describe Packages::Conan::CreatePackageService, feature_category: :package
}
end
- it 'does not create a conan package with same recipe' do
- expect { package }.to raise_error(ActiveRecord::RecordInvalid, /Package recipe already exists/)
- end
+ it_behaves_like 'returning an error service response and not creating conan package',
+ message: 'Validation failed: Package recipe already exists'
end
end
end
diff --git a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb
index 5b79db8abf4..fe696f84553 100644
--- a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb
@@ -31,13 +31,6 @@ RSpec.shared_examples 'User creates wiki page' do
end
end
- it "disables the submit button", :js do
- page.within(".wiki-form") do
- fill_in(:wiki_title, with: "")
- expect(page).to have_button('Create page', disabled: true)
- end
- end
-
it "makes sure links to unknown pages work correctly", :js do
page.within(".wiki-form") do
fill_in(:wiki_content, with: "[link test](test)")
diff --git a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
index a2a99e8e705..f6e2da16887 100644
--- a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
@@ -127,13 +127,6 @@ RSpec.shared_examples 'User updates wiki page' do
end
end
- it "disables the submit button", :js do
- page.within(".wiki-form") do
- fill_in(:wiki_title, with: "")
- expect(page).to have_button('Save changes', disabled: true)
- end
- end
-
it 'shows the emoji autocompletion dropdown', :js do
find('#wiki_content').native.send_keys('')
fill_in(:wiki_content, with: ':')
diff --git a/storybook/.eslintrc.yml b/storybook/.eslintrc.yml
deleted file mode 100644
index f3bbec8f26d..00000000000
--- a/storybook/.eslintrc.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-rules:
- '@gitlab/require-i18n-strings': off
- import/no-extraneous-dependencies: off
- import/no-commonjs: off
- import/no-nodejs-modules: off
- filenames/match-regex: off
- no-console: off
- import/no-unresolved: off
diff --git a/yarn.lock b/yarn.lock
index 39eb360acf4..2fcfe79401c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1273,11 +1273,31 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
+"@eslint/eslintrc@^3.1.0":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.1.0.tgz#dbd3482bfd91efa663cbe7aa1f506839868207b6"
+ integrity sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==
+ dependencies:
+ ajv "^6.12.4"
+ debug "^4.3.2"
+ espree "^10.0.1"
+ globals "^14.0.0"
+ ignore "^5.2.0"
+ import-fresh "^3.2.1"
+ js-yaml "^4.1.0"
+ minimatch "^3.1.2"
+ strip-json-comments "^3.1.1"
+
"@eslint/js@8.57.1":
version "8.57.1"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2"
integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==
+"@eslint/js@^9.13.0":
+ version "9.13.0"
+ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.13.0.tgz#c5f89bcd57eb54d5d4fa8b77693e9c28dc97e547"
+ integrity sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==
+
"@fastify/busboy@^2.0.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.0.tgz#0709e9f4cb252351c609c6e6d8d6779a8d25edff"
@@ -1381,10 +1401,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.119.0.tgz#becbeea7e7ee241baecdad02b9ad04de7a8aed04"
integrity sha512-Os/PF37pCY75uLA0dmGaZe13BmirzlWH+pFLinCAPRChEC7KhHCJtIy0efRAxzkA4uatmHpJHxftuTc7NeiSNQ==
-"@gitlab/ui@97.3.0":
- version "97.3.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-97.3.0.tgz#8d3a59666e4d463032dd6e18ee38f14f7785fd06"
- integrity sha512-4TqMBdHspR9+y83LEqLs+87wUSqOZIOKm9UQr9xJQszKBLdhd6TlhgAR4tsErw35d6CmSuMc4jbuGzckkoDLKA==
+"@gitlab/ui@98.4.0":
+ version "98.4.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-98.4.0.tgz#6f00322f1138abf894ccaed27f19609e9d6ab376"
+ integrity sha512-M+00vM4h4wTRr87C8vGWJzoKGKBtKmlmmdQUzATDratdWbGDqHGRn8qIu/ETjugBPcmfnkRPgipEWhrCTkCoOg==
dependencies:
"@floating-ui/dom" "1.4.3"
echarts "^5.3.2"
@@ -4146,10 +4166,10 @@ acorn@^6.4.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
-acorn@^8.0.4, acorn@^8.1.0, acorn@^8.11.0, acorn@^8.8.1, acorn@^8.9.0:
- version "8.12.1"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248"
- integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==
+acorn@^8.0.4, acorn@^8.1.0, acorn@^8.11.0, acorn@^8.12.0, acorn@^8.8.1, acorn@^8.9.0:
+ version "8.13.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.13.0.tgz#2a30d670818ad16ddd6a35d3842dacec9e5d7ca3"
+ integrity sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==
agent-base@6:
version "6.0.2"
@@ -7323,6 +7343,11 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
+eslint-visitor-keys@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz#1f785cc5e81eb7534523d85922248232077d2f8c"
+ integrity sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==
+
eslint@8.57.1:
version "8.57.1"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9"
@@ -7367,6 +7392,15 @@ eslint@8.57.1:
strip-ansi "^6.0.1"
text-table "^0.2.0"
+espree@^10.0.1:
+ version "10.2.0"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-10.2.0.tgz#f4bcead9e05b0615c968e85f83816bc386a45df6"
+ integrity sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==
+ dependencies:
+ acorn "^8.12.0"
+ acorn-jsx "^5.3.2"
+ eslint-visitor-keys "^4.1.0"
+
espree@^9.3.1, espree@^9.6.0, espree@^9.6.1:
version "9.6.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
@@ -8181,6 +8215,11 @@ globals@^13.19.0, globals@^13.24.0:
dependencies:
type-fest "^0.20.2"
+globals@^14.0.0:
+ version "14.0.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
+ integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
+
globals@^15.7.0:
version "15.9.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-15.9.0.tgz#e9de01771091ffbc37db5714dab484f9f69ff399"
@@ -13554,7 +13593,7 @@ source-map-resolve@^0.5.0:
source-map-url "^0.4.0"
urix "^0.1.0"
-source-map-support@0.5.13:
+source-map-support@0.5.13, source-map-support@~0.5.12:
version "0.5.13"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932"
integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==
@@ -13562,14 +13601,6 @@ source-map-support@0.5.13:
buffer-from "^1.0.0"
source-map "^0.6.0"
-source-map-support@~0.5.12:
- version "0.5.21"
- resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
- integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
- dependencies:
- buffer-from "^1.0.0"
- source-map "^0.6.0"
-
source-map-url@^0.4.0:
version "0.4.1"
resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.1.tgz#0af66605a745a5a2f91cf1bbf8a7afbc283dec56"
@@ -13745,7 +13776,16 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
-"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+"string-width-cjs@npm:string-width@^4.2.0":
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
+"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -13798,7 +13838,7 @@ string_decoder@^1.0.0, string_decoder@^1.1.1, string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -13812,6 +13852,13 @@ strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
strip-ansi@^7.0.1, strip-ansi@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@@ -15536,7 +15583,7 @@ worker-loader@^3.0.8:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -15554,6 +15601,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
+wrap-ansi@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+ integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"