Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-10-25 18:15:49 +00:00
parent 836c276094
commit f4f581c6d9
97 changed files with 2093 additions and 955 deletions

View File

@ -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

View File

@ -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

View File

@ -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}"

View File

@ -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'

View File

@ -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'

View File

@ -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

View File

@ -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"},

View File

@ -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)

View File

@ -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"},

View File

@ -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)

View File

@ -1,7 +1,7 @@
<script>
import { GlFormGroup } from '@gitlab/ui';
import { s__ } from '~/locale';
import ChronicDurationInput from '~/vue_shared/components/chronic_duration_input.vue';
import ChronicDurationInput from '~/admin/application_settings/runner_token_expiration/components/chronic_duration_input.vue';
import ExpirationIntervalDescription from './expiration_interval_description.vue';
export default {

View File

@ -1,3 +0,0 @@
rules:
# https://gitlab.com/gitlab-org/gitlab/issues/28716
import/no-cycle: off

View File

@ -1,5 +0,0 @@
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

View File

@ -1,3 +0,0 @@
rules:
# https://gitlab.com/gitlab-org/gitlab/issues/28719
import/no-cycle: off

View File

@ -1,5 +0,0 @@
globals:
AP: readonly
rules:
'@gitlab/require-i18n-strings': off
'@gitlab/vue-require-i18n-strings': off

View File

@ -199,7 +199,7 @@ export default {
}
eventHub.$on('notesApp.updateIssuableConfidentiality', this.setConfidentiality);
Mousetrap.bind(keysFor(ISSUABLE_COMMENT_OR_REPLY), this.quoteReply);
Mousetrap.bind(keysFor(ISSUABLE_COMMENT_OR_REPLY), (e) => this.quoteReply(e));
},
updated() {
this.$nextTick(() => {
@ -233,9 +233,15 @@ export default {
const node = el.nodeType === Node.TEXT_NODE ? el.parentNode : el;
return node.closest('.js-noteable-discussion');
},
async quoteReply() {
async quoteReply(e) {
const discussionEl = this.getDiscussionInSelection();
const text = await CopyAsGFM.selectionToGfm();
// Prevent 'r' being written.
if (e && typeof e.preventDefault === 'function') {
e.preventDefault();
}
if (!discussionEl) {
this.replyInMainEditor(text);
} else {

View File

@ -0,0 +1,90 @@
<script>
import { GlAlert, GlButton } from '@gitlab/ui';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import { s__, sprintf } from '~/locale';
export default {
name: 'PackageErrorsCount',
components: {
GlAlert,
GlButton,
},
props: {
errorPackages: {
type: Array,
required: false,
default: () => [],
},
},
computed: {
errorTitleAlert() {
if (this.singleErrorPackage) {
return sprintf(s__('PackageRegistry|There was an error publishing %{packageName}'), {
packageName: this.singleErrorPackage.name,
});
}
return sprintf(s__('PackageRegistry|There was an error publishing %{count} packages'), {
count: this.errorPackages.length,
});
},
errorMessageBodyAlert() {
if (this.singleErrorPackage) {
return this.singleErrorPackage.statusMessage || this.$options.i18n.errorMessageBodyAlert;
}
return sprintf(
s__(
'PackageRegistry|Failed to publish %{count} packages. Delete these packages and try again.',
),
{
count: this.errorPackages.length,
},
);
},
singleErrorPackage() {
if (this.errorPackages.length === 1) {
const [errorPackage] = this.errorPackages;
return errorPackage;
}
return null;
},
showErrorPackageAlert() {
return this.errorPackages.length > 0;
},
errorPackagesHref() {
// For reactivity we depend on showErrorPackageAlert so we update accordingly
if (!this.showErrorPackageAlert) {
return '';
}
const pageParams = { after: null, before: null };
return mergeUrlParams({ status: 'error', ...pageParams }, window.location.href);
},
},
methods: {
handleClick() {
this.$emit('confirm-delete', [this.singleErrorPackage]);
},
},
i18n: {
errorMessageBodyAlert: s__(
'PackageRegistry|There was a timeout and the package was not published. Delete this package and try again.',
),
},
};
</script>
<template>
<gl-alert v-if="showErrorPackageAlert" class="gl-mt-5" variant="danger" :title="errorTitleAlert">
{{ errorMessageBodyAlert }}
<template #actions>
<gl-button v-if="singleErrorPackage" variant="confirm" @click="handleClick">{{
s__('PackageRegistry|Delete this package')
}}</gl-button>
<gl-button v-else :href="errorPackagesHref" variant="confirm">{{
s__('PackageRegistry|Show packages with errors')
}}</gl-button>
</template>
</gl-alert>
</template>

View File

@ -1,11 +1,10 @@
<script>
import { GlAlert, GlButton } from '@gitlab/ui';
import { s__, sprintf, n__ } from '~/locale';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import { n__ } from '~/locale';
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 RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
import PackageErrorsCount from '~/packages_and_registries/package_registry/components/list/package_errors_count.vue';
import {
DELETE_PACKAGE_TRACKING_ACTION,
DELETE_PACKAGES_TRACKING_ACTION,
@ -30,9 +29,8 @@ const forwardingFieldToPackageTypeMapping = {
export default {
name: 'PackagesList',
components: {
GlAlert,
GlButton,
DeleteModal,
PackageErrorsCount,
PackagesListLoader,
PackagesListRow,
RegistryList,
@ -90,51 +88,6 @@ export default {
category,
};
},
errorTitleAlert() {
if (this.singleErrorPackage) {
return sprintf(
s__('PackageRegistry|There was an error publishing a %{packageName} package'),
{ packageName: this.singleErrorPackage.name },
);
}
return sprintf(s__('PackageRegistry|There was an error publishing %{count} packages'), {
count: this.errorPackages.length,
});
},
errorMessageBodyAlert() {
if (this.singleErrorPackage) {
return this.singleErrorPackage.statusMessage || this.$options.i18n.errorMessageBodyAlert;
}
return sprintf(
s__(
'PackageRegistry|%{count} packages were not published to the registry. Remove these packages and try again.',
),
{
count: this.errorPackages.length,
},
);
},
singleErrorPackage() {
if (this.errorPackages.length === 1) {
const [errorPackage] = this.errorPackages;
return errorPackage;
}
return null;
},
showErrorPackageAlert() {
return this.errorPackages.length > 0 && !this.hideErrorAlert;
},
errorPackagesHref() {
// For reactivity we depend on showErrorPackageAlert so we update accordingly
if (!this.showErrorPackageAlert) {
return '';
}
const pageParams = { after: null, before: null };
return mergeUrlParams({ status: 'error', ...pageParams }, window.location.href);
},
packageTypesWithForwardingEnabled() {
return Object.keys(this.groupSettings)
.filter((field) => this.groupSettings[field])
@ -183,15 +136,6 @@ export default {
}
this.itemsToBeDeleted = [];
},
showConfirmationModal() {
this.setItemsToBeDeleted([this.singleErrorPackage]);
},
},
i18n: {
errorMessageBodyAlert: s__(
'PackageRegistry|There was a timeout and the package was not published. Delete this package and try again.',
),
deleteThisPackage: s__('PackageRegistry|Delete this package'),
},
};
</script>
@ -205,22 +149,11 @@ export default {
</div>
<template v-else>
<gl-alert
v-if="showErrorPackageAlert"
class="gl-mt-5"
variant="danger"
:title="errorTitleAlert"
>
{{ errorMessageBodyAlert }}
<template #actions>
<gl-button v-if="singleErrorPackage" variant="confirm" @click="showConfirmationModal">{{
$options.i18n.deleteThisPackage
}}</gl-button>
<gl-button v-else :href="errorPackagesHref" variant="confirm">{{
s__('PackageRegistry|Show packages with errors')
}}</gl-button>
</template>
</gl-alert>
<package-errors-count
v-if="!hideErrorAlert"
:error-packages="errorPackages"
@confirm-delete="setItemsToBeDeleted"
/>
<registry-list
data-testid="packages-table"
:hidden-delete="!canDeletePackages"

View File

@ -236,9 +236,6 @@ export default {
displayWikiSpecificMarkdownHelp() {
return !this.isContentEditorActive;
},
disableSubmitButton() {
return !this.title;
},
drawioEnabled() {
return typeof this.drawioUrl === 'string' && this.drawioUrl.length > 0;
},
@ -557,7 +554,6 @@ export default {
variant="confirm"
type="submit"
data-testid="wiki-submit-button"
:disabled="disableSubmitButton"
>{{ submitButtonText }}</gl-button
>
<gl-button

View File

@ -1,36 +0,0 @@
<script>
/**
* 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);
},
},
};
</script>
<template>
<component :is="tagName">
<template v-for="slotName in allSlotNames">
<slot v-if="activeSlotNames.includes(slotName)" :name="slotName"></slot>
</template>
</component>
</template>

View File

@ -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,
},
});
}
},
},
};
</script>
@ -171,10 +211,10 @@ export default {
/>
</span>
<gl-link
:href="childItem.webUrl"
:href="childItemWebUrl"
:class="{ '!gl-text-secondary': !isChildItemOpen }"
class="gl-hyphens-auto gl-break-words gl-font-semibold"
@click.exact="$emit('click', $event)"
@click.exact="handleTitleClick"
@mouseover="$emit('mouseover')"
@mouseout="$emit('mouseout')"
>

View File

@ -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"

View File

@ -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,

View File

@ -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 {

View File

@ -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',
);

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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: {

View File

@ -44,7 +44,12 @@
"type": "number"
}
},
"additionalProperties": false
"additionalProperties": {
"type": [
"string",
"number"
]
}
}
},
"additionalProperties": false

View File

@ -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

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

View File

@ -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.
![Diagram of a high-level overview of the GitLab Dedicated architecture.](img/high_level_architecture_diagram_v17_0.png)
![Diagram of a high-level overview of the GitLab Dedicated architecture.](img/high_level_architecture_diagram_v18_0.png)
When managing GitLab Dedicated tenant instances:

View File

@ -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).

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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.

View File

@ -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 <kbd>Enter</kbd> 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.
![filter issues by specific ID](img/issue_search_by_id_v15_0.png)

View File

@ -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.
![burndown chart](img/burndown_and_burnup_charts_v15_3.png)
![burndown and burnup chart](img/burndown_and_burnup_charts_v15_3.png)
#### 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.
![Project milestone page](img/milestones_project_milestone_page_sidebar_v13_11.png)
@ -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

683
eslint.config.mjs Normal file
View File

@ -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',
},
},
];

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 **

View File

@ -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."

View File

@ -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",

View File

@ -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

View File

@ -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 [<String, Integer>] cpu_r
# @param [String] memory_r
# @param [<String, Integer>] 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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)), '../../');

View File

@ -31,7 +31,7 @@ async function getCurrentTopLevelPatterns() {
return serializeAliasedDependencyPatterns(dependencies)
.concat(serializeAliasedDependencyPatterns(devDependencies))
.filter(isAliasedDependency);
.filter(dep => isAliasedDependency(dep));
} catch {
return [];
}

View File

@ -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

View File

@ -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;
});
}

View File

@ -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.

View File

@ -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);
}

View File

@ -1,7 +0,0 @@
---
extends:
- 'plugin:@gitlab/jest'
settings:
import/core-modules:
- '@pact-foundation/pact'
- jest-pact

View File

@ -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}/**/*') +

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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']

View File

@ -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);

View File

@ -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`,
);
});
});
});
});

View File

@ -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);
});
});
});

View File

@ -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

View File

@ -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: '<a>AGP</a>',
second: '<p>PCI</p>',
};
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));
});
});

View File

@ -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', () => {

View File

@ -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,

View File

@ -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

View File

@ -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] }

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)")

View File

@ -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: ':')

View File

@ -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

View File

@ -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"