From 6c44b676312eb6cdffadef45f9ca3e29a8cc92ab Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 21 Jul 2023 12:08:33 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../ui/pipeline_editor_empty_state.vue | 1 + .../diffs/utils/tree_worker_utils.js | 3 +- .../javascripts/lib/utils/text_utility.js | 30 -- .../components/widget/widget.vue | 21 +- app/services/projects/transfer_service.rb | 4 +- .../_subgroup_creation_level.html.haml | 3 +- .../ci_interpolation_inputs_refactor.yml | 8 + config/webpack.config.js | 5 + ...eline_variables_pipeline_id_is_finished.rb | 23 ++ ...ex_for_ci_pipline_variables_pipeline_id.rb | 14 + db/schema_migrations/20230701043315 | 1 + db/schema_migrations/20230702053002 | 1 + doc/api/packages/pypi.md | 2 +- .../modular_monolith/proof_of_concepts.md | 2 +- doc/ci/components/index.md | 2 +- .../end_to_end_testing_webdriverio/index.md | 2 +- doc/development/advanced_search.md | 31 +- .../deprecation_guidelines/index.md | 2 +- .../documentation/topic_types/index.md | 2 +- doc/development/gems.md | 6 +- doc/development/geo.md | 2 +- doc/development/integrations/secure.md | 2 +- .../snowplow/troubleshooting.md | 2 +- doc/subscriptions/choosing_subscription.md | 2 +- .../application_security/dast/proxy-based.md | 2 +- .../vulnerabilities/severities.md | 2 +- doc/user/group/index.md | 18 + doc/user/profile/index.md | 11 +- doc/user/project/working_with_projects.md | 10 +- jest.config.base.js | 1 + lib/gitlab/ci/config/yaml/interpolator.rb | 6 +- lib/gitlab/ci/interpolation/inputs.rb | 70 ++++ .../ci/interpolation/inputs/base_input.rb | 92 +++++ .../ci/interpolation/inputs/string_input.rb | 31 ++ locale/gitlab.pot | 3 + package.json | 2 +- ...ne_editor_can_create_merge_request_spec.rb | 7 - spec/features/projects/ci/editor_spec.rb | 68 +++- .../diffs/utils/tree_worker_utils_spec.js | 2 +- spec/frontend/lib/utils/text_utility_spec.js | 30 -- .../components/widget/widget_spec.js | 48 +-- .../ci/config/yaml/interpolator_spec.rb | 327 ++++++++++-------- .../interpolation/inputs/base_input_spec.rb | 27 ++ .../gitlab/ci/interpolation/inputs_spec.rb | 97 ++++++ storybook/config/webpack.config.js | 4 + yarn.lock | 8 +- 46 files changed, 709 insertions(+), 328 deletions(-) create mode 100644 config/feature_flags/development/ci_interpolation_inputs_refactor.yml create mode 100644 db/post_migrate/20230701043315_ensure_backfill_for_ci_pipeline_variables_pipeline_id_is_finished.rb create mode 100644 db/post_migrate/20230702053002_create_async_index_for_ci_pipline_variables_pipeline_id.rb create mode 100644 db/schema_migrations/20230701043315 create mode 100644 db/schema_migrations/20230702053002 create mode 100644 lib/gitlab/ci/interpolation/inputs.rb create mode 100644 lib/gitlab/ci/interpolation/inputs/base_input.rb create mode 100644 lib/gitlab/ci/interpolation/inputs/string_input.rb create mode 100644 spec/lib/gitlab/ci/interpolation/inputs/base_input_spec.rb create mode 100644 spec/lib/gitlab/ci/interpolation/inputs_spec.rb diff --git a/app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue b/app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue index d7b8e7151d9..25e4e99bf54 100644 --- a/app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue +++ b/app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue @@ -61,6 +61,7 @@ export default { diff --git a/app/assets/javascripts/diffs/utils/tree_worker_utils.js b/app/assets/javascripts/diffs/utils/tree_worker_utils.js index 01a475f48d1..e1e3495a51f 100644 --- a/app/assets/javascripts/diffs/utils/tree_worker_utils.js +++ b/app/assets/javascripts/diffs/utils/tree_worker_utils.js @@ -1,4 +1,3 @@ -import { truncatePathMiddleToLength } from '~/lib/utils/text_utility'; import { TREE_TYPE } from '../constants'; export const getLowestSingleFolder = (folder) => { @@ -28,7 +27,7 @@ export const getLowestSingleFolder = (folder) => { const { path, tree } = getFolder(folder, [folder.name]); return { - path: truncatePathMiddleToLength(path.join('/'), 40), + path: path.join('/'), treeAcc: tree.length ? tree[tree.length - 1].tree : null, }; }; diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 42f481261a2..31e16f7b4db 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -167,36 +167,6 @@ export const truncateWidth = (string, options = {}) => { */ export const truncateSha = (sha) => sha.substring(0, 8); -const ELLIPSIS_CHAR = '…'; -export const truncatePathMiddleToLength = (text, maxWidth) => { - let returnText = text; - let ellipsisCount = 0; - - while (returnText.length >= maxWidth) { - const textSplit = returnText.split('/').filter((s) => s !== ELLIPSIS_CHAR); - - if (textSplit.length === 0) { - // There are n - 1 path separators for n segments, so 2n - 1 <= maxWidth - const maxSegments = Math.floor((maxWidth + 1) / 2); - return new Array(maxSegments).fill(ELLIPSIS_CHAR).join('/'); - } - - const middleIndex = Math.floor(textSplit.length / 2); - - returnText = textSplit - .slice(0, middleIndex) - .concat( - new Array(ellipsisCount + 1).fill().map(() => ELLIPSIS_CHAR), - textSplit.slice(middleIndex + 1), - ) - .join('/'); - - ellipsisCount += 1; - } - - return returnText; -}; - /** * Capitalizes first character * diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue index 123fc8d9096..5bfe903af96 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue @@ -16,8 +16,6 @@ import DynamicContent from './dynamic_content.vue'; import StatusIcon from './status_icon.vue'; import ActionButtons from './action_buttons.vue'; -const FETCH_TYPE_COLLAPSED = 'collapsed'; -const FETCH_TYPE_EXPANDED = 'expanded'; const WIDGET_PREFIX = 'Widget'; const MISSING_RESPONSE_HEADERS = 'MR Widget: raesponse object should contain status and headers object. Make sure to include that in your `fetchCollapsedData` and `fetchExpandedData` functions.'; @@ -49,15 +47,6 @@ export default { SafeHtml, }, props: { - /** - * @param {value.collapsed} Object - * @param {value.expanded} Object - */ - value: { - type: Object, - required: false, - default: () => ({}), - }, loadingText: { type: String, required: false, @@ -238,7 +227,7 @@ export default { try { if (this.fetchCollapsedData) { - await this.fetch(this.fetchCollapsedData, FETCH_TYPE_COLLAPSED); + await this.fetch(this.fetchCollapsedData); } } catch { this.summaryError = this.errorText; @@ -271,7 +260,7 @@ export default { this.contentError = null; try { - await this.fetch(this.fetchExpandedData, FETCH_TYPE_EXPANDED); + await this.fetch(this.fetchExpandedData); } catch { this.contentError = this.errorText; @@ -282,7 +271,7 @@ export default { this.isLoadingExpandedContent = false; }, - fetch(handler, dataType) { + fetch(handler) { const requests = this.multiPolling ? handler() : [handler]; const promises = requests.map((request) => { @@ -319,9 +308,7 @@ export default { }); }); - return Promise.all(promises).then((data) => { - this.$emit('input', { ...this.value, [dataType]: this.multiPolling ? data : data[0] }); - }); + return Promise.all(promises); }, }, failedStatusIcon: EXTENSION_ICONS.failed, diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 4a9d96d266c..1bae7bde168 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -110,7 +110,7 @@ module Projects update_pending_builds - post_update_hooks(project) + post_update_hooks(project, @old_group) rescue Exception # rubocop:disable Lint/RescueException rollback_side_effects raise @@ -119,7 +119,7 @@ module Projects end # Overridden in EE - def post_update_hooks(project) + def post_update_hooks(project, _old_group) ensure_personal_project_owner_membership(project) invalidate_personal_projects_counts diff --git a/app/views/groups/settings/_subgroup_creation_level.html.haml b/app/views/groups/settings/_subgroup_creation_level.html.haml index d92610367ae..9f0a206312e 100644 --- a/app/views/groups/settings/_subgroup_creation_level.html.haml +++ b/app/views/groups/settings/_subgroup_creation_level.html.haml @@ -1,3 +1,4 @@ .form-group = f.label s_('SubgroupCreationLevel|Roles allowed to create subgroups'), class: 'label-bold' - = f.select :subgroup_creation_level, options_for_select(::Gitlab::Access.subgroup_creation_options, group.subgroup_creation_level), {}, class: 'form-control' + - ::Gitlab::Access.subgroup_creation_options.each do |label, action| + = f.gitlab_ui_radio_component :subgroup_creation_level, action, label diff --git a/config/feature_flags/development/ci_interpolation_inputs_refactor.yml b/config/feature_flags/development/ci_interpolation_inputs_refactor.yml new file mode 100644 index 00000000000..657af12c367 --- /dev/null +++ b/config/feature_flags/development/ci_interpolation_inputs_refactor.yml @@ -0,0 +1,8 @@ +--- +name: ci_interpolation_inputs_refactor +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125632 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/418198 +milestone: '16.3' +type: development +group: group::pipeline authoring +default_enabled: false diff --git a/config/webpack.config.js b/config/webpack.config.js index 39dc08d15c6..3bcdc8245f5 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -371,6 +371,11 @@ module.exports = { include: /node_modules/, loader: 'babel-loader', }, + { + test: /gridstack\/.*\.js$/, + include: /node_modules/, + loader: 'babel-loader', + }, { test: /_worker\.js$/, resourceQuery: /worker/, diff --git a/db/post_migrate/20230701043315_ensure_backfill_for_ci_pipeline_variables_pipeline_id_is_finished.rb b/db/post_migrate/20230701043315_ensure_backfill_for_ci_pipeline_variables_pipeline_id_is_finished.rb new file mode 100644 index 00000000000..723b074a898 --- /dev/null +++ b/db/post_migrate/20230701043315_ensure_backfill_for_ci_pipeline_variables_pipeline_id_is_finished.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class EnsureBackfillForCiPipelineVariablesPipelineIdIsFinished < Gitlab::Database::Migration[2.1] + include Gitlab::Database::MigrationHelpers::ConvertToBigint + + restrict_gitlab_migration gitlab_schema: :gitlab_ci + disable_ddl_transaction! + + TABLE_NAME = :ci_pipeline_variables + + def up + ensure_batched_background_migration_is_finished( + job_class_name: 'CopyColumnUsingBackgroundMigrationJob', + table_name: TABLE_NAME, + column_name: 'pipeline_id', + job_arguments: [['pipeline_id'], ['id_convert_to_bigint']] + ) + end + + def down + # no-op + end +end diff --git a/db/post_migrate/20230702053002_create_async_index_for_ci_pipline_variables_pipeline_id.rb b/db/post_migrate/20230702053002_create_async_index_for_ci_pipline_variables_pipeline_id.rb new file mode 100644 index 00000000000..0919dceed7e --- /dev/null +++ b/db/post_migrate/20230702053002_create_async_index_for_ci_pipline_variables_pipeline_id.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class CreateAsyncIndexForCiPiplineVariablesPipelineId < Gitlab::Database::Migration[2.1] + TABLE_NAME = :ci_pipeline_variables + INDEX_NAME = "index_ci_pipeline_variables_on_pipeline_id_bigint_and_key" + + def up + prepare_async_index TABLE_NAME, [:pipeline_id_convert_to_bigint, :key], unique: true, name: INDEX_NAME + end + + def down + unprepare_async_index TABLE_NAME, [:pipeline_id_convert_to_bigint, :key], unique: true, name: INDEX_NAME + end +end diff --git a/db/schema_migrations/20230701043315 b/db/schema_migrations/20230701043315 new file mode 100644 index 00000000000..15a1fd9cbfe --- /dev/null +++ b/db/schema_migrations/20230701043315 @@ -0,0 +1 @@ +e2a7487d4da68819a1bdde6626b32ef8399a917a75d4cbc8ecf2fa140ba1ff16 \ No newline at end of file diff --git a/db/schema_migrations/20230702053002 b/db/schema_migrations/20230702053002 new file mode 100644 index 00000000000..a2eb709cd56 --- /dev/null +++ b/db/schema_migrations/20230702053002 @@ -0,0 +1 @@ +54ac5a22e121379b1ffcefc7b4c1f26cadd15a9b0cabfd0d9e3cba3886777d46 \ No newline at end of file diff --git a/doc/api/packages/pypi.md b/doc/api/packages/pypi.md index 1b155feb8b5..4ac83311839 100644 --- a/doc/api/packages/pypi.md +++ b/doc/api/packages/pypi.md @@ -28,7 +28,7 @@ is recommended when [FIPS mode](../../development/fips_compliance.md) is enabled > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/225545) in GitLab 13.12. -Download a PyPI package file. The [simple API](#group-level-simple-api-entry-point) +Download a PyPI package file. The [simple API](#group-level-simple-api-entry-point) usually supplies this URL. ```plaintext diff --git a/doc/architecture/blueprints/modular_monolith/proof_of_concepts.md b/doc/architecture/blueprints/modular_monolith/proof_of_concepts.md index c215ffafbe4..7169e12a7b5 100644 --- a/doc/architecture/blueprints/modular_monolith/proof_of_concepts.md +++ b/doc/architecture/blueprints/modular_monolith/proof_of_concepts.md @@ -36,7 +36,7 @@ gRPC would carry messages between modules. ## Use Packwerk to enforce module boundaries Packwerk is a static analyzer that helps defining and enforcing module boundaries -in Ruby. +in Ruby. [In this PoC merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/98801) we demonstrate a possible directory structure of the monolith broken down into separate diff --git a/doc/ci/components/index.md b/doc/ci/components/index.md index ae35b3779c3..77de209dc50 100644 --- a/doc/ci/components/index.md +++ b/doc/ci/components/index.md @@ -367,7 +367,7 @@ This action is not reversible. Any existing CI template, that you share with other projects via `include:` syntax, can be converted to a CI component. -1. Decide whether you want the component to be part of an existing [components repository](#components-repository), +1. Decide whether you want the component to be part of an existing [components repository](#components-repository), if you want to logically group components together. Create and setup a [components repository](#components-repository) otherwise. 1. Create a YAML file in the components repository according to the expected [directory structure](#directory-structure). 1. Copy the content of the template YAML file into the new component YAML file. diff --git a/doc/ci/examples/end_to_end_testing_webdriverio/index.md b/doc/ci/examples/end_to_end_testing_webdriverio/index.md index ee8b6bb30b6..eb956f66dd7 100644 --- a/doc/ci/examples/end_to_end_testing_webdriverio/index.md +++ b/doc/ci/examples/end_to_end_testing_webdriverio/index.md @@ -154,7 +154,7 @@ to interact with your application, so we need to install and run them. Furthermore, WebdriverIO uses Selenium as a common interface to control different browsers, so we need to install and run Selenium as well. Luckily, the Selenium project provides the Docker images for Firefox [standalone-firefox](https://hub.docker.com/r/selenium/standalone-firefox/) and -and for Chrome [standalone-chrome](https://hub.docker.com/r/selenium/standalone-chrome/). +and for Chrome [standalone-chrome](https://hub.docker.com/r/selenium/standalone-chrome/). (Since Safari and Internet Explorer/Edge are not open source and not available for Linux, we are unfortunately unable to use those in GitLab CI/CD). diff --git a/doc/development/advanced_search.md b/doc/development/advanced_search.md index 30e1874f1ed..805459cb4ee 100644 --- a/doc/development/advanced_search.md +++ b/doc/development/advanced_search.md @@ -52,9 +52,9 @@ during indexing and searching operations. Some of the benefits and tradeoffs to - Routing is not used if too many shards would be hit for global and group scoped searches. - Shard size imbalance might occur. -## Existing Analyzers/Tokenizers/Filters +## Existing analyzers and tokenizers -These are all defined in [`ee/lib/elastic/latest/config.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/elastic/latest/config.rb) +The following analyzers and tokenizers are defined in [`ee/lib/elastic/latest/config.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/elastic/latest/config.rb). ### Analyzers @@ -72,7 +72,7 @@ Please see the `sha_tokenizer` explanation later below for an example. #### `code_analyzer` -Used when indexing a blob's filename and content. Uses the `whitespace` tokenizer and the filters: [`code`](#code), `lowercase`, and `asciifolding` +Used when indexing a blob's filename and content. Uses the `whitespace` tokenizer and the [`word_delimiter_graph`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-word-delimiter-graph-tokenfilter.html), `lowercase`, and `asciifolding` filters. The `whitespace` tokenizer was selected to have more control over how tokens are split. For example the string `Foo::bar(4)` needs to generate tokens like `Foo` and `bar(4)` to be properly searched. @@ -81,10 +81,6 @@ Please see the `code` filter for an explanation on how tokens are split. NOTE: The [Elasticsearch `code_analyzer` doesn't account for all code cases](../integration/advanced_search/elasticsearch_troubleshooting.md#elasticsearch-code_analyzer-doesnt-account-for-all-code-cases). -#### `code_search_analyzer` - -Not directly used for indexing, but rather used to transform a search input. Uses the `whitespace` tokenizer and the `lowercase` and `asciifolding` filters. - ### Tokenizers #### `sha_tokenizer` @@ -115,27 +111,10 @@ Example: - `'path/application.js'` - `'application.js'` -### Filters - -#### `code` - -Uses a [Pattern Capture token filter](https://www.elastic.co/guide/en/elasticsearch/reference/5.5/analysis-pattern-capture-tokenfilter.html) to split tokens into more easily searched versions of themselves. - -Patterns: - -- `"(\\p{Ll}+|\\p{Lu}\\p{Ll}+|\\p{Lu}+)"`: captures CamelCase and lowerCamelCase strings as separate tokens -- `"(\\d+)"`: extracts digits -- `"(?=([\\p{Lu}]+[\\p{L}]+))"`: captures CamelCase strings recursively. For example: `ThisIsATest` => `[ThisIsATest, IsATest, ATest, Test]` -- `'"((?:\\"|[^"]|\\")*)"'`: captures terms inside quotes, removing the quotes -- `"'((?:\\'|[^']|\\')*)'"`: same as above, for single-quotes -- `'\.([^.]+)(?=\.|\s|\Z)'`: separate terms with periods in-between -- `'([\p{L}_.-]+)'`: some common chars in file names to keep the whole filename intact (for example `my_file-ñame.txt`) -- `'([\p{L}\d_]+)'`: letters, numbers and underscores are the most common tokens in programming. Always capture them greedily regardless of context. - ## Gotchas -- Searches can have their own analyzers. Remember to check when editing analyzers -- `Character` filters (as opposed to token filters) always replace the original character, so they're not a good choice as they can hinder exact searches +- Searches can have their own analyzers. Remember to check when editing analyzers. +- `Character` filters (as opposed to token filters) always replace the original character. These filters can hinder exact searches. ## Zero downtime reindexing with multiple indices diff --git a/doc/development/deprecation_guidelines/index.md b/doc/development/deprecation_guidelines/index.md index af89da5ec65..d586f25ffbf 100644 --- a/doc/development/deprecation_guidelines/index.md +++ b/doc/development/deprecation_guidelines/index.md @@ -45,7 +45,7 @@ For versioning and upgrade details, see our [Release and Maintenance policy](../ GitLab self-managed packages are semantically versioned and follow our [maintenance policy](../../policy/maintenance.md). This process applies to features and APIs that are generally available, not beta or experimental. -This maintenance policy is in place to allow our customers to prepare for disruptive changes by establishing a clear and predictable pattern that is widely used in the software industry. For many of our customers, GitLab is a business-critical application and surprising changes can cause damages and erode trust. +This maintenance policy is in place to allow our customers to prepare for disruptive changes by establishing a clear and predictable pattern that is widely used in the software industry. For many of our customers, GitLab is a business-critical application and surprising changes can cause damages and erode trust. Introducing breaking changes in minor releases is against policy because it can disrupt our customers and introduces an element of randomness that requires customers to check for breaking changes every minor release to ensure that their business is not impacted. This does not align with our goal [to make it as easy as possible for customers to do business with GitLab](https://about.gitlab.com/company/yearlies/#fy24-yearlies) and is strongly discouraged. diff --git a/doc/development/documentation/topic_types/index.md b/doc/development/documentation/topic_types/index.md index 3f055ca7ed7..40039ca5b1a 100644 --- a/doc/development/documentation/topic_types/index.md +++ b/doc/development/documentation/topic_types/index.md @@ -47,7 +47,7 @@ In general, for topic titles: - Follow [capitalization](../styleguide/index.md#topic-titles) guidelines. - Do not repeat text from earlier topic titles. For example, if the page is about merge requests, instead of `Troubleshooting merge requests`, use only `Troubleshooting`. -- Avoid using hyphens to separate information. +- Avoid using hyphens to separate information. For example, instead of `Internal analytics - Architecture`, use `Internal analytics architecture` or `Architecture of internal analytics`. See also [guidelines for heading levels in Markdown](../styleguide/index.md#heading-levels-in-markdown). diff --git a/doc/development/gems.md b/doc/development/gems.md index 57acac7287b..9259e9f7d50 100644 --- a/doc/development/gems.md +++ b/doc/development/gems.md @@ -13,7 +13,7 @@ We extract libraries from our codebase when their functionality is highly isolated and we want to use them in other applications ourselves or we think it would benefit the wider community. -Extracting code to a gem also ensures that the gem does not contain any hidden +Extracting code to a gem also ensures that the gem does not contain any hidden dependencies on our application code. Gems should always be used when implementing functionality that can be considered isolated, @@ -36,7 +36,7 @@ You can always start by creating a new Gem [in the same repo](#in-the-same-repo) to be used by a wider community. WARNING: -To prevent malicious actors from name-squatting the extracted Gems, follow the instructions +To prevent malicious actors from name-squatting the extracted Gems, follow the instructions to [reserve a gem name](#reserve-a-gem-name). ## Advantages of using Gems @@ -81,7 +81,7 @@ and prevents complexity (coordinating changes across repos, new permissions, mul Gems stored in the same repo should be referenced in `Gemfile` with the `path:` syntax. WARNING: -To prevent malicious actors from name-squatting the extracted Gems, follow the instructions +To prevent malicious actors from name-squatting the extracted Gems, follow the instructions to [reserve a gem name](#reserve-a-gem-name). ### Create and use a new Gem diff --git a/doc/development/geo.md b/doc/development/geo.md index a39f97f1241..e8eb19b1572 100644 --- a/doc/development/geo.md +++ b/doc/development/geo.md @@ -680,7 +680,7 @@ on, check out our [self-service framework](geo/framework.md). After triggering a successful [e2e:package-and-test-ee](testing_guide/end_to_end/index.md#using-the-package-and-test-job) pipeline, you can manually trigger a job named `GET:Geo`: -1. In the [GitLab project](https://gitlab.com/gitlab-org/gitlab), select the **Pipelines** tab of a merge request. +1. In the [GitLab project](https://gitlab.com/gitlab-org/gitlab), select the **Pipelines** tab of a merge request. 1. Select the `Stage: qa` stage on the latest pipeline to expand and list all the related jobs. 1. Select `trigger-omnibus` to view the [Omnibus GitLab Mirror](https://gitlab.com/gitlab-org/build/omnibus-gitlab-mirror) pipeline corresponding to the merge request. 1. The `GET:Geo` job can be found and triggered under the `trigger-qa` stage. diff --git a/doc/development/integrations/secure.md b/doc/development/integrations/secure.md index 09778127050..fa8392b5580 100644 --- a/doc/development/integrations/secure.md +++ b/doc/development/integrations/secure.md @@ -577,7 +577,7 @@ All other attributes are optional. ##### SAST -The `location` of a SAST vulnerability must have a `file` that gives the path of the affected file and +The `location` of a SAST vulnerability must have a `file` that gives the path of the affected file and a `start_line` field with the affected line number. It may also have an `end_line`, a `class`, and a `method`. diff --git a/doc/development/internal_analytics/snowplow/troubleshooting.md b/doc/development/internal_analytics/snowplow/troubleshooting.md index 2f59543e0f4..c085ceb0f56 100644 --- a/doc/development/internal_analytics/snowplow/troubleshooting.md +++ b/doc/development/internal_analytics/snowplow/troubleshooting.md @@ -42,7 +42,7 @@ or even a result of a public holiday in some regions of the world with a larger 1. Check (via [Grafana explore tab](https://dashboards.gitlab.net/explore) ) following Prometheus counters `gitlab_snowplow_events_total`, `gitlab_snowplow_failed_events_total` and `gitlab_snowplow_successful_events_total` to see how many events were fired correctly from GitLab.com. Example query to use `sum(rate(gitlab_snowplow_successful_events_total{env="gprd"}[5m])) / sum(rate(gitlab_snowplow_events_total{env="gprd"}[5m]))` would chart rate at which number of good events rose in comparison to events sent in total. If number drops from 1 it means that problem might be in communication between GitLab and AWS collectors fleet. 1. Check [logs in Kibana](https://log.gprd.gitlab.net/app/discover#) and filter with `{ "query": { "match_phrase": { "json.message": "failed to be reported to collector at" } } }` if there are some failed events logged -We conducted an investigation into an unexpected drop in snowplow events volume. +We conducted an investigation into an unexpected drop in snowplow events volume. GitLab team members can view more information in this confidential issue: `https://gitlab.com/gitlab-org/gitlab/-/issues/335206` diff --git a/doc/subscriptions/choosing_subscription.md b/doc/subscriptions/choosing_subscription.md index ccb68792531..ad42eecc9c1 100644 --- a/doc/subscriptions/choosing_subscription.md +++ b/doc/subscriptions/choosing_subscription.md @@ -30,7 +30,7 @@ A new subscription must be purchased and applied as needed. ## Choose a GitLab tier Pricing is [tier-based](https://about.gitlab.com/pricing/), allowing you to choose -the features that fit your budget. +the features that fit your budget. For more details, see [a comparison of self-managed features available in each tier](https://about.gitlab.com/pricing/feature-comparison/). diff --git a/doc/user/application_security/dast/proxy-based.md b/doc/user/application_security/dast/proxy-based.md index 0eec04bfeff..6d36abf25cf 100644 --- a/doc/user/application_security/dast/proxy-based.md +++ b/doc/user/application_security/dast/proxy-based.md @@ -501,7 +501,7 @@ To run an existing on-demand scan: 1. In the scan's row, select **Run scan**. If the branch saved in the scan no longer exists, you must: - + 1. [Edit the scan](#edit-an-on-demand-scan). 1. Select a new branch. 1. Save the edited scan. diff --git a/doc/user/application_security/vulnerabilities/severities.md b/doc/user/application_security/vulnerabilities/severities.md index 9553d15bbe9..605fef39090 100644 --- a/doc/user/application_security/vulnerabilities/severities.md +++ b/doc/user/application_security/vulnerabilities/severities.md @@ -22,7 +22,7 @@ GitLab analyzers make an effort to fit the severity descriptions below, but they ## Critical severity -Vulnerabilities identified at the Critical Severity level should be investigated immediately. Vulnerabilities at this level assume exploitation of the flaw could lead to full system or data compromise. Examples of critical severity flaws are Command/Code Injection and SQL Injection. Typically these flaws are rated with CVSS 3.1 between 9.0-10.0. +Vulnerabilities identified at the Critical Severity level should be investigated immediately. Vulnerabilities at this level assume exploitation of the flaw could lead to full system or data compromise. Examples of critical severity flaws are Command/Code Injection and SQL Injection. Typically these flaws are rated with CVSS 3.1 between 9.0-10.0. ## High severity diff --git a/doc/user/group/index.md b/doc/user/group/index.md index 7ff9648e41e..654fe928cd4 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -63,6 +63,24 @@ This page shows groups that you are a member of: - Through membership of a subgroup's parent group. - Through direct or inherited membership of a project in the group or subgroup. +## View group activity + +To view the activity of a project: + +1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group. +1. Select **Manage > Activity**. +1. Optional. To filter activity by contribution type, select a tab: + + - **All**: All contributions by group members in the group and group's projects. + - **Push events**: Push events in the group's projects. + - **Merge events**: Accepted merge requests in the group's projects. + - **Issue events**: Issues opened and closed in the group's projects. + - **Epic events**: Epics opened and closed in the group. + - **Comments**: Comments posted by group members in the group's projects. + - **Wiki**: Updates to wiki pages in the group. + - **Designs**: Designs added, updated, and removed in the group's projects. + - **Team**: Group members who joined and left the group's projects. + ## Create a group To create a group: diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md index fa83f0dab05..02653591da5 100644 --- a/doc/user/profile/index.md +++ b/doc/user/profile/index.md @@ -354,7 +354,16 @@ To view your activity: 1. On the left sidebar, expand the top-most chevron (**{chevron-down}**). 1. Select **Your work**. 1. Select **Activity**. -1. Optional. To filter your activity by contribution type, in the **Your Activity** tab, select a tab. +1. Optional. To filter your activity by contribution type, in the **Your Activity** tab, select a tab: + + - **All**: All contributions you made in your groups and projects. + - **Push events**: Push events you made in your projects. + - **Merge events**: Merge requests you accepted in your projects. + - **Issue events**: Issues you opened and closed in your projects. + - **Comments**: Comments you posted in your projects. + - **Wiki**: Wiki pages you created and updated in your projects. + - **Designs**: Designs you added, updated, and removed in your projects. + - **Team**: Projects you joined and left. ## Session duration diff --git a/doc/user/project/working_with_projects.md b/doc/user/project/working_with_projects.md index 472bbb81648..fc6e6d8d314 100644 --- a/doc/user/project/working_with_projects.md +++ b/doc/user/project/working_with_projects.md @@ -178,7 +178,15 @@ To view the activity of a project: 1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project. 1. Select **Manage > Activity**. -1. Select a tab to view the type of project activity. +1. Optional. To filter activity by contribution type, select a tab: + + - **All**: All contributions by project members. + - **Push events**: Push events in the project. + - **Merge events**: Accepted merge requests in the project. + - **Issue events**: Issues opened and closed in the project. + - **Comments**: Comments posted by project members. + - **Designs**: Designs added, updated, and removed in the project. + - **Team**: Members who joined and left the project. ## Search in projects diff --git a/jest.config.base.js b/jest.config.base.js index 1907b084fb6..5253286bfa9 100644 --- a/jest.config.base.js +++ b/jest.config.base.js @@ -203,6 +203,7 @@ module.exports = (path, options = {}) => { '@gitlab/favicon-overlay', '@gitlab/cluster-client', 'bootstrap-vue', + 'gridstack', 'three', 'monaco-editor', 'monaco-yaml', diff --git a/lib/gitlab/ci/config/yaml/interpolator.rb b/lib/gitlab/ci/config/yaml/interpolator.rb index d8c81bf8b98..bb6c3215661 100644 --- a/lib/gitlab/ci/config/yaml/interpolator.rb +++ b/lib/gitlab/ci/config/yaml/interpolator.rb @@ -73,7 +73,11 @@ module Gitlab end def inputs - @inputs ||= Ci::Input::Inputs.new(spec, args) + @inputs ||= if Feature.enabled?(:ci_interpolation_inputs_refactor) + Ci::Interpolation::Inputs.new(spec, args) + else + Ci::Input::Inputs.new(spec, args) + end end def context diff --git a/lib/gitlab/ci/interpolation/inputs.rb b/lib/gitlab/ci/interpolation/inputs.rb new file mode 100644 index 00000000000..910606e6ab2 --- /dev/null +++ b/lib/gitlab/ci/interpolation/inputs.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Interpolation + # Interpolation inputs provided by the user. + class Inputs + UnknownInputTypeError = Class.new(StandardError) + + TYPES = [ + StringInput + ].freeze + + def initialize(specs, args) + @specs = specs.to_h + @args = args.to_h + @inputs = [] + @errors = [] + + validate! + fabricate! + end + + def errors + @errors + @inputs.flat_map(&:errors) + end + + def valid? + errors.none? + end + + def to_hash + @inputs.inject({}) do |hash, input| + hash.merge(input.to_hash) + end + end + + private + + def validate! + unknown_inputs = @args.keys - @specs.keys + return if unknown_inputs.empty? + + @errors.push("unknown input arguments: #{unknown_inputs.join(', ')}") + end + + def fabricate! + @specs.each do |input_name, spec| + input_type = TYPES.find { |klass| klass.matches?(spec) } + + unless input_type + @errors.push( + "unknown input specification for `#{input_name}` (valid types: #{valid_type_names.join(', ')})") + next + end + + @inputs.push(input_type.new( + name: input_name, + spec: spec, + value: @args[input_name])) + end + end + + def valid_type_names + TYPES.map(&:type_name) + end + end + end + end +end diff --git a/lib/gitlab/ci/interpolation/inputs/base_input.rb b/lib/gitlab/ci/interpolation/inputs/base_input.rb new file mode 100644 index 00000000000..20b9bfacb17 --- /dev/null +++ b/lib/gitlab/ci/interpolation/inputs/base_input.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Interpolation + class Inputs + ## + # This is a common abstraction for all input types + class BaseInput + ArgumentNotValidError = Class.new(StandardError) + + # Checks whether the class matches the type in the specification + def self.matches?(spec) + raise NotImplementedError + end + + # Human readable type used in error messages + def self.type_name + raise NotImplementedError + end + + # Checks whether the provided value is of the given type + def valid_value?(value) + raise NotImplementedError + end + + attr_reader :errors, :name, :spec, :value + + def initialize(name:, spec:, value:) + @name = name + @errors = [] + + # Treat minimal spec definition (nil) as a valid hash: + # spec: + # inputs: + # website: + @spec = spec || {} # specification from input definition + @value = value # actual value provided by the user + + validate! + end + + def to_hash + raise ArgumentNotValidError unless valid? + + { name => actual_value } + end + + def valid? + @errors.none? + end + + private + + def validate! + return error('required value has not been provided') if required_input? && value.nil? + + # validate default value + return error("default value is not a #{self.class.type_name}") if !required_input? && !valid_value?(default) + + # validate provided value + error("provided value is not a #{self.class.type_name}") unless valid_value?(value) + end + + def error(message) + @errors.push("`#{name}` input: #{message}") + end + + def actual_value + # nil check is to support boolean values. + value.nil? ? default : value + end + + # An input specification without a default value is required. + # For example: + # ```yaml + # spec: + # inputs: + # website: + # ``` + def required_input? + !spec.key?(:default) + end + + def default + spec[:default] + end + end + end + end + end +end diff --git a/lib/gitlab/ci/interpolation/inputs/string_input.rb b/lib/gitlab/ci/interpolation/inputs/string_input.rb new file mode 100644 index 00000000000..7a042964cbb --- /dev/null +++ b/lib/gitlab/ci/interpolation/inputs/string_input.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Interpolation + class Inputs + class StringInput < BaseInput + def self.matches?(spec) + # The input spec can be `nil` when using a minimal specification + # and also when `type` is not specified. + # + # ```yaml + # spec: + # inputs: + # foo: + # ``` + spec.nil? || (spec.is_a?(Hash) && [nil, type_name].include?(spec[:type])) + end + + def self.type_name + 'string' + end + + def valid_value?(value) + value.nil? || value.is_a?(String) + end + end + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 20a658b155e..3bfce5e0273 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -51441,6 +51441,9 @@ msgstr "" msgid "Vulnerability|Try it out" msgstr "" +msgid "Vulnerability|URL:" +msgstr "" + msgid "Vulnerability|Unmodified Response" msgstr "" diff --git a/package.json b/package.json index ec468be2ba1..92e366217f9 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "gettext-parser": "^6.0.0", "graphql": "^15.7.2", "graphql-tag": "^2.11.0", - "gridstack": "^7.3.0", + "gridstack": "^8.3.0", "highlight.js": "^11.8.0", "immer": "^9.0.15", "ipaddr.js": "^1.9.1", diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_can_create_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_can_create_merge_request_spec.rb index 1f7871b0900..562c9c0c55f 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_can_create_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_can_create_merge_request_spec.rb @@ -27,13 +27,6 @@ module QA Page::Project::PipelineEditor::New.perform(&:create_new_ci) Page::Project::PipelineEditor::Show.perform do |show| - # Editor should display default content when project does not have CI file yet - # New MR checkbox should not be rendered when a new target branch is yet to be provided - aggregate_failures 'check editor default conditions' do - expect(show.editing_content).not_to be_empty - expect(show).to have_no_new_mr_checkbox - end - # The new MR checkbox is visible after a new branch name is set show.set_source_branch(SecureRandom.hex(10)) expect(show).to have_new_mr_checkbox diff --git a/spec/features/projects/ci/editor_spec.rb b/spec/features/projects/ci/editor_spec.rb index 43da57c16d1..b09aa91f4ab 100644 --- a/spec/features/projects/ci/editor_spec.rb +++ b/spec/features/projects/ci/editor_spec.rb @@ -11,6 +11,7 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d let(:default_branch) { 'main' } let(:other_branch) { 'test' } let(:branch_with_invalid_ci) { 'despair' } + let(:branch_without_ci) { 'empty' } let(:default_content) { 'Default' } @@ -45,6 +46,7 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d project.repository.create_file(user, project.ci_config_path_or_default, default_content, message: 'Create CI file for main', branch_name: default_branch) project.repository.create_file(user, project.ci_config_path_or_default, valid_content, message: 'Create CI file for test', branch_name: other_branch) project.repository.create_file(user, project.ci_config_path_or_default, invalid_content, message: 'Create CI file for test', branch_name: branch_with_invalid_ci) + project.repository.create_file(user, 'index.js', "file", message: 'New js file', branch_name: branch_without_ci) visit project_ci_pipeline_editor_path(project) wait_for_requests @@ -62,6 +64,31 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d end end + describe 'when there are no CI config file' do + before do + visit project_ci_pipeline_editor_path(project, branch_name: branch_without_ci) + end + + it 'renders the empty page', :aggregate_failures do + expect(page).to have_content 'Optimize your workflow with CI/CD Pipelines' + expect(page).to have_selector '[data-testid="create_new_ci_button"]' + end + + context 'when clicking on the create new CI button' do + before do + click_button 'Configure pipeline' + end + + it 'renders the source editor with default content', :aggregate_failures do + expect(page).to have_selector('#source-editor-') + + page.within('#source-editor-') do + expect(page).to have_content('This file is a template, and might need editing before it works on your project.') + end + end + end + end + describe 'When CI yml has valid syntax' do before do visit project_ci_pipeline_editor_path(project, branch_name: other_branch) @@ -149,15 +176,6 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d end shared_examples 'default branch switcher behavior' do - def switch_to_branch(branch) - find('[data-testid="branch-selector"]').click - - page.within '[data-testid="branch-selector"]' do - click_button branch - wait_for_requests - end - end - it 'displays current branch' do page.within('[data-testid="branch-selector"]') do expect(page).to have_content(default_branch) @@ -195,12 +213,20 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d end describe 'Branch Switcher' do + def switch_to_branch(branch) + # close button for the popover + find('[data-testid="close-button"]').click + find('[data-testid="branch-selector"]').click + + page.within '[data-testid="branch-selector"]' do + click_button branch + wait_for_requests + end + end + before do visit project_ci_pipeline_editor_path(project) wait_for_requests - - # close button for the popover - find('[data-testid="close-button"]').click end it_behaves_like 'default branch switcher behavior' @@ -262,6 +288,24 @@ RSpec.describe 'Pipeline Editor', :js, feature_category: :pipeline_composition d end describe 'Commit Form' do + context 'when targetting the main branch' do + it 'does not show the option to create a Merge request', :aggregate_failures do + expect(page).not_to have_selector('[data-testid="new-mr-checkbox"]') + expect(page).not_to have_content('Start a new merge request with these changes') + end + end + + context 'when targetting any non-main branch' do + before do + find('#source-branch-field').set('new_branch', clear: :backspace) + end + + it 'shows the option to create a Merge request', :aggregate_failures do + expect(page).to have_selector('[data-testid="new-mr-checkbox"]') + expect(page).to have_content('Start a new merge request with these changes') + end + end + it 'is preserved when changing tabs' do find('#commit-message').set('message', clear: :backspace) find('#source-branch-field').set('new_branch', clear: :backspace) diff --git a/spec/frontend/diffs/utils/tree_worker_utils_spec.js b/spec/frontend/diffs/utils/tree_worker_utils_spec.js index 5fba00f9258..b29275f45a6 100644 --- a/spec/frontend/diffs/utils/tree_worker_utils_spec.js +++ b/spec/frontend/diffs/utils/tree_worker_utils_spec.js @@ -382,7 +382,7 @@ describe('~/diffs/utils/tree_worker_utils', () => { }, { type: 'tree', - name: 'ee/lib/…/…/…/longtreenametomakepath', + name: 'ee/lib/ee/gitlab/checks/longtreenametomakepath', tree: [ { name: 'diff_check.rb', diff --git a/spec/frontend/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js index 8f1f6899935..b7d6bbd3991 100644 --- a/spec/frontend/lib/utils/text_utility_spec.js +++ b/spec/frontend/lib/utils/text_utility_spec.js @@ -238,36 +238,6 @@ describe('text_utility', () => { }); }); - describe('truncatePathMiddleToLength', () => { - it('does not truncate text', () => { - expect(textUtils.truncatePathMiddleToLength('app/test', 50)).toEqual('app/test'); - }); - - it('truncates middle of the path', () => { - expect(textUtils.truncatePathMiddleToLength('app/test/diff', 13)).toEqual('app/…/diff'); - }); - - it('truncates multiple times in the middle of the path', () => { - expect(textUtils.truncatePathMiddleToLength('app/test/merge_request/diff', 13)).toEqual( - 'app/…/…/diff', - ); - }); - - describe('given a path too long for the maxWidth', () => { - it.each` - path | maxWidth | result - ${'aa/bb/cc'} | ${1} | ${'…'} - ${'aa/bb/cc'} | ${2} | ${'…'} - ${'aa/bb/cc'} | ${3} | ${'…/…'} - ${'aa/bb/cc'} | ${4} | ${'…/…'} - ${'aa/bb/cc'} | ${5} | ${'…/…/…'} - `('truncates ($path, $maxWidth) to $result', ({ path, maxWidth, result }) => { - expect(result.length).toBeLessThanOrEqual(maxWidth); - expect(textUtils.truncatePathMiddleToLength(path, maxWidth)).toEqual(result); - }); - }); - }); - describe('slugifyWithUnderscore', () => { it('should replaces whitespaces with underscore and convert to lower case', () => { expect(textUtils.slugifyWithUnderscore('My Input String')).toEqual('my_input_string'); diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js index 9343a3a5e90..18fdba32f52 100644 --- a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js @@ -121,14 +121,15 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => { }); describe('fetch', () => { - it('sets the data.collapsed property after a successfull call - multiPolling: false', async () => { + it('calls fetchCollapsedData properly when multiPolling is false', async () => { const mockData = { headers: {}, status: HTTP_STATUS_OK, data: { vulnerabilities: [] } }; - createComponent({ propsData: { fetchCollapsedData: () => Promise.resolve(mockData) } }); + const fetchCollapsedData = jest.fn().mockResolvedValue(mockData); + createComponent({ propsData: { fetchCollapsedData } }); await waitForPromises(); - expect(wrapper.emitted('input')[0][0]).toEqual({ collapsed: mockData.data, expanded: null }); + expect(fetchCollapsedData).toHaveBeenCalledTimes(1); }); - it('sets the data.collapsed property after a successfull call - multiPolling: true', async () => { + it('calls fetchCollapsedData properly when multiPolling is true', async () => { const mockData1 = { headers: {}, status: HTTP_STATUS_OK, @@ -140,22 +141,22 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => { data: { vulnerabilities: [{ vuln: 2 }] }, }; + const fetchCollapsedData = [ + jest.fn().mockResolvedValue(mockData1), + jest.fn().mockResolvedValue(mockData2), + ]; + createComponent({ propsData: { multiPolling: true, - fetchCollapsedData: () => [ - () => Promise.resolve(mockData1), - () => Promise.resolve(mockData2), - ], + fetchCollapsedData: () => fetchCollapsedData, }, }); await waitForPromises(); - expect(wrapper.emitted('input')[0][0]).toEqual({ - collapsed: [mockData1.data, mockData2.data], - expanded: null, - }); + expect(fetchCollapsedData[0]).toHaveBeenCalledTimes(1); + expect(fetchCollapsedData[1]).toHaveBeenCalledTimes(1); }); it('throws an error when the handler does not include headers or status objects', async () => { @@ -328,11 +329,12 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => { }; const fetchExpandedData = jest.fn().mockResolvedValue(mockDataExpanded); + const fetchCollapsedData = jest.fn().mockResolvedValue(mockDataCollapsed); await createComponent({ propsData: { isCollapsible: true, - fetchCollapsedData: () => Promise.resolve(mockDataCollapsed), + fetchCollapsedData, fetchExpandedData, }, }); @@ -340,17 +342,8 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => { findToggleButton().vm.$emit('click'); await waitForPromises(); - // First fetches the collapsed data - expect(wrapper.emitted('input')[0][0]).toEqual({ - collapsed: mockDataCollapsed.data, - expanded: null, - }); - - // Then fetches the expanded data - expect(wrapper.emitted('input')[1][0]).toEqual({ - collapsed: null, - expanded: mockDataExpanded.data, - }); + expect(fetchCollapsedData).toHaveBeenCalledTimes(1); + expect(fetchExpandedData).toHaveBeenCalledTimes(1); // Triggering a click does not call the expanded data again findToggleButton().vm.$emit('click'); @@ -371,14 +364,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => { findToggleButton().vm.$emit('click'); await waitForPromises(); - // First fetches the collapsed data - expect(wrapper.emitted('input')[0][0]).toEqual({ - collapsed: undefined, - expanded: null, - }); - expect(fetchExpandedData).toHaveBeenCalledTimes(1); - expect(wrapper.emitted('input')).toHaveLength(1); // Should not an emit an input call because request failed findToggleButton().vm.$emit('click'); await waitForPromises(); diff --git a/spec/lib/gitlab/ci/config/yaml/interpolator_spec.rb b/spec/lib/gitlab/ci/config/yaml/interpolator_spec.rb index b744adc570f..1621ff6655d 100644 --- a/spec/lib/gitlab/ci/config/yaml/interpolator_spec.rb +++ b/spec/lib/gitlab/ci/config/yaml/interpolator_spec.rb @@ -9,151 +9,9 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli subject { described_class.new(result, arguments) } - context 'when input data is valid' do - let(:header) do - { spec: { inputs: { website: nil } } } - end - - let(:content) do - { test: 'deploy $[[ inputs.website ]]' } - end - - let(:arguments) do - { website: 'gitlab.com' } - end - - it 'correctly interpolates the config' do - subject.interpolate! - - expect(subject).to be_valid - expect(subject.to_hash).to eq({ test: 'deploy gitlab.com' }) - end - end - - context 'when config has a syntax error' do - let(:result) { ::Gitlab::Ci::Config::Yaml::Result.new(error: 'Invalid configuration format') } - - let(:arguments) do - { website: 'gitlab.com' } - end - - it 'surfaces an error about invalid config' do - subject.interpolate! - - expect(subject).not_to be_valid - expect(subject.error_message).to eq subject.errors.first - expect(subject.errors).to include 'Invalid configuration format' - end - end - - context 'when spec header is invalid' do - let(:header) do - { spec: { arguments: { website: nil } } } - end - - let(:content) do - { test: 'deploy $[[ inputs.website ]]' } - end - - let(:arguments) do - { website: 'gitlab.com' } - end - - it 'surfaces an error about invalid header' do - subject.interpolate! - - expect(subject).not_to be_valid - expect(subject.error_message).to eq subject.errors.first - expect(subject.errors).to include('header:spec config contains unknown keys: arguments') - end - end - - context 'when interpolation block is invalid' do - let(:header) do - { spec: { inputs: { website: nil } } } - end - - let(:content) do - { test: 'deploy $[[ inputs.abc ]]' } - end - - let(:arguments) do - { website: 'gitlab.com' } - end - - it 'correctly interpolates the config' do - subject.interpolate! - - expect(subject).not_to be_valid - expect(subject.errors).to include 'unknown interpolation key: `abc`' - expect(subject.error_message).to eq 'interpolation interrupted by errors, unknown interpolation key: `abc`' - end - end - - context 'when provided interpolation argument is invalid' do - let(:header) do - { spec: { inputs: { website: nil } } } - end - - let(:content) do - { test: 'deploy $[[ inputs.website ]]' } - end - - let(:arguments) do - { website: ['gitlab.com'] } - end - - it 'correctly interpolates the config' do - subject.interpolate! - - expect(subject).not_to be_valid - expect(subject.error_message).to eq subject.errors.first - expect(subject.errors).to include 'unsupported value in input argument `website`' - end - end - - context 'when multiple interpolation blocks are invalid' do - let(:header) do - { spec: { inputs: { website: nil } } } - end - - let(:content) do - { test: 'deploy $[[ inputs.something.abc ]] $[[ inputs.cde ]] $[[ efg ]]' } - end - - let(:arguments) do - { website: 'gitlab.com' } - end - - it 'correctly interpolates the config' do - subject.interpolate! - - expect(subject).not_to be_valid - expect(subject.error_message).to eq 'interpolation interrupted by errors, unknown interpolation key: `something`' - end - end - - describe '#to_hash' do - context 'when interpolation is not used' do - let(:result) do - ::Gitlab::Ci::Config::Yaml::Result.new(config: content) - end - - let(:content) do - { test: 'deploy production' } - end - - let(:arguments) { nil } - - it 'returns original content' do - subject.interpolate! - - expect(subject).not_to be_interpolated - expect(subject.to_hash).to eq(content) - end - end - - context 'when interpolation is available' do + # Remove shared examples when ci_interpolation_inputs_refactor is removed. + shared_examples 'interpolator' do + context 'when input data is valid' do let(:header) do { spec: { inputs: { website: nil } } } end @@ -166,12 +24,189 @@ RSpec.describe Gitlab::Ci::Config::Yaml::Interpolator, feature_category: :pipeli { website: 'gitlab.com' } end - it 'correctly interpolates content' do + it 'correctly interpolates the config' do subject.interpolate! expect(subject).to be_interpolated + expect(subject).to be_valid expect(subject.to_hash).to eq({ test: 'deploy gitlab.com' }) end end + + context 'when config has a syntax error' do + let(:result) { ::Gitlab::Ci::Config::Yaml::Result.new(error: 'Invalid configuration format') } + + let(:arguments) do + { website: 'gitlab.com' } + end + + it 'surfaces an error about invalid config' do + subject.interpolate! + + expect(subject).not_to be_valid + expect(subject.error_message).to eq subject.errors.first + expect(subject.errors).to include 'Invalid configuration format' + end + end + + context 'when spec header is invalid' do + let(:header) do + { spec: { arguments: { website: nil } } } + end + + let(:content) do + { test: 'deploy $[[ inputs.website ]]' } + end + + let(:arguments) do + { website: 'gitlab.com' } + end + + it 'surfaces an error about invalid header' do + subject.interpolate! + + expect(subject).not_to be_valid + expect(subject.error_message).to eq subject.errors.first + expect(subject.errors).to include('header:spec config contains unknown keys: arguments') + end + end + + context 'when interpolation block is invalid' do + let(:header) do + { spec: { inputs: { website: nil } } } + end + + let(:content) do + { test: 'deploy $[[ inputs.abc ]]' } + end + + let(:arguments) do + { website: 'gitlab.com' } + end + + it 'correctly interpolates the config' do + subject.interpolate! + + expect(subject).not_to be_valid + expect(subject.errors).to include 'unknown interpolation key: `abc`' + expect(subject.error_message).to eq 'interpolation interrupted by errors, unknown interpolation key: `abc`' + end + end + + context 'when multiple interpolation blocks are invalid' do + let(:header) do + { spec: { inputs: { website: nil } } } + end + + let(:content) do + { test: 'deploy $[[ inputs.something.abc ]] $[[ inputs.cde ]] $[[ efg ]]' } + end + + let(:arguments) do + { website: 'gitlab.com' } + end + + it 'correctly interpolates the config' do + subject.interpolate! + + expect(subject).not_to be_valid + expect(subject.error_message) + .to eq 'interpolation interrupted by errors, unknown interpolation key: `something`' + end + end + + describe '#to_hash' do + context 'when interpolation is not used' do + let(:result) do + ::Gitlab::Ci::Config::Yaml::Result.new(config: content) + end + + let(:content) do + { test: 'deploy production' } + end + + let(:arguments) { nil } + + it 'returns original content' do + subject.interpolate! + + expect(subject.to_hash).to eq(content) + end + end + + context 'when interpolation is available' do + let(:header) do + { spec: { inputs: { website: nil } } } + end + + let(:content) do + { test: 'deploy $[[ inputs.website ]]' } + end + + let(:arguments) do + { website: 'gitlab.com' } + end + + it 'correctly interpolates content' do + subject.interpolate! + + expect(subject.to_hash).to eq({ test: 'deploy gitlab.com' }) + end + end + end + end + + it_behaves_like 'interpolator' do + context 'when provided interpolation argument is invalid' do + let(:header) do + { spec: { inputs: { website: nil } } } + end + + let(:content) do + { test: 'deploy $[[ inputs.website ]]' } + end + + let(:arguments) do + { website: ['gitlab.com'] } + end + + it 'returns an error' do + subject.interpolate! + + expect(subject).not_to be_valid + expect(subject.error_message).to eq subject.errors.first + expect(subject.errors).to include '`website` input: provided value is not a string' + end + end + end + + context 'when feature flag ci_interpolation_inputs_refactor is disabled' do + before do + stub_feature_flags(ci_interpolation_inputs_refactor: false) + end + + it_behaves_like 'interpolator' + + context 'when provided interpolation argument is invalid' do + let(:header) do + { spec: { inputs: { website: nil } } } + end + + let(:content) do + { test: 'deploy $[[ inputs.website ]]' } + end + + let(:arguments) do + { website: ['gitlab.com'] } + end + + it 'returns an error' do + subject.interpolate! + + expect(subject).not_to be_valid + expect(subject.error_message).to eq subject.errors.first + expect(subject.errors).to include 'unsupported value in input argument `website`' + end + end end end diff --git a/spec/lib/gitlab/ci/interpolation/inputs/base_input_spec.rb b/spec/lib/gitlab/ci/interpolation/inputs/base_input_spec.rb new file mode 100644 index 00000000000..30dbf1ffe51 --- /dev/null +++ b/spec/lib/gitlab/ci/interpolation/inputs/base_input_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Interpolation::Inputs::BaseInput, feature_category: :pipeline_composition do + describe '.matches?' do + it 'is not implemented' do + expect { described_class.matches?(double) }.to raise_error(NotImplementedError) + end + end + + describe '.type_name' do + it 'is not implemented' do + expect { described_class.type_name }.to raise_error(NotImplementedError) + end + end + + describe '#valid_value?' do + it 'is not implemented' do + expect do + described_class.new( + name: 'website', spec: { website: nil }, value: { website: 'example.com' } + ).valid_value?('test') + end.to raise_error(NotImplementedError) + end + end +end diff --git a/spec/lib/gitlab/ci/interpolation/inputs_spec.rb b/spec/lib/gitlab/ci/interpolation/inputs_spec.rb new file mode 100644 index 00000000000..d7d7c85d04f --- /dev/null +++ b/spec/lib/gitlab/ci/interpolation/inputs_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Interpolation::Inputs, feature_category: :pipeline_composition do + let(:inputs) { described_class.new(specs, args) } + let(:specs) { { foo: { default: 'bar' } } } + let(:args) { {} } + + context 'when inputs are valid' do + where(:specs, :args, :merged) do + [ + [ + { foo: { default: 'bar' } }, {}, + { foo: 'bar' } + ], + [ + { foo: { default: 'bar' } }, { foo: 'test' }, + { foo: 'test' } + ], + [ + { foo: nil }, { foo: 'bar' }, + { foo: 'bar' } + ], + [ + { foo: { type: 'string' } }, { foo: 'bar' }, + { foo: 'bar' } + ], + [ + { foo: { type: 'string', default: 'bar' } }, { foo: 'test' }, + { foo: 'test' } + ], + [ + { foo: { type: 'string', default: 'bar' } }, {}, + { foo: 'bar' } + ], + [ + { foo: { default: 'bar' }, baz: nil }, { baz: 'test' }, + { foo: 'bar', baz: 'test' } + ] + ] + end + + with_them do + it 'contains the merged inputs' do + expect(inputs).to be_valid + expect(inputs.to_hash).to eq(merged) + end + end + end + + context 'when inputs are invalid' do + where(:specs, :args, :errors) do + [ + [ + { foo: nil }, { foo: 'bar', test: 'bar' }, + ['unknown input arguments: test'] + ], + [ + { foo: nil }, { test: 'bar', gitlab: '1' }, + ['unknown input arguments: test, gitlab', '`foo` input: required value has not been provided'] + ], + [ + { foo: 123 }, {}, + ['unknown input specification for `foo` (valid types: string)'] + ], + [ + { a: nil, foo: 123 }, { a: '123' }, + ['unknown input specification for `foo` (valid types: string)'] + ], + [ + { foo: nil }, {}, + ['`foo` input: required value has not been provided'] + ], + [ + { foo: { default: 123 } }, { foo: 'test' }, + ['`foo` input: default value is not a string'] + ], + [ + { foo: { default: 'test' } }, { foo: 123 }, + ['`foo` input: provided value is not a string'] + ], + [ + { foo: nil }, { foo: 123 }, + ['`foo` input: provided value is not a string'] + ] + ] + end + + with_them do + it 'contains the merged inputs', :aggregate_failures do + expect(inputs).not_to be_valid + expect(inputs.errors).to contain_exactly(*errors) + end + end + end +end diff --git a/storybook/config/webpack.config.js b/storybook/config/webpack.config.js index a10ae0887ff..d447211cbd8 100644 --- a/storybook/config/webpack.config.js +++ b/storybook/config/webpack.config.js @@ -117,6 +117,10 @@ module.exports = function storybookWebpackConfig({ config }) { config.resolve.extensions = Array.from( new Set([...config.resolve.extensions, ...gitlabWebpackConfig.resolve.extensions]), ); + config.resolve.alias = { + ...config.resolve.alias, + gridstack: require.resolve('gridstack/dist/es5/gridstack.js'), + }; // Replace any Storybook-defined CSS loaders with our custom one. config.module.rules = [ diff --git a/yarn.lock b/yarn.lock index 713d8455812..b423b70ed2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6746,10 +6746,10 @@ graphql@^15.7.2: resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.7.2.tgz#85ab0eeb83722977151b3feb4d631b5f2ab287ef" integrity sha512-AnnKk7hFQFmU/2I9YSQf3xw44ctnSFCfp3zE0N6W174gqe9fWG/2rKaKxROK7CcI3XtERpjEKFqts8o319Kf7A== -gridstack@^7.3.0: - version "7.3.0" - resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-7.3.0.tgz#7b32395edcd885bc39b84068ac86f2831f7a2451" - integrity sha512-JKZgsHzm1ljkn1NnBZpf8j4NDOBCXTuw0m1ZC0sr6NKUh0BFWzXAONIxtX1hWGUVeKLj5l1VcmnTwCXw5ypDNw== +gridstack@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/gridstack/-/gridstack-8.3.0.tgz#4c79f8b8c4cffeb3664266108e38ed91b3d0f7b4" + integrity sha512-RcL2xskAYKOpakvpSwHdKheG7C7YgNY7777C5m+T1JMjSgcmEc3qPBM573l0NuyjMz4Errx1/3p+rMgUfF4+mw== gzip-size@^6.0.0: version "6.0.0"